Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ CDP_API_SECRET=
ROBINHOOD_API_KEY=
ROBINHOOD_PRIVATE_KEY=

# Alpaca keys for the equities paper profile (issue #370 B2). Read by `load_alpaca_secrets`
# from the environment first and this file second, and consumed only when a config's
# `broker:` section selects `name: alpaca` (config.paper-equities.yaml does). PAPER keys
# suffice for the paper profile: generate them from the Alpaca dashboard's paper trading
# account, and `broker.endpoint: paper` selects paper-api.alpaca.markets -- the adapter
# derives the host from that word and accepts no URL, so these cannot be pointed at the
# live venue by any configuration.
ALPACA_API_KEY_ID=
ALPACA_API_SECRET_KEY=

# Optional. Where CRITICAL escalations are POSTed as JSON (ntfy, Pushover, a Slack/Discord
# webhook, anything that accepts a POST body). Unset means alerting is off entirely and keel
# makes no network call for it -- but then `reconcile.position_unprotected` ("this tranche is
Expand Down
97 changes: 97 additions & 0 deletions com.keel.paper-equities.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.keel.paper-equities</string>

<!-- NOTE ON COMMENT STYLE, same as every sibling plist: the rest of this repo separates
asides with a double hyphen. XML forbids a double hyphen inside a comment, and while
Apple's CoreFoundation parser is lenient enough to accept one (plutil -lint says OK),
a strict parser such as expat does not, which makes the file unreadable to ordinary
XML tooling and to the tests that assert on the schedule below. Do not "fix" it back.

EQUITIES PAPER profile (issue #370 Phase B2): one paper cycle per day against
config.paper-equities.yaml and its OWN database keel-equities.db (pinned by
paper-equities-run.sh), trading the daily turtle rules on US equities through
Alpaca's PAPER API. It places NOTHING real. It exists because every profile so far
exercises the engine on one venue and one asset class: this one accrues evidence
(rail vetoes, outcomes, pending lifespans, intent divergence) on a SESSION-BOUND
venue and a second asset class. NO PROVEN EDGE ON ANY ASSET CLASS: the crypto
configurations are measured net-negative and these rules have never been measured
on equities at all. Evidence, not profit.

WHY INSIDE THE SESSION AND NOT SHORTLY AFTER THE CLOSE. The US regular session runs
09:30 to 16:00 ET, and the engine's session gate (#370 B1, PR #385) skips the WHOLE
cycle whenever the venue clock answers closed, so a trigger at, say, 16:40 would log
market_closed and never evaluate a bar: zero evidence, forever. The daily bar that
closes at 16:00 ET is therefore evaluated at the NEXT session's open, which is also
how daily breakout systems are conventionally run: signal on the close, execute at
the next open. The 10:00 anchor gives the 09:30 open thirty minutes to settle, and
every trigger at 10:00-15:00 local falls inside the session.

THE SIX TRIGGERS ARE CATCH-UP BREADTH, NOT EXTRA CYCLES. launchd re-runs a missed
StartCalendarInterval on wake from SLEEP but NOT when the trigger passed while the
machine was powered OFF, so a machine down at 10:00 would otherwise lose the day.
The runner is day-stamped: the first trigger that finds no stamp for the UTC day
runs the cycle, every later one is a no-op, and a FAILED cycle leaves the day
unstamped so the next hour retries. The runner's own window guard (the window is
10:00 inclusive to 16:00 exclusive, LOCAL: the 15:00 trigger runs, the 16:00 one
does not) keeps a RunAtLoad boot outside the session from stamping a closed-market
skip as the day's work and suppressing the real evaluation.

ET ANCHORING AND THE DST CAVEAT, STATED HONESTLY. The deployment machine's local
zone is America/New_York, so a fixed local trigger time keeps its Eastern meaning
across BOTH US DST transitions: the zone shifts with ET, and what moves is the UTC
instant (10:00 ET is 14:00 UTC in summer, 15:00 UTC in winter), never the distance
from the 09:30 open. That makes this schedule correct on an ET-anchored host, or
on one within about four hours of ET where the trigger hours still land inside the
09:30-16:00 ET session. On any other host it is not "slightly drifted", it is
WRONG: a machine far enough ahead of ET can fire every trigger pre-open, and the
runner's local-hours guard will endorse those triggers (the guard reads the HOST's
clock, so it cannot see that they sit outside the ET session), stamping each day
with a closed-market skip: permanently zero evidence. If this profile moves to
another host, re-anchor the trigger hours so they land 10:00-15:00 ET; the
local-hours guard is a backstop against off-schedule boots, not a drift absorber.

