Webull F7 Verdict Readout ProtocolPHASE 3 · PRE-REGISTERED REPORT FORMAT

Webull F7 (options) · Phase 3 · 2026-06-18 · ~7 min read · Pre-defines the report a bucket gets when it clears — written now so it is not invented under pressure


Purpose. When a webull_strategy_id bucket crosses the F7 gate — to EDGE_CONFIRMED or EDGE_REJECTED — this document is the standardized readout that gets filled in. It is pre-registered on purpose: a report format chosen before the result is known cannot be reverse-engineered to flatter the result. Each section below states what it reports, the SQL/Python recipe that produces it, and how to read it. The math itself is not re-derived here — it is binding in the F7 Options whitepaper (Wilson §4, break-even §3, Kelly §5). This protocol produces the replicated verdict only; it is silent on what the operator does about it (§6).
Contents
  1. §1 Bucket identity
  2. §2 Confidence math & the triggering inequality
  3. §3 R-distribution
  4. §4 Replication-arm provenance
  5. §5 Comparison to the CandleVision F7 baseline
  6. §6 Operator decision frame
  7. Worked example (fabricated)

§1 · Bucket identity

The header of every verdict report. Records which bucket cleared, which way, when, and on how many closed trades. The canonical source of the verdict itself is the gate function paper_trade_service.f7_gate() (served at GET /api/webull/f7/gate) — there is no f7_gate_view; the gate is computed, not a stored view. Capture the live snapshot:

curl -s http://127.0.0.1:8030/api/webull/f7/gate \
  | python3 -c "import sys,json; d=json.load(sys.stdin); \
      b=[x for x in d['buckets'] if x['strategy_id']=='<bucket_id>'][0]; \
      print(b['strategy_id'], b['verdict'], b['n_closed'])"

and the supporting n_closed directly from the trade table (the gate counts exactly these rows):

SELECT webull_strategy_id AS strategy_id,
       COUNT(*)           AS n_closed,
       strftime('%Y-%m-%dT%H:%M:%SZ','now') AS verdict_ts
FROM webull_paper_trades
WHERE webull_strategy_id = '<bucket_id>'
  AND status='closed' AND pnl_usd IS NOT NULL;

The verdict timestamp is the moment the readout is generated, recorded in ISO-8601 UTC. n_closed at verdict time is frozen into the report — later accumulation does not retroactively edit a published verdict. (Table lives in webull_paper_trades.db (Phase 3.1 dedicated baseline); the gate query filters status='closed' AND pnl_usd IS NOT NULL so status-flipped-but-unclosed rows can never inflate the count.)

§2 · Confidence math & the triggering inequality

Four numbers, all from the gate, all defined in the F7 whitepaper — reported, not re-derived:

SymbolMeaningSource
point estimate = wins / n_closedgate p_hat
p_lowerWilson 95% lower bound (F7 §4)gate p_lower
p_upperWilson 95% upper bound (F7 §4)gate p_upper (computed by the same Wilson formula, z=1.96)
bR-multiple win/loss ratio = avg_win_r / avg_loss_r (Phase 2.5 §1.2)gate b
break_even1/(1+b) — the Kelly break-even (F7 §3)gate break_even

The operative verdict logic (exactly what f7_gate() returns, per F7 whitepaper §2):

n_closed < 100 → INSUFFICIENT_SAMPLE (keep accumulating) n_closed ≥ 100 AND p_lower > break_even → EDGE_CONFIRMED n_closed ≥ 100 AND p_lower ≤ break_even → EDGE_REJECTED

EDGE_CONFIRMED fires when the Wilson lower bound clears break-even — the conservative test: even the pessimistic end of the 95% interval beats the cost of being wrong. That is the F7 discipline (size off p_lower, never ).

Strength-of-rejection annotation (readout layer, not a gate state). An EDGE_REJECTED verdict is sub-classified in the report by where the Wilson upper bound sits — this is interpretation the readout adds on top of the gate's binary verdict, it does not change what f7_gate() returns:

