# ml-alpha Live Integration Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Wire the Plan-2-validated alpha model into the existing `trading_agent_service` as `AlphaPpoStrategy`, audit the IBKR adapter, prove the integration with 5 trading days of paper trading against live MBP-10, then deploy with `$1k` and `max_live=1` for a 5-day live window. Stop after the live window unless realized Sharpe > 0 with zero IBKR rejections — ramping is operator-controlled per the spec.

**Architecture:** Strategy plugin in `services/trading_agent_service/`. Owns an `AlphaModel` (from `crates/ml-alpha`) on GPU; hot path is `on_snapshot` (every snapshot, captured Graph A) and `on_fill` (every broker fill, captured ingest_fill). Reuses the existing IBKR adapter at `crates/trading_engine/src/brokers/interactive_brokers.rs`. Cold-start follows the 4-state bootstrap (init→warmup→baseline→steady) using `phase_b_baseline.json` from Plan 2.

**Tech Stack:** Plan 1+2 deliverables + `services/trading_agent_service` (existing) + `services/data_acquisition_service` (Databento streaming, existing) + `services/trading_service::execution_engine` + `crates/trading_engine` IBKR adapter + Prometheus + QuestDB.

**Source spec:** `docs/superpowers/specs/2026-05-16-ml-alpha-cfc-ppo-design.md` Sections 1 (component boundaries), 3.5 (IBKR quirks + capital math), 5 (live runtime + persistence), 5.5 (CUDA Graphs), 5.6 (disconnect handling), 6 cold-start bootstrap.

**Prerequisite:** Plan 2 has merged with `walk_forward.json` showing `mean_sharpe > 0 and std < mean` across folds 1-4, and a `phase_b_baseline.json` artifact pinned to the released weights.

**Capital + risk:**
- Initial capital reserved for live: **$1,000** (NOT the full $35k yet — first-window test only).
- `max_live = 1` contract (no ramp until the success criteria below clear).
- Daily loss limit: 5% of equity (~$50) — when hit, flat + halt + alert.
- Manual kill-switch armed via `alpha-control halt` at all times during the live window.

---

## File structure

| File | Status | Responsibility |
|---|---|---|
| `services/trading_agent_service/src/strategies/mod.rs` | Refactor | Split out existing `strategies.rs` into a module; keep its public surface unchanged for compat. |
| `services/trading_agent_service/src/strategies/alpha_ppo.rs` | Create | `AlphaPpoStrategy` — hot-path snapshot + fill handlers; owns `AlphaModel`. |
| `services/trading_agent_service/src/strategies/cold_start.rs` | Create | `ColdStartFsm` — 4-state bootstrap (init/warmup/baseline/steady) per spec Section 6. |
| `services/trading_agent_service/src/strategies/disconnect.rs` | Create | Databento + IBKR disconnect detection + recovery per spec Section 5.6. |
| `services/trading_agent_service/src/strategies/observability.rs` | Create | Prometheus histograms + QuestDB row emission per spec Section 5 observability. |
| `services/trading_agent_service/src/control/mod.rs` | Create | `alpha-control` IPC server (Unix socket). |
| `services/trading_agent_service/src/control/commands.rs` | Create | Control commands: halt, restart, rollback, status, set-config. |
| `services/trading_agent_service/Cargo.toml` | Modify | Add `ml-alpha` dep. |
| `bin/fxt/src/cmd/alpha_control.rs` | Create | CLI client to the control socket. |
| `bin/fxt/src/main.rs` | Modify | Wire `alpha-control` subcommand. |
| `services/trading_agent_service/tests/alpha_ppo_lifecycle.rs` | Create | Integration test: synthetic snapshot stream + synthetic fills, assert correct on_snapshot/on_fill ordering and state updates. |
| `services/trading_agent_service/tests/cold_start_fsm.rs` | Create | All 4 states + state transitions on synthetic decision-count. |
| `services/trading_agent_service/tests/disconnect_recovery.rs` | Create | Force Databento gap > max_snapshot_gap_ms; assert target → 0 + reconnect path. |
| `services/trading_agent_service/tests/control_socket.rs` | Create | Halt, status, rollback round-trips. |
| `docs/audits/ibkr-adapter-audit-2026-05-16.md` | Create | Audit checklist + findings (Section 3.5 pre-deployment tasks). |
| `infra/k8s/argo/alpha-paper-trading-template.yaml` | Create | 5-day paper-trading workflow against live Databento + sim execution. |
| `infra/k8s/manifests/alpha-live-deployment.yaml` | Create | Live deployment manifest pinning $1k capital limit + max_live=1. |
| `scripts/alpha-paper-trading.sh` | Create | Submit paper-trading workflow. |
| `scripts/alpha-live-deploy.sh` | Create | Apply live deployment manifest after manual ack gate. |