WEEKENDS AND HOLIDAYS ARE QUIET BY DESIGN: the venue clock answers closed, the
agent skips with reason market_closed (recording the session, which keeps the
fetch command's staleness check non-alerting through the closure), and the day is
stamped. A clock that cannot be READ is the other skip kind, stamped differently:
the agent exits MARKET_CLOCK_UNAVAILABLE_EXIT (nonzero) for it, so the runner
leaves the day unstamped and the next trigger retries once the clock answers
again. A trading day lost to the
machine being powered off is lost: the runner cannot replay bars that closed while
it was down. That is a day of evidence, not a day of money. -->
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/elmehdiaitbrahim/keel/paper-equities-run.sh</string>
</array>

<key>StartCalendarInterval</key>
<array>
<dict><key>Hour</key><integer>10</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Hour</key><integer>11</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Hour</key><integer>12</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Hour</key><integer>13</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Hour</key><integer>14</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Hour</key><integer>15</integer><key>Minute</key><integer>0</integer></dict>
</array>

<!-- Run on load, so a boot inside the window runs the day's cycle immediately instead of
waiting for the next trigger. A boot OUTSIDE the window is refused by the runner's
guard (an early-morning closed-market skip exits 0 and must not stamp the day). The
day-stamp makes a repeated load harmless. -->
<key>RunAtLoad</key>
<true/>

<key>WorkingDirectory</key>
<string>/Users/elmehdiaitbrahim/keel</string>

<key>StandardOutPath</key>
<string>/Users/elmehdiaitbrahim/keel/logs/paper-equities.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/elmehdiaitbrahim/keel/logs/paper-equities.err.log</string>
</dict>
</plist>
167 changes: 167 additions & 0 deletions config.paper-equities.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# keel runtime configuration — the EQUITIES paper profile (issue #370, Phase B2).
#
# The fourth deployment profile, and the first on a SECOND ASSET CLASS: keel's daily turtle
# rules running on US equities through Alpaca's PAPER trading API (`broker:` below selects
# the alpaca adapter, its paper endpoint and the IEX data feed). It runs on its OWN database,
# keel-equities.db, pinned to this config by the keel-equities wrapper — never in keel.db
# (the daily crypto paper account) or keel-live.db. Bootstrap commands are in
# docs/operator-runbook.md, "The equities paper profile".
#
# THE ALLOWLIST IS PAPER CANDIDATES, NOTHING MORE. MSFT, AAPL, GOOGL, NVDA and COST are
# chosen for LIQUIDITY (deep daily books, long split-adjusted history, IEX data quality on
# mega-caps) and for being the kind of liquid large cap an equity screen can be RUN on —
# not because any of them has been screened, and not because any of them is expected to be
# profitable. Classification is OPERATOR-ATTESTED per (alpaca, SYMBOL), from attributed
# sources, before any live consideration (see the runbook's attestation semantics: AAOIFI /
# IFSB-class standards are the watch); the engine never classifies, and this file
# asserts nothing religiously. Trading these names here is paper evidence collection, full
# stop.
#
# HONEST CAVEAT, STATED UP FRONT: there is NO PROVEN EDGE on any asset class. The crypto
# configurations are measured net-negative on their own clocks, and these rules have never
# been measured on equities at all — nothing in this file changes either fact. This profile
# exists to accrue ADMISSIBLE EVIDENCE — rail vetoes, outcomes, pending lifespans, intent
# divergence, this time on a session-bound venue and a second asset class — not profit. Do
# not read a positive stretch as anything but noise.
#
# A DAILY CLOCK. market_data.granularities is ONE_DAY ONLY: the daily turtle rules trade
# ONE_DAY bars, and hourly bars exist only within sessions (Alpaca mints them 09:30–16:00 ET
# on trading days), so they are not needed by daily rules and polling them would add rate-
# limit surface for nothing. interval_sec: 86400 is the matching cadence — and it scales the
# feed-staleness window, which B1's session awareness (#385) reads closed-explained through:
# a weekend is "market closed", never "feed stale".
#
# `allowlist` and `caps` are required and validated by keel.config.load_config; missing or
# invalid values raise ConfigError naming the offending key rather than silently defaulting.

allowlist:
- MSFT
- AAPL
- GOOGL
- NVDA
- COST

# Flat 20% each, summing to exactly 1.000. Flatness states NO view: nobody has earned a
# conviction weighting on an asset class nothing has been measured on, and every name's
# shariah classification is unrecorded (that is the operator's attestation, not this file).
# It is the sizing half of the same guardrail logic as the hourly profile's flat Tier-2
# caps — a candidate can only ever be a 20% position while it is still a candidate.
target_weights:
MSFT: 0.200
AAPL: 0.200
GOOGL: 0.200
NVDA: 0.200
COST: 0.200

risk_pct: 0.01

# The venue this deployment talks to — the whole of the venue-selection surface. `name`
# resolves through the keel.brokers entry points (the deployment must have keel-broker-alpaca
# installed); `endpoint: paper` selects Alpaca's PAPER host (paper-api.alpaca.markets — the
# adapter derives the host from this word and accepts no URL, so a paper credential cannot
# be pointed at the live venue); `data_feed: iex` declares the free data tier (sip is the
# subscribed tier — the choice is declared, never assumed, because the venue's server-side
# default silently fails for keys without the subscription). Omitting the section entirely
# selects Coinbase, byte-compatibly with every other profile.
broker:
name: alpaca
endpoint: paper
data_feed: iex

caps:
# Same non-binding internal limits as the crypto paper profiles — deliberately NOT tighter,
# for the same reason: this profile's evidence is partly ABOUT rail behaviour (rail vetoes
# are admissible evidence), so silently clamping sizing here would bias exactly the thing
# being measured.
max_exposure_usd: 5000
max_per_asset_pct: 0.50

market_data:
# See the header: ONE_DAY ONLY — a daily-clock equities profile. history_days mirrors the
# crypto paper profiles so a year of warm cache backs the daily lookbacks.
granularities:
- ONE_DAY
history_days: 365

auto_trade:
mode: paper
# NOTE: currently UNUSED by any code path -- it is NOT a kill-switch and setting it
# true or false changes nothing. Use `keel kill` to halt trading.
enabled: false
# One cycle per UTC day. The LaunchAgent (com.keel.paper-equities.plist) triggers daily
# at 10:00-15:00 local, inside the US regular session (the runner's window is 10:00
# inclusive to 16:00 exclusive, so the 15:00 trigger runs), because B1's session gate
# skips any cycle the venue clock answers closed — see that plist's comment for the full
# reasoning. That schedule is correct on an ET-anchored host (or one within ±4h of ET);
# on any other host, re-anchor the trigger hours to land 10:00-15:00 ET — the runner's
# local-hours guard is a backstop against off-schedule boots, not a drift absorber. This
# value also scales the staleness window (86400 x FEED_STALENESS_CYCLES), which is what
# keeps weekends and holidays reading market-closed instead of stale.
interval_sec: 86400

promotion:
min_trades: 100
min_expectancy: 0.0
min_rr: 1.5
min_win_rate: 0.55

money_mgmt:
profit_trigger_pct: 0.10
acceleration_pct: 0.05
max_total_dd_pct: 0.20
max_weekly_dd_pct: 0.08
# Rail 16 (consecutive-loss breaker) — DISABLED by default (0 = off), same as the siblings.
max_consecutive_losses: 0
streak_cooloff_days: 0

dca:
budget_usd: 50
cadence_days: 7

paper:
# Same seed/contribution as the crypto paper profiles, deliberately: a like-for-like
# comparison of rail behaviour across venues wants the same sizing basis, and changing it
# here would confound exactly that. At $10k+ synthetic equity, rail 14's $500/month
# allowance can veto large sized setups — those vetoes are themselves evidence.
starting_equity_usd: 10000
monthly_contribution_usd: 500

# Equities settle in USD; Alpaca only trades USD-quoted symbols (the adapter refuses any
# other quote leg), so this is not a choice so much as a restatement of the venue.
quote_currency: USD

subscription:
# Rail 14's simulator assumptions. Alpaca has no subscription tiers — commission-free
# trading with regulatory fees on sells — so the tier CATALOGUE below stays at its
# Coinbase defaults (rail 14 reads the ATTESTED record, not this catalogue, and a live
# equities path would restate all of this before it mattered; see the runbook).
assumed_free_volume_usd: 500
unsubscribed_allowance_usd: 0
pacing: opportunistic

# Alpaca equities are commission-free: $0 per trade, so paper fills price at a 0% commission
# here — the honest statement about commission on this venue. What this does NOT model is
# the sell-side regulatory fees (SEC Section 31 + FINRA TAF, passed through on sells) and
# the spread; the adapter's preview estimates the former from the venue's own published
# formulas, and Phase C's cost-fidelity work re-measures all of it before any strategy
# evaluation on this asset class is believed (PRD §6.3).
fees:
taker_pct: 0.0
maker_pct: 0.0

# Daily cadence means one cycle per trading day, but rail vetoes and no-signal reasons still
# arrive every cycle and verbose=false would make "why no order today?" a manual database
# replay. Rotation bounds the volume. (Weekends and holidays are quiet by design — B1.)
logging:
verbose: true
file: logs/keel-equities.log
max_file_mb: 25
file_count: 5

# G4 overfitting gate (KB §78). NEVER tune these to obtain a desired verdict. Same floors as
# every other profile; this profile's rows are EXPECTED to fail them — an unmeasured asset
# class is the definition of no edge, and that failure is the finding, not a configuration
# problem.
research:
pbo_max: 0.05
slope_floor: -0.5
Loading