§3 · R-distribution

A verdict number hides the shape of the outcomes that produced it. The readout publishes the full distribution of realized_r (R-multiples, max-loss-normalized — the defined-risk-honest unit) across the n closes: a 7-bin histogram plus median, IQR, max-drawdown-R and max-win-R. A high built on a few enormous winners and many small losers is a different animal than a steadily-positive one, and the histogram is where that shows.

WITH closes AS (
  SELECT realized_r FROM webull_paper_trades
  WHERE webull_strategy_id = '<bucket_id>'
    AND status='closed' AND pnl_usd IS NOT NULL
)
SELECT
  SUM(CASE WHEN realized_r < -3                          THEN 1 ELSE 0 END) AS r_lt_neg3,
  SUM(CASE WHEN realized_r >= -3 AND realized_r < -1     THEN 1 ELSE 0 END) AS r_neg3_neg1,
  SUM(CASE WHEN realized_r >= -1 AND realized_r < 0      THEN 1 ELSE 0 END) AS r_neg1_zero,
  SUM(CASE WHEN realized_r = 0                            THEN 1 ELSE 0 END) AS r_zero,
  SUM(CASE WHEN realized_r > 0 AND realized_r <= 1       THEN 1 ELSE 0 END) AS r_zero_one,
  SUM(CASE WHEN realized_r > 1 AND realized_r <= 3       THEN 1 ELSE 0 END) AS r_one_three,
  SUM(CASE WHEN realized_r > 3                            THEN 1 ELSE 0 END) AS r_gt_three,
  MIN(realized_r) AS max_drawdown_r,
  MAX(realized_r) AS max_win_r
FROM closes;

Median and IQR are awkward in SQLite — compute them in Python alongside:

import sqlite3, statistics
con = sqlite3.connect("webull_paper_trades.db")
rs = [r[0] for r in con.execute(
    "SELECT realized_r FROM webull_paper_trades "
    "WHERE webull_strategy_id=? AND status='closed' AND pnl_usd IS NOT NULL",
    ("<bucket_id>",)).fetchall() if r[0] is not None]
q1, med, q3 = statistics.quantiles(rs, n=4)   # 25th, 50th, 75th percentile
print("median", med, "IQR", q1, "→", q3, "min", min(rs), "max", max(rs))

Report the seven bin counts as a sparkline/bar row, then the five summary stats. The IQR width relative to the median is the at-a-glance read on whether the edge is steady (tight IQR) or lottery-shaped (wide IQR, fat right tail) — the same ergodicity concern the F7 whitepaper §5 raises about sizing.

§4 · Replication-arm provenance

Phase 3's whole point is replication: the same F7 methodology run against five different signal types (the five labs). A verdict supported by several arms is sturdier than one carried by a single lab. The webull_lab column (added Phase 3a) records which arm produced each close:

SELECT webull_lab, COUNT(*) AS n_closes
FROM webull_paper_trades
WHERE webull_strategy_id = '<bucket_id>'
  AND status='closed' AND pnl_usd IS NOT NULL
GROUP BY webull_lab
ORDER BY n_closes DESC;

Classify the verdict's replication breadth:

ClassificationConditionReading
Full replication≥ 2 arms each contributing ≥ 10% of closes The F7 methodology produced the verdict across different signal types — high confidence in the result.
Partial replication≥ 2 arms, but one dominates ≥ 80% Supported, but mostly one lab — possible lab-specific effect. Still valid; flag for follow-up.
Narrow replication1 arm only "This verdict reflects ONE arm of the planned 5-arm study; the result is real for that arm but is not yet a replicated cross-arm finding." Not invalidating — honest about scope.
A narrow-replication verdict is not downgraded or hidden — it is reported with the explicit one-arm caveat. Today (Phase 3 open) the three populated buckets (long_call, bull_put_ladder, short_box_spread) are all single-arm (edge-lab); once the Phase-3a bridge feeds the other four labs, re-running this query is what upgrades a verdict from narrow to partial/full.