---

## Task 1: IBKR adapter audit

**Files:**
- Create: `docs/audits/ibkr-adapter-audit-2026-05-16.md`
- Read-only: `crates/trading_engine/src/brokers/interactive_brokers.rs`

The 1177-LOC adapter exists; Section 3.5 of the spec lists 6 audit items. This task produces a checklist with a status per item and concrete findings.

- [ ] **Step 1: Open the audit document with the spec's audit table**

Create `docs/audits/ibkr-adapter-audit-2026-05-16.md` with this skeleton (the rows match Section 3.5 pre-deployment tasks):

```markdown
# IBKR Adapter Audit — 2026-05-16

Audited file: `crates/trading_engine/src/brokers/interactive_brokers.rs` (~1177 LOC, `ibapi` v10.19+).

| # | Audit item | Status | Location | Notes |
|---|---|---|---|---|
| 1 | Handles ES futures contract spec (root=`ES`, exchange=`GLOBEX`, multiplier=`50`) | ⬜ | TBD | TBD |
| 2 | `whatif` pre-trade margin check is exposed | ⬜ | TBD | TBD |
| 3 | Rollover detection (quarterly expiry: Mar/Jun/Sep/Dec) + symbol-update logic | ⬜ | TBD | TBD |
| 4 | Halt-status subscription (LULDs, circuit breakers, trading halts) | ⬜ | TBD | TBD |
| 5 | Rejection-code catalog coverage (margin, contract spec, throttle) | ⬜ | TBD | TBD |
| 6 | PDT counter integration with `services/trading_service::risk_manager` | ⬜ | TBD | TBD |

## Findings

(populated as the audit progresses)

## Pre-deployment blockers

(populated; each is a Task in this plan if found)
```

- [ ] **Step 2: Audit items 1+2 — ES contract spec + whatif margin check**

Read the adapter end-to-end, then grep:

```bash
grep -nE "ES|GLOBEX|multiplier|whatif|wif" crates/trading_engine/src/brokers/interactive_brokers.rs
```

Populate rows 1 and 2 with `✅` (file + line refs) or `❌` (gap → block + Task to add).

- [ ] **Step 3: Audit items 3+4 — rollover + halt subscription**

```bash
grep -nE "rollover|expiry|front_month|HaltStatus|LULD|trading_halt|symbol_update" crates/trading_engine/src/brokers/interactive_brokers.rs
```

- [ ] **Step 4: Audit items 5+6 — rejection coverage + PDT counter**

```bash
grep -nE "OrderReject|Reject|reject_code|PDT|pattern_day_trade|day_trade_count" crates/trading_engine/src/brokers/interactive_brokers.rs services/trading_service/src/core/risk_manager.rs
```

- [ ] **Step 5: Commit the audit + open issues for any ❌ rows**

```bash
git add docs/audits/ibkr-adapter-audit-2026-05-16.md
git commit -m "audit(ibkr): IBKR adapter pre-deployment audit checklist + findings

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
```

If any row is ❌, **stop here** and add tasks to this plan addressing each gap before continuing. Live capital cannot deploy through an incomplete adapter.

---

## Task 2: `AlphaPpoStrategy` scaffold

**Files:**
- Modify: `services/trading_agent_service/src/strategies.rs` → refactor into `services/trading_agent_service/src/strategies/mod.rs` (preserve `pub use` surface)
- Create: `services/trading_agent_service/src/strategies/alpha_ppo.rs`
- Modify: `services/trading_agent_service/Cargo.toml` (add `ml-alpha = { workspace = true }`)
- Create: `services/trading_agent_service/tests/alpha_ppo_lifecycle.rs`

