Webull F4 Live-Flip CeremonyPHASE 4 · OPERATOR RUNBOOK · REAL MONEY

Webull F4 (live) · Phase 4 · 2026-06-18 · ~9 min read · Real capital on a real exchange. Operator-signed, per-strategy, $100/ticket. UAT first.


What this is. A step-by-step runbook for the operator to authorize a single webull_strategy_id for live execution, watch its first trades, measure whether live results converge on the paper-derived edge, and kill it from anywhere. This is a lab experiment — AI as lab rat, real money as the labeled training data — not a deployment. The safety binding is not an env flag: it is a per-strategy authorization row in webull_live_authorizations.db. A strategy with no unrevoked row falls back to the paper bridge (Phase 3.1). Hard limits are code-constants in webull_live_adapter.py: MAX_OPEN_LIVE_POSITIONS=3, MAX_DOLLARS_PER_TRADE=100, MAX_DAILY_REALIZED_LOSS=250, MAX_CONSECUTIVE_LIVE_LOSSES=5. UAT (us-openapi-alb.uat.webullbroker.com) is validated before PROD.
This ceremony is downstream of, not a substitute for, the science. The F7 Verdict Readout Protocol produces the paper verdict; this runbook authorizes a separate live dataset whose only scientific purpose is to test convergence with paper (§4). The Phase-4 pre-flip thresholds below (n≥30, p_lower>0.30, b>0.5) are the operator's empirical lab-entry bar — deliberately lower than the F7 EDGE_CONFIRMED gate (n≥100, p_lower>break_even). Clearing them authorizes a small-stakes live experiment; it does not assert an edge. Edge claims require paper and live to agree.
Contents
  1. §1 Pre-flip checklist
  2. §2 Authorization command & expected output
  3. §3 First-3-trades monitoring protocol
  4. §4 Convergence check (every 10 live closes)
  5. §5 Kill-switch operations
  6. §6 Rollback / loss-floor condition

§1 · Pre-flip checklist

Every row must PASS before running the §2 authorize script. These are the operator's empirical lab-entry thresholds, not the F7 EDGE_CONFIRMED gate.

CheckSourceThreshold (PASS if)Rationale
Paper n_closed/api/webull/f7/gate?mode=paper≥ 30Enough closes to compute a stable Wilson lower bound.
Paper p_lowersame> 0.30Lower bound exceeds 30% — a meaningful prior, not noise.
Paper b (R-multiple)same> 0.5Asymmetric payoff favors winners enough to matter.
R-distribution sanitySQL on webull_paper_tradesno |R| > 5 in any binNo infinity-tail blow-ups distorting the average.
5-arm provenanceSQL by webull_lab≥ 2 labs contributed closesSingle-arm could be a lab-specific artifact.
Service healthsystemctl --user is-active stockpickeraiactiveNo restart pending mid-flip.
Reconciliation croncrontab -linstalledThe §1.4 reconcile cron must be running before any live order exists.

Recipes & PASS/FAIL assertions

Gate metrics (rows 1–3):

curl -s 'http://127.0.0.1:8030/api/webull/f7/gate?mode=paper' \
  | python3 -c "import sys,json; d=json.load(sys.stdin); \
      b=[x for x in d['buckets'] if x['strategy_id']=='<strategy>'][0]; \
      assert b['n_closed'] >= 30,  f'FAIL n_closed={b[\"n_closed\"]}'; \
      assert b['p_lower']  > 0.30, f'FAIL p_lower={b[\"p_lower\"]}'; \
      assert b['b']        > 0.5,  f'FAIL b={b[\"b\"]}'; \
      print('PASS', b['strategy_id'], b['n_closed'], b['p_lower'], b['b'])"

R-distribution sanity (row 4) — fires FAIL if any close has |R| > 5:

SELECT COUNT(*) AS extreme_r
FROM webull_paper_trades
WHERE webull_strategy_id='<strategy>' AND is_live=0 AND status='closed'
  AND ABS(realized_r) > 5;
-- PASS iff extreme_r = 0

5-arm provenance (row 5) — fires FAIL if fewer than 2 labs contributed:

SELECT COUNT(DISTINCT webull_lab) AS arms
FROM webull_paper_trades
WHERE webull_strategy_id='<strategy>' AND is_live=0 AND status='closed';
-- PASS iff arms >= 2

Service health (row 6) and cron installed (row 7):

systemctl --user is-active stockpickerai          # PASS iff prints: active
crontab -l | grep -q phase4_reconcile.py && echo "PASS: reconcile cron installed" \
                                          || echo "FAIL: install §1.4 cron first"
If any row FAILs, do not authorize. A failed provenance or sanity check is the system telling you the paper baseline is not ready to be tested against real money — fix the upstream condition (accumulate more closes, install the cron) and re-run the checklist.

§2 · Authorization command & expected output

Authorization is a deliberate, signed operator action — it writes one row to webull_live_authorizations.db. Run it interactively:

python3 scripts/phase4_authorize_strategy.py \
    --strategy long_call \
    --by "operator-2026-06-18" \
    --max-usd 50 \
    --notes "Phase 4 first authorization: lab experiment 4a"

Expected JSON output (the script snapshots paper metrics at the moment of auth as a sanity record):

{
  "status": "authorized",
  "strategy_id": "long_call",
  "authorized_at": 1781747820,
  "paper_snapshot": {"n_closed": 32, "p_lower": 0.412, "b": 0.83},
  "max_position_usd": 50
}

SQL invariant immediately after auth — exactly one active authorization for the strategy:

SELECT COUNT(*) FROM webull_live_authorizations
WHERE strategy_id='long_call' AND revoked_at IS NULL;
-- expected: 1