§5 · Comparison to the CandleVision F7 baseline

The F7 methodology was first run on CandleVision binary candle-count + MA-stacking exhaustion data (the binary labs). Where a Webull strategy_id corresponds to a setup family that the CandleVision F7 work actually studied, the readout cites the original baseline and discusses congruence. Where no such mapping exists, the readout says so explicitly — silence is the honest answer, not a fabricated comparison.

Webull strategy_idCandleVision F7 counterpartBaseline finding to cite
long_call (directional-bull) Maps to the directional-bull exhaustion-reversal family studied by the Binary Exhaustion Lab (hybrid_v1) and the vision-directional read in the TRAIL Phase 1.5 study. The binary baseline did not clear a durable edge: TRAIL Phase 1.5 reported a 36% directional hit rate (< random, n=11), and the project's headline finding #1 is that stated confidence overstated realized p_lower by 5–15 pts, collapsing below break-even after friction. Cite the binary verdict as null-not-rejected, not as a clean p̂_baseline — the honest baseline is "no edge isolated."
bull_put_ladder No direct CandleVision map — a multi-leg credit structure, not expressible as a single-candle binary bet. Report explicitly: "no CandleVision baseline mapping exists for this strategy_id — this is a Webull-only finding."
short_box_spread No direct CandleVision map — a near-arbitrage structure, not directional. Same: "Webull-only finding; no CV baseline."

When a baseline exists, report congruence as: does the Webull fall within the CandleVision baseline's 95% CI (congruent) or outside it (divergent)? A directional-bull long_call verdict that confirms an edge where the CandleVision binary version found none is the most interesting possible result — it would mean the options payoff-shaping rescued a signal the binary contract could not monetise (the pivot's founding hypothesis, WEBULL_PIVOT §1.4 finding 5). Report that congruence/divergence plainly; do not editorialize beyond the numbers.

§6 · Operator decision frame

This is a replicated result, not a flip authorization. The decision to deploy real capital against this bucket is a separate, operator-only step covered in the future Phase 4 live-flip ceremony document. The current readout produces only the replicated verdict — what the science says — and is silent on what the operator does about it. Paper-mode binding (WEBULL_PAPER=true) remains in force for the rail regardless of this verdict; the only path to live execution is operator-signed via the Phase 4 ceremony.

Worked example — long_call (fabricated)

Fabricated for documentation; not a real verdict. The numbers below were chosen to illustrate the readout format, not to predict future results. As of this writing long_call stands at n_closed=5 (single arm) — nowhere near the n≥100 gate.

§1 Identity

strategy_id = long_call   verdict = EDGE_CONFIRMED
verdict_ts  = 2026-08-14T17:32:10Z   n_closed = 120

§2 Confidence math

p̂ = 0.612 p_lower = 0.519 p_upper = 0.694 (Wilson 95%) b = 1.43 (avg_win_r 0.85 / avg_loss_r 0.594) break_even = 1/(1+1.43) = 0.412 VERDICT: EDGE_CONFIRMED ⇐ p_lower 0.519 > break_even 0.412 ✓

§3 R-distribution (n=120)

r<-3-3..-1-1..0r=00..11..3r>3
31429138287
median = +0.34   IQR = [-0.71, +1.12]   max_drawdown_r = -4.2   max_win_r = +5.8

§4 Replication-arm provenance

edge        n=68
exhaustion  n=42
hybrid      n=10
→ FULL REPLICATION (3 arms; two ≥10% of 120, top arm 57% ≤ 80%)

§5 CandleVision baseline

CandleVision directional-bull exhaustion p̂_CV = 0.587
Webull p̂ = 0.612 falls inside the CV 95% CI → CONGRUENT.
Note: the CV binary version never cleared break-even after friction; the Webull
defined-risk long_call clearing EDGE_CONFIRMED would be a payoff-shaping result
(WEBULL_PIVOT finding #5) — report plainly, do not over-claim.

§6 Operator frame

(verbatim block from §6 above — "replicated result, not a flip authorization" — appended to every report).