- [ ] **Step 1: Failing integration test**

```rust
// services/trading_agent_service/tests/alpha_ppo_lifecycle.rs
use trading_agent_service::strategies::alpha_ppo::{AlphaPpoStrategy, AlphaConfig};

#[test]
fn on_snapshot_emits_signal_only_when_target_changes() {
    let cfg = AlphaConfig {
        weights_path: "tests/fixtures/alpha-weights.bin".into(),
        ewc_anchor_path: "tests/fixtures/alpha-anchor.bin".into(),
        phase_b_baseline_path: "tests/fixtures/phase_b_baseline.json".into(),
        decision_stride: 50,
        max_live: 1,
        ..AlphaConfig::test_defaults()
    };
    let mut strat = AlphaPpoStrategy::new_for_test(cfg).expect("init");
    let snaps = synthetic_snaps(/*n=*/200);
    let mut signals_emitted = 0usize;
    for s in &snaps {
        if strat.on_snapshot(s).is_some() { signals_emitted += 1; }
    }
    // With decision_stride=50, 200 snapshots = 4 decisions; signals emitted only on target change.
    assert!(signals_emitted <= 4, "more signals than decisions: {signals_emitted}");
}
```

- [ ] **Step 2: Run — fails**

```bash
SQLX_OFFLINE=true cargo test -p trading_agent_service --test alpha_ppo_lifecycle 2>&1 | tail -10
```

- [ ] **Step 3: Implement `AlphaPpoStrategy`**

Per spec Section 5:

```rust
// services/trading_agent_service/src/strategies/alpha_ppo.rs
use anyhow::Result;
use ml_alpha::{AlphaModel, AlphaConfig as ModelConfig, Action, UpdateStatus};
use std::path::PathBuf;

pub struct AlphaConfig {
    pub weights_path: PathBuf,
    pub ewc_anchor_path: PathBuf,
    pub phase_b_baseline_path: PathBuf,
    pub decision_stride: usize,
    pub max_live: i8,
    pub online_updates_enabled: bool,
    pub update_interval_secs: u64,
    pub update_interval_transitions: usize,
    pub kill_switch_enabled: bool,
}

pub struct AlphaPpoStrategy {
    model: AlphaModel,
    decision_counter: usize,
    last_target: i8,
    config: AlphaConfig,
    update_clock: UpdateClock,
    cold_start: crate::strategies::cold_start::ColdStartFsm,
    metrics: AlphaMetrics,
}

impl AlphaPpoStrategy {
    pub fn new(cfg: AlphaConfig) -> Result<Self> {
        let model_cfg = ModelConfig {
            weights: cfg.weights_path.clone(),
            ewc_anchor: cfg.ewc_anchor_path.clone(),
            phase_b_baseline: cfg.phase_b_baseline_path.clone(),
            max_live: cfg.max_live,
        };
        let model = AlphaModel::load(&model_cfg)?;
        Ok(Self {
            model,
            decision_counter: 0,
            last_target: 0,
            cold_start: crate::strategies::cold_start::ColdStartFsm::new(&cfg.phase_b_baseline_path)?,
            update_clock: UpdateClock::new(cfg.update_interval_secs, cfg.update_interval_transitions),
            metrics: AlphaMetrics::default(),
            config: cfg,
        })
    }
}

impl trading_agent_service::strategies::Strategy for AlphaPpoStrategy {
    fn on_snapshot(&mut self, snap: &Mbp10Snapshot) -> Option<Signal> {
        self.model.snap_pinned_slot_mut().write_volatile(snap.into());
        self.model.perception_forward(); // Graph A

        self.decision_counter += 1;
        if self.decision_counter % self.config.decision_stride != 0 { return None; }

        let action: Action = self.model.policy_action(); // Graph B
        let target = action.clamp_to(self.config.max_live);

        let emit = if target != self.last_target {
            self.last_target = target;
            Some(Signal::SetTargetPosition { contracts: target })
        } else { None };

        // Online update — only after cold-start clears Bootstrap-init
        if self.cold_start.allows_online_updates() && self.update_clock.tick() {
            let mix = self.cold_start.replay_offline_fraction();
            let batch = self.model.replay_buffer().sample_mixed(8192, mix);
            match self.model.update(&batch) {
                UpdateStatus::Applied { metrics } => {
                    self.cold_start.record_update(&metrics);
                    self.metrics.applied(metrics);
                }
                UpdateStatus::RolledBack { reason, deviating_metric, sigma } => {
                    self.config.online_updates_enabled = false;
                    self.metrics.rolled_back(&reason, &deviating_metric, sigma);
                    tracing::error!(reason, deviating_metric, sigma, "online update rolled back");
                }
            }
        }
        emit
    }

    fn on_fill(&mut self, fill: &Fill) {
        self.model.fill_pinned_slot_mut().write_volatile(fill.into());
        self.model.ingest_fill();
    }
}
```

