18 KiB
Trading TDD Plan — Progress Tracker
Companion to TRADING_DEVELOPMENT_PLAN.md. Each step follows Red → Green → Confirm before moving on.
How agents should use this file:
- Pick the first unchecked step in order.
- Write the failing test (Red), implement minimal code (Green), run the Confirm gate.
- Check off boxes and add a one-line note under Progress log with date and result.
- Do not skip Confirm gates or start Phase 2 until Gate B passes.
Overall status
| Milestone | Status | Notes |
|---|---|---|
| Step 0 — Test harness | ✅ Done | 2026-05-23 |
| Steps 1–11 — Phase 1 MVP | 🔄 In progress | Day 7: Steps 9–10 done; Gate A scripted test green |
| Gate A — Local loop (test mode) | ✅ Done | 2026-05-26 — trading_orchestrator_gate_a_test.dart |
| Gate B — Paper Alpaca E2E | ⬜ Not started | |
| Steps 12–15 — Phase 2 | ⬜ Not started | |
| Steps 16–19 — Phase 3 | ⬜ Deferred |
Legend: ⬜ Not started · 🔄 In progress · ✅ Done · ⏸ Blocked
Progress log
| Date | Step | Result |
|---|---|---|
| 2026-05-26 | 10 + Gate A | dart test green (57 tests); TradingOrchestrator (ingest → evaluate → actuate per user) + QuestionBackgroundWorker integration; Gate A scripted test (seed → tick → question → +10 → tick → trade_orders row → tick → cooldown) passing in test mode |
| 2026-05-26 | 9 | dart test green (56 tests); AlpacaTradingClient (POST /v2/orders, GET by_client_order_id, dup detection) + TradeActuator (test mode + mocked Alpaca, guardrails, idempotency); tagged alpaca live roundtrip script |
| 2026-05-25 | 7–8 | dart test green (47 tests); TradingPipeline evaluate/handleAnswer + QuestionPipeline branch |
| 2026-05-25 | 5–6 | dart test green (39 tests); rule engine + guardrails (pure logic) |
| 2026-05-23 | 3–4 | dart test green (21 tests); Alpaca MD client, ingest, poll interval; live Alpaca OK |
| 2026-05-23 | 1.3–1.4, 2 | dart test green (17 tests); config merge, trade orders, Alpaca env/models |
| 2026-05-23 | 0, 1.1–1.2 | dart test green (4 tests); migration 004 + MarketDataDb |
Principles
| Principle | How it applies |
|---|---|
| One behavior per cycle | One failing test → minimal code → gate check |
| No Alpaca in unit tests | Mock HTTP clients; inject fixture JSON |
| DB tests are integration | Real Postgres test DB (cyberhybridhub_test) |
| E2E gates are manual/scripted | Flutter swipe + Alpaca paper dashboard at defined checkpoints |
| Feature flags off until wired | TRADING_ENABLED=false until Step 10 complete |
Step 0 — Test harness
0.1 Server test setup
- Add
dev_dependencies: test: ^1.25.0toserver/pubspec.yaml - Create
server/test/smoke test - Create
server/test/helpers/fixture_loader.dart - Create
server/test/helpers/mock_http_client.dart - Create
server/test/helpers/test_env.dart - Create fixture files:
server/test/fixtures/alpaca_latest_trade.jsonserver/test/fixtures/alpaca_daily_bars.jsonserver/test/fixtures/trading_config_default.jsonserver/test/fixtures/market_snapshots_spy_dip.json
Red: Smoke test fails until layout exists.
Green: Minimal test directory and helpers.
Confirm: cd server && dart test passes with 1 smoke test.
0.2 Integration test DB
- Test helper applies migrations 001–004 on
cyberhybridhub_test server/test/integration/migration_test.dart— migration applies cleanly
Red: Migration test fails (no 004_trading.sql yet).
Green: Migration runner + empty-schema apply.
Confirm: dart test test/integration/migration_test.dart passes locally and in CI.
0.3 Flutter (minimal)
- No new Flutter tests until Step 9 — existing
SwipeQuestionTile+ SignalR contract unchanged
Phase 1 — Foundation (MVP)
Maps to TRADING_DEVELOPMENT_PLAN §5, §6, §7, §8, §9, §10, §14 Phase 1.
Step 1 — Schema & DB accessors
Refs: §5 Postgres schema · market_data_db.dart · trading_config_db.dart · trade_orders_db.dart
1.1 Migration
- Red: Integration test — INSERT into
market_data_snapshots,user_trading_config,trade_orders; FK tousersenforced - Green:
server/migrations/004_trading.sql - Green: Seed
trading_config_templatesrowdefault_paper_watchlist - Confirm:
\d market_data_snapshotsin psql; seed template queryable
1.2 MarketDataDb
- Red:
insertSnapshot()thenlatestForSymbol(symbol, metric)returns newest row byas_of - Green:
server/lib/trading/market_data_db.dart - Confirm: Two SPY/
last_tradeinserts → latest returns newer price
1.3 TradingConfigDb
- Red:
resolveEffectiveConfig(uid)merges template + user JSONB; user override wins - Green:
server/lib/trading/trading_config_db.dart - Green:
server/lib/trading/trading_config.dartparser - Confirm: Partial user override yields merged
enabled,data_inputs,rules
1.4 TradeOrdersDb
- Red: Duplicate
client_order_idraises unique violation;findByClientOrderIdreturns existing row - Green:
server/lib/trading/trade_orders_db.dart - Confirm: Idempotency check prevents double-insert
Step 2 — Alpaca env & models
Refs: §3 · §11 · alpaca_env.dart · alpaca_models.dart
- Red:
AlpacaEnv.fromMap()reads keys;assertPaperOnly()throws when live URL +ALPACA_ALLOW_LIVE=false - Green:
server/lib/alpaca/alpaca_env.dart - Green:
server/lib/alpaca/alpaca_models.dart(Trade, Bar, OrderRequest) - Green: Extend
server/lib/env.dartwith trading flags (after env tests pass) - Confirm: Unit tests — paper default, live blocked, missing keys
Step 3 — Alpaca Market Data client (REST)
Refs: §9.1 · alpaca_market_data_client.dart
- Red: Mock HTTP —
getLatestTrade('SPY')parses fixture; sendsAPCA-API-KEY-IDheaders - Green:
server/lib/alpaca/alpaca_market_data_client.dartwith injectablehttp.Client - Confirm: Optional tagged test
@Tags(['alpaca'])— one real call (skipped in CI)
Step 4 — Snapshot normalization & ingest
Refs: §9.3 · market_data_ingest.dart
- Red: Config
data_inputswith[last_trade, daily_bar, prev_close]→ 3 snapshot rows with correctmetric,price,as_of - Green:
server/lib/trading/market_data_ingest.dart—runIfDue()writes viaMarketDataDb - Confirm: Integration — ingest for seeded user;
SELECT * FROM market_data_snapshots WHERE symbol='SPY'
4.1 Poll interval
- Red: Second call within
poll_interval_secondsdoes not fetch (usesuser_trading_state.context) - Green: Update
user_trading_stateon ingest - Confirm: Two rapid
runIfDuecalls → mock HTTP call count = 1
Step 5 — Rule engine (pure logic)
Refs: §4.2 · §8 · rule_engine.dart
- Red:
price_below_pct_of_ref— SPYlast_trade=492,prev_close=500, threshold-1.5→ fires withpct ≈ -1.6 - Green:
server/lib/trading/rule_engine.dart—RuleEngine.evaluate(rule, snapshots) → RuleEvaluation? - Confirm: Table-driven unit tests:
| Case | Expected |
|---|---|
| Above threshold (-0.5%) | No fire |
| Missing metric | No fire |
Stale as_of (> max_staleness) |
No fire |
| Cooldown: rule fired today | No fire |
Template {{pct}}, {{symbol}}, {{price}} |
Substituted |
Step 6 — Guardrails
Refs: §8.3
- Red:
Guardrails.check()rejects whenmax_orders_per_dayexceeded - Red: Rejects blocklisted symbol
- Red: Rejects when
require_question_before_orderviolated - Green:
server/lib/trading/guardrails.dart - Confirm: Server-side max notional ceiling enforced even if config allows more
Step 7 — Trading pipeline — question creation
Refs: §8.1 · trading_pipeline.dart · QuestionService
- Red: Rule fires + guardrails pass + queue room →
createAndDeliverQuestionwithpipeline_key=trading,pipeline_step=dip_confirm:await_confirm,correct_answer=10, substituted text - Green:
server/lib/trading/trading_pipeline.dart—evaluate()with mocked deps - Confirm: Integration — seeded snapshots + config → row in
questionswith correct tags - Green:
user_trading_state.contextrecords pending rule id and phase
Step 8 — Answer handling → order proposal
Refs: §8.2 · extend QuestionPipeline.onAnswerSubmitted
- Red:
pipeline_key=trading, user+10,BranchDecision.yesNo→ match → stages order (submit_order), no Alpaca yet - Red: User
-10→ skip logged, no order - Green:
PipelineKeys.tradinginquestion_pipeline.dart - Green:
_handleTradingAnswerinquestion_pipeline.dart(delegates toTradingPipeline.handleAnswer) - Confirm: Integration test exercises the
QuestionPipeline.onAnswerSubmittedswitch forpipeline_key=trading
Step 9 — Trade actuator
Refs: §10 · alpaca_trading_client.dart · TradeActuator
- Red: Mock HTTP —
submitOrder(notional: 10, side: buy)POSTs paper URL withclient_order_id;trade_ordersrowstatus=accepted - Red: Duplicate
client_order_id→ 422 → resolved viagetOrderByClientOrderId - Green:
server/lib/alpaca/alpaca_trading_client.dart - Green:
server/lib/trading/trade_actuator.dart(test mode short-circuit + Alpaca path) - Confirm: Tagged integration test (
server/test/alpaca/alpaca_trading_live_test.dart) — paper roundtrip, skipped without credentials - Confirm:
client_order_idformat{firebase_uid}-{rule_id}-{question_id}unique (set byTradingPipeline.handleAnswer, enforced bytrade_orders.client_order_id UNIQUE)
Step 10 — Worker integration
Refs: §7 · question_background_worker.dart · trading_orchestrator.dart
- Red:
TRADING_ENABLED=true, test mode (no Alpaca client) — one orchestrator tick runs evaluate + actuate against fixture snapshots - Green:
server/lib/trading/trading_orchestrator.dart(ingest → evaluate → actuate, per-stage failure isolation) - Green:
QuestionBackgroundWorkeraccepts optionalTradingOrchestrator, runs it afterQuestionPipeline.runMaintenanceCycle - Green:
bin/server.dartbuilds the trading stack only whenTRADING_ENABLED=true; uses real Alpaca clients only whenQUESTION_PIPELINE_TEST_MODE=falseand credentials present - Confirm: Gate A below —
test/integration/trading_orchestrator_gate_a_test.dartgreen
Step 11 — End-to-end with real Alpaca paper
Refs: §17 · §15 integration
- Red: Manual/scripted E2E checklist (or tagged automated test)
- Green: Real keys in
.env,QUESTION_PIPELINE_TEST_MODE=false - Confirm: Gate B (see below)
Gate A — Local loop (test mode)
Prerequisite: Steps 0–10 complete.
Automated as server/test/integration/trading_orchestrator_gate_a_test.dart.
- Seed user with
user_trading_config.enabled=trueand dip rule - Insert fixture snapshots (or test-mode ingest)
- Run one worker tick → question queued
- Submit answer +10 via API
- Run next tick →
trade_ordersrow (fake Alpaca id in test mode) - Third tick → rule does not re-fire (cooldown)
- Set
TRADING_ENABLED=truein dev.envonly after Gate A passes (manual — flip the flag when ready to start using the live stack against paper Alpaca)
Gate B — Paper Alpaca E2E
Prerequisite: Gate A complete.
- Ingest real SPY prices
- Rule fires (threshold or market conditions)
- Flutter: question via SignalR
- User swipes +10
- Order visible in Alpaca paper UI
trade_orders.question_idandrule_idpopulated
Phase 2 — Configuration API & hardening
Start only after Gate B passes. Maps to TRADING_DEVELOPMENT_PLAN §13, §14 Phase 2.
Step 12 — Config validation
- Red: Validator rejects >30 symbols, missing
rules[].id, invalidmode - Green:
TradingConfigValidator - Confirm: Invalid JSON → 400 on PUT
Step 13 — HTTP endpoints
- Red: Handler tests —
GET/PUT /v1/me/trading/config - Red:
GET /v1/me/trading/orders - Red:
GET /v1/me/trading/snapshots?symbol=SPY - Green:
server/lib/handlers/trading_handler.dart - Confirm: curl with Firebase token; sanitized config (no secrets)
Step 14 — Daily guardrail counters
- Red: After order submit,
context.daily_order_countincrements; resets UTC midnight - Green: Counter in
user_trading_state.context - Confirm: Third order same day blocked when
max_orders_per_day=3
Step 15 — Order status polling
- Red: Mock Alpaca status
filled→trade_orders.statusandfilled_atupdated - Green: Poll on worker tick or post-submit
- Confirm: Failed order → follow-up question (optional)
Phase 3 — Streaming & observability
Deferred until Phase 2 stable. Maps to TRADING_DEVELOPMENT_PLAN §9.2, §14 Phase 3.
| Step | Red test | Green impl | Done |
|---|---|---|---|
| 16 WS ingest | Mock WS message → snapshot upsert | alpaca_ws_ingest.dart |
⬜ |
| 17 Symbol cap | 31st symbol rejected | Config validator + WS manager | ⬜ |
18 SignalR ReceiveTradeUpdate |
Hub mock receives on fill | Optional Flutter listener | ⬜ |
| 19 Metrics | Log ingest lag, rule fires, order latency | Structured logging | ⬜ |
Test pyramid (target)
┌─────────────────┐
│ Gate B (manual) │ 1 scenario / release
├─────────────────┤
│ Gate A (worker) │ 1 scripted integration
├─────────────────┤
│ HTTP handlers │ ~8 tests
├─────────────────┤
│ DB integration │ ~12 tests
├─────────────────┤
│ Unit (engine, │ ~40 tests
│ clients, config)│
└─────────────────┘
Suggested schedule (first 2 weeks)
| Day | Steps | Deliverable |
|---|---|---|
| 1 | 0, 1.1–1.2 | Test harness + migration + snapshot DB |
| 2 | 1.3–1.4, 2 | Config merge + env |
| 3 | 3–4 | Alpaca MD client + ingest |
| 4 | 5–6 | Rule engine + guardrails |
| 5 | 7–8 | Questions from rules + answer branch |
| 6 | 9 | Trade actuator |
| 7 | 10 | Worker wire-up → Gate A |
| 8 | 11 | Gate B with paper account |
CI configuration
# Suggested CI jobs
- dart test # unit + DB integration (no Alpaca)
- dart test --tags=alpaca # optional; secrets required; nightly/manual
CI env:
TRADING_ENABLED=true
QUESTION_PIPELINE_TEST_MODE=true
DATABASE_URL=postgres://.../cyberhybridhub_test
# No ALPACA_* keys in default CI job
Safety tests (must pass before live)
These should fail the build if removed:
AlpacaEnvrefuses live URL whenALPACA_ALLOW_LIVE=false- Server-side max notional ceiling enforced regardless of config
- Every order has non-null
question_idwhenrequire_question_before_order=true client_order_idUNIQUE prevents duplicate Alpaca POST
Mapping to existing code
| New piece | Extends |
|---|---|
TradingPipeline.evaluate |
QuestionPipeline._canEnqueue queue limits |
onAnswerSubmitted trading branch |
geography/weather switch in question_pipeline.dart |
BranchDecision.yesNo |
+10/-10 swipe (already used for weather) |
ExternalDataFetcher pattern |
Alpaca clients with injectable http.Client |
QUESTION_PIPELINE_TEST_MODE |
Skip Alpaca; fixture snapshots |
Existing touchpoints:
| Component | Path |
|---|---|
| Interval worker | server/lib/workers/question_background_worker.dart |
| Question pipeline | server/lib/pipeline/question_pipeline.dart |
| Branch decisions | server/lib/pipeline/branch_decision.dart |
| External fetcher | server/lib/pipeline/external_data_fetcher.dart |
| Env | server/lib/env.dart |
| Flutter hub | lib/services/questions_hub_service.dart |
| Flutter swipe UI | lib/widgets/swipe_question_tile.dart |
MVP definition of done
dart testinserver/green (~40+ tests, no network in default job)- Gate A passes with test mode
- Gate B passes on Alpaca paper
- TRADING_DEVELOPMENT_PLAN §17 scenario reproducible from seed SQL
TRADING_ENABLED=falsedefault; no secrets in Flutter- Every order traceable via
trade_orders.question_id+rule_id
References
Document version: 1.0 — TDD progress tracker for Alpaca trading MVP.