feat(paper-equities): the equities paper profile — tracked config, venue selection, deployment artifacts, runbook (#370 B2) - #386
Merged
Conversation
…nue selection, deployment artifacts, runbook (#370 B2) Phase B2 of #370 (PRD docs/superpowers/specs/2026-08-18-keel-broker-alpaca-prd.md, Phase B / §5): keel's daily turtle rules on US equities via Alpaca's PAPER API, on their own database, accruing hash-chained paper evidence end-to-end. Deliverables: - config.paper-equities.yaml — the tracked profile: broker alpaca/paper/iex, ONE_DAY only (a daily clock; hourly bars exist only within sessions and daily rules do not read them), interval_sec 86400, weights summing to 1.000, risk_pct 0.01, caps mirroring the paper profiles, and the paper-hourly-style honesty caveat in the header. - Deployment artifacts mirroring the paper-hourly trio: com.keel.paper-equities .plist (strict-XML validated), paper-equities-run.sh (UTC-day stamp, set -e, stamp only after success, local-hour window guard), keel-equities wrapper (config+db travel as a pair). - Runbook: "The equities paper profile" — bootstrap, the fourth profile-comparison column, attestation semantics (operator-supplied per (alpaca, SYMBOL), AAOIFI/ IFSB-class sources, the engine never classifies), rail 17 as ACATS transfer-out, the T+1 x daily-cadence interaction (immaterial for entries; exits never spend), and the two operator-verified opt-outs (stock lending OFF for qabd, high-yield sweep OFF for riba). .env.example gains the ALPACA_* key names. Venue selection — the minimal engine wiring the profile needs. New `broker:` config section (name -> keel.brokers entry-point discovery; endpoint paper|live; data_feed iex|sip, both validated at load) threaded through the one broker- construction seam the CLI/agent/TUI paths share, keel/commands/_common.py:: _build_broker, plus telemetry's bind_venue in _load_cfg. Secrets follow the CDP pattern: load_alpaca_secrets reads ALPACA_API_KEY_ID/ALPACA_API_SECRET_KEY from the environment or .env, and the missing-keys error names the venue and both vars. WHEN THE SECTION IS ABSENT the construction path is byte-compatible Coinbase — pinned by test against monkeypatched RESTClient/CoinbaseClient, and every pre-existing test stays green unchanged. The config golden baseline is regenerated (deliberately, per its own docs) for the new field. The allowlist is PAPER CANDIDATES: MSFT, AAPL, GOOGL, NVDA, COST — chosen for liquidity and as the kind of low-debt large cap a screen COULD be run on, flat 20% each (no view). Classification is operator-attested per (alpaca, SYMBOL) before any live consideration; the engine never classifies and the file asserts nothing religiously. There is NO PROVEN EDGE on any asset class; the profile exists for evidence, not profit. Schedule note, deliberate deviation from the obvious copy-paste: the plist fires 10:00-15:00 local (ET) — INSIDE the US regular session, not ~30-60 min after the 16:00 close — because B1's session gate skips the whole cycle whenever the venue clock answers closed, so an after-close trigger would log market_closed and never evaluate a bar. The daily bar is evaluated at the next session's open (signal on close, execute next open); the runner's window guard keeps a pre-open/after-close closed-market skip (exit 0) from being stamped as the day's work. The host is America/New_York, so fixed local triggers keep their ET meaning across both US DST transitions. Deliberately NOT here: keel/assets screening venue semantics stay hardcoded to coinbase (#233 live-path work; runbook open item — equity classifications live in the operator's records, the profile trades unattested paper candidates); deployment to the operator's machine (needs their Alpaca paper credentials — the runbook is the bootstrap); Phase C cost fidelity/DCA benchmark. Refs #370
…ing docs, clock-failure retry Eight adversarial-review findings on PR #386 (#370 B2): 1. Rail 14 keyed every BUY on the hardcoded DEFAULT_VENUE (coinbase), so an alpaca deployment was gated on a coinbase record nothing writes (out of the box: $0 allowance, every BUY vetoed with advice naming the wrong venue). The rail now reads the deployment's venue through the SAME ContextVar binding _load_cfg makes for telemetry (bind_venue(config.broker.name), via current_venue()); coinbase remains the answer when nothing is bound. subscription attest/set default --venue to the bound venue and show's empty advice names it. 2. Runbook gains the PRD 5/6.4 account-posture section: cash accounts only (margin borrowing is riba; also sidesteps PDT's $25k margin-account threshold), PDT explained, T+1 cross-referenced not duplicated, enforcement fenced to #372. 3. Runbook fences dividend purification (FR-10 events, math against the attested ratio, recorded disposition) as the Phase B3 slice. 4. assets holdings auth advice is venue-aware: alpaca names ALPACA_API_KEY_ID/ ALPACA_API_SECRET_KEY; coinbase keeps the CDP advice. 5. keel agent (single-cycle) exits MARKET_CLOCK_UNAVAILABLE_EXIT (5) on a market_clock_unavailable skip, mirroring DATA_NOT_READY_EXIT, so the runner's set -e declines to stamp the UTC day and the next trigger retries; market_closed still exits 0 and stamps (correct cadence bookkeeping). Runner comments state the two skip kinds. 6. DST caveat restated honestly in plist/runbook/config: the schedule is correct on an ET-anchored host (or within ±4h); elsewhere re-anchor triggers to 10:00-15:00 ET; the local-hours guard is a backstop, not a drift absorber. 7. Window wording aligned everywhere: triggers at 10:00-15:00 local; the runner's window is 10:00 inclusive to 16:00 exclusive (the 15:00 trigger runs). 8. Typo (ABSSENCE) and 'low-debt large cap' phrasing (an unattested leverage fact) replaced with 'liquid large caps', disclaimer intact.
This was referenced Aug 19, 2026
Merged
eaitbrahim
added a commit
that referenced
this pull request
Aug 20, 2026
Minor, not patch. Three things since v0.9.3 change what an implementor or a deployment can rely on: * A NEW DISTRIBUTION. `keel-broker-alpaca` (#382, #384) plus the paper-equities profile that selects it (#386), so a deployment can now be US equities via the broker port rather than crypto only. * THE PORT CONTRACT MOVED TWICE. `market_clock`/`market_schedule` made venues session-aware (#385), and `place_order` gained `idempotency_key` (#419). Both carry defaults so no CALLER breaks, but a third-party adapter that does not accept them is no longer a `Broker` -- the conformance suite now says so. That is exactly the kind of change a patch bump must not hide. * THE OPERATOR CONSOLE. The TUI became keel's console across #399-#408, and `keel update` (#415/#417) makes a deployment self-updating. Every pinned sibling moves with it. The four production distributions are required `==` at this exact version (`RELEASING.md`, "Release assets"), so a bump that missed one would install a mixed set -- the `keel-trader 0.5.7` against `keel-core 0.5.5` failure `keel versions` exists to catch, and which `~/keel` actually ran across two releases. Also in this window, on the Robinhood adapter: the best_bid_ask fixture corrected against the live venue (#414), a credential guard that catches the error it only claimed to (#416), pre-flight sizing reported on the preview (#418), transport backoff (#420), and the fenced one-order probe (#421). Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Phase B2 of #370 (PRD
docs/superpowers/specs/2026-08-18-keel-broker-alpaca-prd.md, Phase B / §5, FR-9/FR-10 posture): the equities paper profile — keel's daily turtle rules on US equities via Alpaca's PAPER API, on their own database, accruing hash-chained paper evidence end-to-end — plus the minimal engine wiring it needs: config-driven venue selection.The profile (tracked, mirrors the paper-hourly pattern)
config.paper-equities.yaml—broker: {name: alpaca, endpoint: paper, data_feed: iex};market_data.granularitiesis ONE_DAY only (a daily clock — hourly bars exist only within sessions and daily rules do not read them; stated in comments);interval_sec: 86400; weights sum to exactly 1.000;risk_pct: 0.01; caps mirroring the paper profiles; the paper-hourly-style honesty caveat in the header.com.keel.paper-equities.plist(strict-XML validated; schedule below),paper-equities-run.sh(UTC-day stamp,set -e, stamp only after a successful cycle, session-window guard),keel-equitieswrapper (config +keel-equities.dbtravel as a pair).docs/operator-runbook.mdgains "The equities paper profile": why it exists (evidence on a session-bound venue; the same honest no-edge caveat), bootstrap (Alpaca paper account, env vars,keel migrate --db keel-equities.db,rules add --kind turtle_breakout --product SYMBOL --params '{"granularity": "ONE_DAY"}'+rules promote --forcewith the documented bypass rationale,fetchwarm — B1 makes weekends quiet), the fourth profile-comparison column, attestation semantics (operator-supplied per(alpaca, SYMBOL), AAOIFI/IFSB-class sources per the fiqh source review; the engine never classifies), rail 17 as ACATS transfer-out, the T+1 × daily-cadence interaction (immaterial for entries; exits never spend), and the two operator-verified opt-outs (stock lending OFF — qabd; high-yield sweep OFF — riba; where to verify each in the Alpaca dashboard)..env.examplewires the new key names.Venue selection (the engine half)
A new
broker:config section —name(→keel.brokersentry-point discovery),endpoint(paper|live),data_feed(iex|sip), both validated at load — threaded through the one broker-construction seam every CLI/agent/TUI path shares (keel/commands/_common.py::_build_broker), plus telemetry'sbind_venuein_load_cfg. Secrets follow the CDP pattern:load_alpaca_secretsreadsALPACA_API_KEY_ID/ALPACA_API_SECRET_KEYfrom the environment or.env; the missing-keys error names the venue and both variables.When the section is absent, the Coinbase construction path is byte-compatible — pinned by a test that drives the real
_build_brokeragainst monkeypatchedRESTClient/CoinbaseClientand asserts the exact kwargs — and every pre-existing test stays green unchanged. The config golden baseline is regenerated deliberately (its own documented step) for the new field.The candidate set — PAPER CANDIDATES
MSFT, AAPL, GOOGL, NVDA, COST — chosen for liquidity (deep daily books, long split-adjusted history, IEX data quality on mega-caps) and as the kind of low-debt large cap a screen could be run on, flat 20% each (flatness states no view). Classification is operator-attested per
(alpaca, SYMBOL)before any live consideration; the engine never classifies and this file asserts nothing religiously. There is NO PROVEN EDGE on any asset class; the profile exists for evidence, not profit.Schedule — a deliberate deviation, stated
The plist fires 10:00–15:00 local (ET), inside the US regular session — not ~30–60 min after the 16:00 ET close. Reason: B1's session gate (#385) skips the whole cycle whenever the venue clock answers closed, so an after-close trigger would log
market_closedand never evaluate a bar — zero evidence, forever. The daily bar that closes at 16:00 ET is evaluated at the next session's open (signal on close, execute next open — the conventional daily-system semantics). The runner's window guard (local hours 10–15) keeps a pre-open/after-close closed-market skip (which exits 0) from being stamped as the day's work. DST: the host's zone is America/New_York, so fixed local triggers keep their ET meaning across both US transitions (what moves is the UTC instant, never the distance from the open); the caveat for other hosts is in the plist comment and the runbook.Deliberately NOT here
keel/assetsscreening venue semantics stay hardcoded tocoinbase(that hardcoding is deliberate, Venue visibility must be capability-based, not key-presence-based — a read-only key looks identical to a working one #233 live-path work; recorded as the runbook's open item — equity classifications live in the operator's records, the profile trades unattested paper candidates).Test-first + gates
tests/test_paper_equities_profile.py(35 tests) was written red first — config/broker parsing, alpaca construction, missing-secrets error, plist strict-parse + triggers, the runner harness (real script, date shim, stubbed keel) including the pre-open/after-close stamp regressions, wrapper, runbook pins — then drove the implementation. The byte-compat default pin passed before and after, as a pin should.uv run pytest -q— 3225 passed, 2 skipped (both pre-existing expected skips)uv run ruff check keel tests packages— cleanuv run mypy— clean (250 files)Refs #370 (Phase C continues — not a fix)