- [ ] **Step 4: Run — passes**

```bash
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p trading_agent_service --test alpha_ppo_lifecycle 2>&1 | tail -10
```

- [ ] **Step 5: Commit**

```bash
git add services/trading_agent_service/Cargo.toml \
        services/trading_agent_service/src/strategies/ \
        services/trading_agent_service/tests/alpha_ppo_lifecycle.rs
git commit -m "feat(trading-agent): AlphaPpoStrategy plugin

GPU-resident hot path: on_snapshot triggers captured Graph A, decision
ticks replay Graph B; on_fill writes mapped-pinned fill -> ingest_fill
GPU kernel. No CPU compute on model tensors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
```

---

## Task 3: Cold-start FSM (4-state bootstrap)

**Files:**
- Create: `services/trading_agent_service/src/strategies/cold_start.rs`
- Create: `services/trading_agent_service/tests/cold_start_fsm.rs`

States per spec Section 6 cold-start:
- `BootstrapInit`: live_decisions < 1000 → replay = 100% offline, no updates
- `BootstrapWarmup`: 1000 ≤ live_decisions < 10_000 → replay ramps 100% → 80/20 linearly; updates allowed but kill-switch uses Phase B baseline as 2σ reference
- `BootstrapBaseline`: 20+ successful updates landed → switch to rolling kill-switch
- `SteadyState`: thereafter

- [ ] **Step 1: Failing FSM test**

```rust
// services/trading_agent_service/tests/cold_start_fsm.rs
use trading_agent_service::strategies::cold_start::ColdStartFsm;

#[test]
fn fsm_transitions_through_all_states() {
    let mut f = ColdStartFsm::new_for_test();
    assert!(matches!(f.state(), ColdStartState::BootstrapInit));
    for _ in 0..999 { f.record_live_decision(); }
    assert!(matches!(f.state(), ColdStartState::BootstrapInit));
    f.record_live_decision();
    assert!(matches!(f.state(), ColdStartState::BootstrapWarmup));
    for _ in 0..9000 { f.record_live_decision(); }
    assert!(matches!(f.state(), ColdStartState::BootstrapWarmup));
    f.record_live_decision();
    // 10001 decisions — eligible for Bootstrap-baseline once updates accrue
    for _ in 0..19 {
        f.record_update_success(synthetic_ok_metrics());
        assert!(matches!(f.state(), ColdStartState::BootstrapWarmup));
    }
    f.record_update_success(synthetic_ok_metrics());
    assert!(matches!(f.state(), ColdStartState::BootstrapBaseline));
    // BootstrapBaseline switches to rolling kill-switch reference immediately
    assert!(matches!(f.state(), ColdStartState::BootstrapBaseline | ColdStartState::SteadyState));
}

#[test]
fn replay_mix_ramp_linear_during_warmup() {
    let mut f = ColdStartFsm::new_for_test();
    for _ in 0..1000 { f.record_live_decision(); }
    // At 1000 (start of warmup): all offline
    assert!((f.replay_offline_fraction() - 1.0).abs() < 1e-6);
    for _ in 0..4500 { f.record_live_decision(); }
    // Mid-warmup: half-ramp
    let mix = f.replay_offline_fraction();
    assert!(mix > 0.86 && mix < 0.95, "mid-warmup ramp not linear; mix={mix}");
    for _ in 0..4500 { f.record_live_decision(); }
    // Past warmup (10K): 80/20 reached
    assert!((f.replay_offline_fraction() - 0.8).abs() < 1e-6);
}
```

