cyberhybridhub/TRADING_TDD_PLAN.md

454 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Trading TDD Plan — Progress Tracker
Companion to [TRADING_DEVELOPMENT_PLAN.md](./TRADING_DEVELOPMENT_PLAN.md). Each step follows **Red → Green → Confirm** before moving on.
**How agents should use this file:**
1. Pick the first unchecked step in order.
2. Write the failing test (Red), implement minimal code (Green), run the Confirm gate.
3. Check off boxes and add a one-line note under **Progress log** with date and result.
4. 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 111 — Phase 1 MVP | 🔄 In progress | Day 7: Steps 910 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 1215 — Phase 2 | ⬜ Not started | |
| Steps 1619 — Phase 3 | ⬜ Deferred | |
Legend: ⬜ Not started · 🔄 In progress · ✅ Done · ⏸ Blocked
---
## Progress log
<!-- Agents: append newest entries at the top -->
| 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 | 78 | `dart test` green (47 tests); TradingPipeline evaluate/handleAnswer + QuestionPipeline branch |
| 2026-05-25 | 56 | `dart test` green (39 tests); rule engine + guardrails (pure logic) |
| 2026-05-23 | 34 | `dart test` green (21 tests); Alpaca MD client, ingest, poll interval; live Alpaca OK |
| 2026-05-23 | 1.31.4, 2 | `dart test` green (17 tests); config merge, trade orders, Alpaca env/models |
| 2026-05-23 | 0, 1.11.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
- [x] Add `dev_dependencies: test: ^1.25.0` to `server/pubspec.yaml`
- [x] Create `server/test/` smoke test
- [x] Create `server/test/helpers/fixture_loader.dart`
- [x] Create `server/test/helpers/mock_http_client.dart`
- [x] Create `server/test/helpers/test_env.dart`
- [x] Create fixture files:
- [x] `server/test/fixtures/alpaca_latest_trade.json`
- [x] `server/test/fixtures/alpaca_daily_bars.json`
- [x] `server/test/fixtures/trading_config_default.json`
- [x] `server/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
- [x] Test helper applies migrations 001004 on `cyberhybridhub_test`
- [x] `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
- [x] **Red:** Integration test — INSERT into `market_data_snapshots`, `user_trading_config`, `trade_orders`; FK to `users` enforced
- [x] **Green:** `server/migrations/004_trading.sql`
- [x] **Green:** Seed `trading_config_templates` row `default_paper_watchlist`
- [x] **Confirm:** `\d market_data_snapshots` in psql; seed template queryable
#### 1.2 MarketDataDb
- [x] **Red:** `insertSnapshot()` then `latestForSymbol(symbol, metric)` returns newest row by `as_of`
- [x] **Green:** `server/lib/trading/market_data_db.dart`
- [x] **Confirm:** Two SPY/`last_trade` inserts → latest returns newer price
#### 1.3 TradingConfigDb
- [x] **Red:** `resolveEffectiveConfig(uid)` merges template + user JSONB; user override wins
- [x] **Green:** `server/lib/trading/trading_config_db.dart`
- [x] **Green:** `server/lib/trading/trading_config.dart` parser
- [x] **Confirm:** Partial user override yields merged `enabled`, `data_inputs`, `rules`
#### 1.4 TradeOrdersDb
- [x] **Red:** Duplicate `client_order_id` raises unique violation; `findByClientOrderId` returns existing row
- [x] **Green:** `server/lib/trading/trade_orders_db.dart`
- [x] **Confirm:** Idempotency check prevents double-insert
---
### Step 2 — Alpaca env & models
**Refs:** §3 · §11 · `alpaca_env.dart` · `alpaca_models.dart`
- [x] **Red:** `AlpacaEnv.fromMap()` reads keys; `assertPaperOnly()` throws when live URL + `ALPACA_ALLOW_LIVE=false`
- [x] **Green:** `server/lib/alpaca/alpaca_env.dart`
- [x] **Green:** `server/lib/alpaca/alpaca_models.dart` (Trade, Bar, OrderRequest)
- [x] **Green:** Extend `server/lib/env.dart` with trading flags (after env tests pass)
- [x] **Confirm:** Unit tests — paper default, live blocked, missing keys
---
### Step 3 — Alpaca Market Data client (REST)
**Refs:** §9.1 · `alpaca_market_data_client.dart`
- [x] **Red:** Mock HTTP — `getLatestTrade('SPY')` parses fixture; sends `APCA-API-KEY-ID` headers
- [x] **Green:** `server/lib/alpaca/alpaca_market_data_client.dart` with injectable `http.Client`
- [x] **Confirm:** Optional tagged test `@Tags(['alpaca'])` — one real call (skipped in CI)
---
### Step 4 — Snapshot normalization & ingest
**Refs:** §9.3 · `market_data_ingest.dart`
- [x] **Red:** Config `data_inputs` with `[last_trade, daily_bar, prev_close]` → 3 snapshot rows with correct `metric`, `price`, `as_of`
- [x] **Green:** `server/lib/trading/market_data_ingest.dart``runIfDue()` writes via `MarketDataDb`
- [x] **Confirm:** Integration — ingest for seeded user; `SELECT * FROM market_data_snapshots WHERE symbol='SPY'`
#### 4.1 Poll interval
- [x] **Red:** Second call within `poll_interval_seconds` does not fetch (uses `user_trading_state.context`)
- [x] **Green:** Update `user_trading_state` on ingest
- [x] **Confirm:** Two rapid `runIfDue` calls → mock HTTP call count = 1
---
### Step 5 — Rule engine (pure logic)
**Refs:** §4.2 · §8 · `rule_engine.dart`
- [x] **Red:** `price_below_pct_of_ref` — SPY `last_trade=492`, `prev_close=500`, threshold `-1.5` → fires with `pct ≈ -1.6`
- [x] **Green:** `server/lib/trading/rule_engine.dart``RuleEngine.evaluate(rule, snapshots) → RuleEvaluation?`
- [x] **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
- [x] **Red:** `Guardrails.check()` rejects when `max_orders_per_day` exceeded
- [x] **Red:** Rejects blocklisted symbol
- [x] **Red:** Rejects when `require_question_before_order` violated
- [x] **Green:** `server/lib/trading/guardrails.dart`
- [x] **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`
- [x] **Red:** Rule fires + guardrails pass + queue room → `createAndDeliverQuestion` with `pipeline_key=trading`, `pipeline_step=dip_confirm:await_confirm`, `correct_answer=10`, substituted text
- [x] **Green:** `server/lib/trading/trading_pipeline.dart``evaluate()` with mocked deps
- [x] **Confirm:** Integration — seeded snapshots + config → row in `questions` with correct tags
- [x] **Green:** `user_trading_state.context` records pending rule id and phase
---
### Step 8 — Answer handling → order proposal
**Refs:** §8.2 · extend `QuestionPipeline.onAnswerSubmitted`
- [x] **Red:** `pipeline_key=trading`, user `+10`, `BranchDecision.yesNo` → match → stages order (`submit_order`), no Alpaca yet
- [x] **Red:** User `-10` → skip logged, no order
- [x] **Green:** `PipelineKeys.trading` in `question_pipeline.dart`
- [x] **Green:** `_handleTradingAnswer` in `question_pipeline.dart` (delegates to `TradingPipeline.handleAnswer`)
- [x] **Confirm:** Integration test exercises the `QuestionPipeline.onAnswerSubmitted` switch for `pipeline_key=trading`
---
### Step 9 — Trade actuator
**Refs:** §10 · `alpaca_trading_client.dart` · `TradeActuator`
- [x] **Red:** Mock HTTP — `submitOrder(notional: 10, side: buy)` POSTs paper URL with `client_order_id`; `trade_orders` row `status=accepted`
- [x] **Red:** Duplicate `client_order_id` → 422 → resolved via `getOrderByClientOrderId`
- [x] **Green:** `server/lib/alpaca/alpaca_trading_client.dart`
- [x] **Green:** `server/lib/trading/trade_actuator.dart` (test mode short-circuit + Alpaca path)
- [x] **Confirm:** Tagged integration test (`server/test/alpaca/alpaca_trading_live_test.dart`) — paper roundtrip, skipped without credentials
- [x] **Confirm:** `client_order_id` format `{firebase_uid}-{rule_id}-{question_id}` unique (set by `TradingPipeline.handleAnswer`, enforced by `trade_orders.client_order_id UNIQUE`)
---
### Step 10 — Worker integration
**Refs:** §7 · `question_background_worker.dart` · `trading_orchestrator.dart`
- [x] **Red:** `TRADING_ENABLED=true`, test mode (no Alpaca client) — one orchestrator tick runs evaluate + actuate against fixture snapshots
- [x] **Green:** `server/lib/trading/trading_orchestrator.dart` (ingest → evaluate → actuate, per-stage failure isolation)
- [x] **Green:** `QuestionBackgroundWorker` accepts optional `TradingOrchestrator`, runs it after `QuestionPipeline.runMaintenanceCycle`
- [x] **Green:** `bin/server.dart` builds the trading stack only when `TRADING_ENABLED=true`; uses real Alpaca clients only when `QUESTION_PIPELINE_TEST_MODE=false` and credentials present
- [x] **Confirm:** **Gate A** below — `test/integration/trading_orchestrator_gate_a_test.dart` green
---
### 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 010 complete.
Automated as `server/test/integration/trading_orchestrator_gate_a_test.dart`.
- [x] Seed user with `user_trading_config.enabled=true` and dip rule
- [x] Insert fixture snapshots (or test-mode ingest)
- [x] Run one worker tick → question queued
- [x] Submit answer +10 via API
- [x] Run next tick → `trade_orders` row (fake Alpaca id in test mode)
- [x] Third tick → rule does not re-fire (cooldown)
- [x] Set `TRADING_ENABLED=true` in dev `.env` only 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_id` and `rule_id` populated
---
## 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`, invalid `mode`
- [ ] **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_count` increments; 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.status` and `filled_at` updated
- [ ] **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)
```text
┌─────────────────┐
│ 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.11.2 | Test harness + migration + snapshot DB |
| 2 | 1.31.4, 2 | Config merge + env |
| 3 | 34 | Alpaca MD client + ingest |
| 4 | 56 | Rule engine + guardrails |
| 5 | 78 | 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
```yaml
# Suggested CI jobs
- dart test # unit + DB integration (no Alpaca)
- dart test --tags=alpaca # optional; secrets required; nightly/manual
```
**CI env:**
```bash
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:
- [ ] `AlpacaEnv` refuses live URL when `ALPACA_ALLOW_LIVE=false`
- [ ] Server-side max notional ceiling enforced regardless of config
- [ ] Every order has non-null `question_id` when `require_question_before_order=true`
- [ ] `client_order_id` UNIQUE 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 test` in `server/` 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=false` default; no secrets in Flutter
- [ ] Every order traceable via `trade_orders.question_id` + `rule_id`
---
## References
- [TRADING_DEVELOPMENT_PLAN.md](./TRADING_DEVELOPMENT_PLAN.md)
- [server/README.md](./server/README.md)
- [Alpaca Market Data API](https://docs.alpaca.markets/docs/market-data-api)
- [Alpaca Trading API](https://docs.alpaca.markets/docs/trading-api)
---
*Document version: 1.0 — TDD progress tracker for Alpaca trading MVP.*