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).
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.)
Four numbers, all from the gate, all defined in the F7 whitepaper — reported, not re-derived:
| Symbol | Meaning | Source |
|---|---|---|
p̂ | point estimate = wins / n_closed | gate p_hat |
p_lower | Wilson 95% lower bound (F7 §4) | gate p_lower |
p_upper | Wilson 95% upper bound (F7 §4) | gate p_upper (computed by the same Wilson formula, z=1.96) |
b | R-multiple win/loss ratio = avg_win_r / avg_loss_r (Phase 2.5 §1.2) | gate b |
break_even | 1/(1+b) — the Kelly break-even (F7 §3) | gate break_even |
The operative verdict logic (exactly what f7_gate() returns, per F7 whitepaper §2):
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 p̂).
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:
p_upper < break_even: the entire 95% interval is below
break-even. The null is falsified the other way — this bucket is positively shown to lack an edge, not
merely unproven. Retire it.p_lower ≤ break_even < p_upper: break-even sits inside
the interval. The gate rejects (lower bound didn't clear) but the data cannot rule an edge out either.
Flag for continued observation rather than permanent retirement.
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 p̂ 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.
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:
| Classification | Condition | Reading |
|---|---|---|
| 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 replication | 1 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. |
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.
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_id | CandleVision F7 counterpart | Baseline 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 p̂ 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.
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.
long_call (fabricated)long_call stands at n_closed=5
(single arm) — nowhere near the n≥100 gate.
strategy_id = long_call verdict = EDGE_CONFIRMED verdict_ts = 2026-08-14T17:32:10Z n_closed = 120
| r<-3 | -3..-1 | -1..0 | r=0 | 0..1 | 1..3 | r>3 |
|---|---|---|---|---|---|---|
| 3 | 14 | 29 | 1 | 38 | 28 | 7 |
median = +0.34 IQR = [-0.71, +1.12] max_drawdown_r = -4.2 max_win_r = +5.8
edge n=68 exhaustion n=42 hybrid n=10 → FULL REPLICATION (3 arms; two ≥10% of 120, top arm 57% ≤ 80%)
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.
(verbatim block from §6 above — "replicated result, not a flip authorization" — appended to every report).