- [ ] **Step 2-5: TDD cycle.**

---

## Task 4: Disconnect + failure handling

**Files:**
- Create: `services/trading_agent_service/src/strategies/disconnect.rs`
- Create: `services/trading_agent_service/tests/disconnect_recovery.rs`

Per spec Section 5.6:
- Databento stream-loss: gap > `max_snapshot_gap_ms` → mark `Degraded::DataStale`, issue `target=0`, freeze `h`, reconnect with exponential backoff (1,2,5,10,30s cap)
- IBKR rejection/throttle/disconnect: same flat+pause pattern
- Position drift > 60s reconcile interval: hard error, alert + halt; never silent re-sync
- After 300s gap: force full model reload

- [ ] **Step 1: Failing test** — synthetic gap > `max_snapshot_gap_ms` triggers `target=0` signal exactly once + transitions to `Degraded::DataStale`.
- [ ] **Step 2-5: TDD cycle.**

---

## Task 5: Observability — Prometheus histograms + QuestDB rows

**Files:**
- Create: `services/trading_agent_service/src/strategies/observability.rs`
- Modify: `services/trading_agent_service/src/lib.rs` (export metrics handle)

Per spec Section 5 observability:
- Histogram: `alpha_snapshot_latency_us` (buckets: 50, 100, 200, 500, 1000, 5000)
- Histogram: `alpha_decision_latency_us` (buckets: 100, 250, 500, 1000, 5000)
- Counter: `alpha_signals_emitted_total{change}` (label: 0 = no change, 1 = change)
- Counter: `alpha_kill_switch_fired_total{metric}`
- Gauge: `alpha_position_contracts`
- QuestDB row per decision: `(ts, alpha_p_h30, alpha_p_h100, alpha_p_h300, alpha_p_h1000, alpha_p_h6000, action, log_prob, value)`
- QuestDB row per fill: `(ts, side, qty, price, commission, realized_pnl_ticks, position_after)`
- QuestDB row per update: `(ts, loss_ppo, mean_reward, hit_rate, kl, entropy, kill_switch_margin, lambda_ewc_eff, entropy_coef_eff)`

- [ ] **Step 1: Failing test** — synthetic decision triggers exactly one of each metric emission.
- [ ] **Step 2-5: TDD cycle.**

---

## Task 6: `alpha-control` IPC server + `fxt alpha-control` CLI

**Files:**
- Create: `services/trading_agent_service/src/control/mod.rs`
- Create: `services/trading_agent_service/src/control/commands.rs`
- Create: `bin/fxt/src/cmd/alpha_control.rs`
- Modify: `bin/fxt/src/main.rs`
- Create: `services/trading_agent_service/tests/control_socket.rs`

Commands (over a Unix socket at `/run/fxt/alpha-control.sock`):

| Command | Effect |
|---|---|
| `status` | Returns JSON: state (steady/bootstrap-init/...), position, last_action, kill_switch_enabled, online_updates_enabled |
| `halt` | Issues `target=0`, sets `kill_switch_enabled=true` (blocks all signals), records ack required to resume |
| `resume` | Manual ack to restart signals after halt |
| `rollback --to <weights-snapshot-id>` | Atomic swap to a specified weights snapshot from `/data/ml-alpha/` |
| `set-config <key=value>` | Hot-set: max_live (≤max_train), online_updates_enabled, C_vol_floor (each persisted) |
| `disable-online-updates` | One-way until next operator restart |

- [ ] **Step 1: Failing test** — `status` round-trip, `halt` blocks emission, `rollback` swaps weights file.
- [ ] **Step 2-5: TDD cycle.**

---

## Task 7: Restart semantics + state persistence

**Files:**
- Modify: `services/trading_agent_service/src/strategies/alpha_ppo.rs` (add `Shutdown::clean()` + `restore_from_disk`)
- Create: `services/trading_agent_service/tests/restart_round_trip.rs`