From this moment, the next open_trade() for long_call with live_mode_request=True routes through webull_live_adapter.place_option_order() (which still runs broker preview_option first, never skipping it). Every other strategy remains paper. The max_position_usd on the row caps the ticket regardless of the global MAX_DOLLARS_PER_TRADE.

§3 · First-3-trades monitoring protocol

The first three live trades after authorization are watched by the operator in sequence. Each has a specific gate; failing any one is an operator-only revoke + investigation — there is no automation here.

TradeWhat the operator confirmsFlag / abort condition
Trade 1 Watch in real time. Confirm the broker order ID matches the local client_order_id. Record the actual fill price vs. the paper-expected fill price. fill_diff > 5% above mid → possible routing problem → revoke + investigate.
Trade 2 Confirm trade 1 has reconciled — the broker says CLOSED before trade 2 opens (no overlap). Compare trade 1's close R-multiple to the paper baseline distribution. R-multiple is an outlier (> 2σ from the paper median) → flag.
Trade 3 Confirm rows are landing in webull_live_orders.db AND in webull_paper_trades with is_live=1. Verify the kill switch is reachable: run curl -X POST .../api/webull/f4/kill/long_call, confirm the revoke, then re-authorize and let the bot run. Rows missing from either DB, or kill switch unreachable → flag + revoke until fixed.
The trade-3 kill-switch drill is mandatory: you prove you can stop the experiment before you let it run unattended. Re-authorization after the drill is a fresh row (the revoked row stays in history).

§4 · Convergence check (every 10 live closes)

This is the science. Live execution is a second, independent dataset; the edge claim requires it to agree with paper. After every 10 live closes for an authorized strategy, run:

WITH live AS (
    SELECT COUNT(*) AS n,
           AVG(CASE WHEN realized_r > 0 THEN 1.0 ELSE 0.0 END) AS p_hat_live,
           AVG(realized_r) AS mean_r_live
    FROM webull_paper_trades
    WHERE webull_strategy_id=? AND is_live=1 AND status='closed'
),
paper AS (
    SELECT COUNT(*) AS n,
           AVG(CASE WHEN realized_r > 0 THEN 1.0 ELSE 0.0 END) AS p_hat_paper,
           AVG(realized_r) AS mean_r_paper
    FROM webull_paper_trades
    WHERE webull_strategy_id=? AND is_live=0 AND status='closed'
)
SELECT live.p_hat_live, paper.p_hat_paper,
       ABS(live.p_hat_live - paper.p_hat_paper) AS delta,
       live.mean_r_live, paper.mean_r_paper
FROM live, paper;

Convergence rule on delta = |p_hat_live − p_hat_paper|:

deltaVerdictAction
< 0.10CONVERGENTPaper edge replicates on live — continue.
0.10 – 0.15WATCHDrift; record but continue.
≥ 0.15DIVERGENTProbable regime shift OR market microstructure (slippage / fills / IV smile) eats the edge. Auto-kill the strategy, alert operator.

Auto-divergence detection is a candidate for future automation; in v1 the operator runs this query manually and decides. Divergence is not a failure of the rail — it is exactly the finding Phase 4 exists to surface: a paper edge that does not survive real execution is a real (and valuable) negative result.

§5 · Kill-switch operations

Three reachable surfaces. The kill revokes the authorization row — the next open_trade() for that strategy reverts to paper. No service restart needed.

1 · From any phone with bash (the intended use case)

curl -X POST https://gpu3.<host>/api/webull/f4/kill/long_call \
     -H "Content-Type: application/json" \
     -d '{"reason":"phone kill - lunch meeting"}'

Expected response:

{
  "status": "revoked",
  "strategy_id": "long_call",
  "open_live_positions": 2,
  "next_open_trade_routes_to": "paper"
}

2 · From the dashboard (cursor's lane)

/webull-live-dashboard — a kill button per strategy plus a red KILL ALL (double-click) that hits POST /api/webull/f4/kill/all and cascades over every active authorization.

3 · From the CLI (during dev)

python3 scripts/phase4_authorize_strategy.py --strategy long_call --revoke --reason "kill"
Open live positions are NOT auto-cancelled by the kill switch. Revoking the authorization stops new live trades; positions already at the broker are operator-managed (defensible — market context matters at the moment of a kill). To cancel the open broker orders too, re-run the kill with ?cancel_open_orders=true, or hit the broker directly via webull_live_adapter.cancel_order(client_order_id).

§6 · Rollback / loss-floor condition

When MAX_DAILY_REALIZED_LOSS = $250 is breached, the adapter enforces the floor without operator intervention:

  1. webull_live_adapter.check_position_limits() returns a blocked reason.
  2. All subsequent live trade attempts return {"status":"blocked","reason":"MAX_DAILY_REALIZED_LOSS breached: today_pnl=-$251.34"}.
  3. The routing layer falls back to the paper bridge — observations keep accumulating to the paper baseline, but no new live trades fire.
  4. Open live positions are NOT auto-closed — the operator decides (market context matters).
  5. At UTC midnight the daily counter resets. The operator's next-day decision: re-authorize or leave revoked.

SQL the operator uses to inspect the day's realized live loss:

SELECT SUM(pnl_usd) AS today_realized
FROM webull_paper_trades
WHERE is_live=1 AND status='closed'
  AND closed_at >= strftime('%s', 'now', 'start of day');
The loss floor is a circuit breaker, not a stop-loss on individual positions. It halts new risk-taking for the day once the cumulative realized live loss crosses the threshold; it is intentionally blunt. The per-strategy MAX_CONSECUTIVE_LIVE_LOSSES=5 auto-fallback (a strategy that loses 5 live trades in a row reverts to paper) is the finer-grained sibling control, enforced in the adapter.