Per spec Section 5.6 restart:
1. `alpha-control restart` issues target=0 + waits for fill ack
2. Persist `(weights, h, position, replay_ring, ewc_anchor, kill_switch_state, isv_state)` to `/data/ml-alpha/restart-<ts>/`
3. Acknowledge shutdown
4. Cold-start on next launch reloads all of the above

- [ ] **Step 1: Failing test** — round-trip: build a state with non-zero position/h/replay; `Shutdown::clean()`; `restore_from_disk` produces bit-equivalent state.
- [ ] **Step 2-5: TDD cycle.**

---

## Task 8: Paper-trading harness (5 trading days)

**Files:**
- Create: `infra/k8s/argo/alpha-paper-trading-template.yaml`
- Create: `scripts/alpha-paper-trading.sh`
- Create: `services/trading_agent_service/src/control/paper_mode.rs`

The harness:
- Subscribes to **live Databento MBP-10** (same code as live)
- Routes signals to a **simulated execution engine** (existing `services/trading_service::execution_engine` has a `Simulated` mode; if not, the spec requires it — Task 8.5 below covers any gap)
- Records every signal + simulated fill to QuestDB
- After 5 trading days, emits `paper_report.json` with realized Sharpe, hit rate, latency p50/p99, kill-switch fires, kernel timing histograms

Pass criteria per spec Section 7:
- Live P&L tracks backtest ±15%
- No kill-switch fires
- Latency p99 < 500μs

- [ ] **Step 1: Verify or add `Simulated` mode in `execution_engine`**

```bash
grep -n "Simulated\|simulated\|paper" services/trading_service/src/core/execution_engine.rs | head
```

If absent, the gap is itself a sub-task: add a `Simulated` mode that fills at the next snapshot's mid (no slippage by default; configurable spread cost). Test with a 100-tick synthetic stream.

- [ ] **Step 2: Workflow template** — single long-running pod (5 days), restart policy `Never`, persistent volume for QuestDB writes, prometheus sidecar.

- [ ] **Step 3: Submission**

```bash
git push -u origin ml-alpha-live
./scripts/alpha-paper-trading.sh
```

- [ ] **Step 4: After 5 days, fetch + judge**

```bash
mkdir -p artifacts/paper-trading/
kubectl -n foxhunt cp $(POD):/data/paper_report.json artifacts/paper-trading/
jq '.pass_criteria' artifacts/paper-trading/paper_report.json
```

Expected: all three pass criteria `true`.

- [ ] **Step 5: Commit + judgment**

```bash
git add infra/k8s/argo/alpha-paper-trading-template.yaml scripts/alpha-paper-trading.sh services/trading_agent_service/src/control/paper_mode.rs
git commit -m "feat(alpha): paper-trading harness against live Databento + simulated exec

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
```

If any pass criterion fails, **stop** and triage:
- P&L > 15% delta to backtest → execution simulation cost model wrong, or live-stream regime out-of-sample. Investigate snapshot sampling consistency.
- Kill-switch fired → controllers or Phase B baseline mis-calibrated. Examine the `kill_switch_event.json` payload, follow controller state to root cause.
- p99 > 500μs → CUDA Graph not actually capturing on warm path (host branch inside graph capture; see `pearl_no_host_branches_in_captured_graph.md`). Re-check Plan 1 Task 11 and Plan 2 Task 11.

---

## Task 9: Live deployment manifest + capital + risk envelope

**Files:**
- Create: `infra/k8s/manifests/alpha-live-deployment.yaml`
- Create: `scripts/alpha-live-deploy.sh`
- Modify: `services/trading_service/src/core/risk_manager.rs` (only if PDT counter or daily-loss-limit gaps found in Task 1)

Live deployment is **gated on a manual ack** — the script reads a file path on disk that must be touched after the operator reviews the paper-trading report:

```bash
# scripts/alpha-live-deploy.sh
#!/usr/bin/env bash
set -euo pipefail
ACK_PATH=/tmp/alpha-live-ack-$(date +%Y%m%d).sentinel
test -f "$ACK_PATH" || {
  echo "Live ack sentinel not found at $ACK_PATH"
  echo "Operator: review artifacts/paper-trading/paper_report.json, then run:"
  echo "  touch $ACK_PATH"
  exit 1
}
test -f artifacts/paper-trading/paper_report.json || { echo "No paper report; refusing"; exit 1; }
PASS=$(jq -r '.pass_criteria.all_passed' artifacts/paper-trading/paper_report.json)
test "$PASS" = "true" || { echo "Paper trading did not meet pass criteria; refusing"; exit 1; }

kubectl apply -f infra/k8s/manifests/alpha-live-deployment.yaml
```

The manifest itself pins:
- `MAX_LIVE_CONTRACTS=1`
- `STARTING_CAPITAL_USD=1000`
- `DAILY_LOSS_LIMIT_USD=50`
- `ONLINE_UPDATES_ENABLED=true`
- `KILL_SWITCH_ENABLED=true`
- env-var to the Phase B `weights.bin` + `phase_b_baseline.json` paths

- [ ] **Step 1**: Write the manifest + the deploy script.
- [ ] **Step 2**: Validate manifest with `kubectl apply --dry-run=client -f`.
- [ ] **Step 3**: Verify the gate fails without the ack sentinel.
- [ ] **Step 4**: Verify the gate fails when paper_report.json shows non-pass.
- [ ] **Step 5**: Commit.

---

## Task 10: Live $1k deploy + 5-day window

**Files:** No new files. This task is procedural.

- [ ] **Step 1**: Operator reviews `artifacts/paper-trading/paper_report.json`. If pass: touch the ack sentinel.
- [ ] **Step 2**: `./scripts/alpha-live-deploy.sh` (this is the live capital deployment).
- [ ] **Step 3**: Monitor for 5 trading days. Track every minute via:
  - `kubectl logs -n foxhunt deploy/alpha-live` (operator dashboard)
  - Prometheus alert wired on `alpha_kill_switch_fired_total > 0`, `alpha_daily_loss_usd > 40`, `alpha_ibkr_rejection_total > 0`
  - Manual halt available via `fxt alpha-control halt`
- [ ] **Step 4**: After 5 days, halt + emit live_report.json:

```bash
fxt alpha-control halt
fxt alpha-control status > artifacts/live-window/live_report.json
```

- [ ] **Step 5**: Pass criteria per spec Section 7:
  - Realized Sharpe > 0
  - Zero IBKR rejections
  - Rollover clean (if encountered)

  Decision tree:
  - Pass → operator decides whether to extend live window or scale to `max_live=2` per the spec Section 3.5 ramp table (manual config bump, not automatic).
  - Fail on Sharpe → review whether 5 days is too small a sample for the alpha hypothesis (per `pearl_single_window_oos_is_not_oos.md` — single-window OOS is not OOS); operator decides whether to extend or revisit.
  - Fail on IBKR rejection → adapter gap; back to Task 1's audit.
  - Fail on rollover → existing rollover logic in `crates/trading_engine` had a bug exposed by live; back to Task 1's audit.

---

## Gate-pass exit criteria

Plan 3 is "complete" when **all of**:
- [ ] IBKR adapter audit: all 6 items ✅
- [ ] All AlphaPpoStrategy / cold-start / disconnect / observability / control unit + integration tests pass
- [ ] Paper trading: 5-day report shows all 3 pass criteria met
- [ ] Live $1k deployment runs for 5 trading days without manual halt
- [ ] Live realized Sharpe > 0, zero IBKR rejections, rollover (if any) clean
- [ ] `alpha-control rollback` and `alpha-control halt` proven to work in production at least once each (drill-test before scaling)

**On all-pass:** the operator decides whether to ramp `max_live` per the Section 3.5 schedule. Ramp is **explicitly manual** — no autonomous scaling. After 100+ trades with positive realized Sharpe, `max_live=2`; after 500+ trades with ≥3-month positive track record, `max_live=3`.

**On any-fail:** halt, write a failure post-mortem to `docs/incidents/`, update the spec with the failure mode, and re-plan whichever sub-system failed (Plan 1 if perception drift, Plan 2 if policy regression, this Plan if integration/operations).
