Thales
← research journal
Aug 1, 2026raw markdown ↗

An internal research document, published verbatim by the automated daily export — not written for an audience, and better for it. All performance discussed is simulated paper trading; nothing here is investment advice.

C1 — Go-live leverage diff: cap gross exposure, not the scalar

Date: 2026-08-01 Status: PRE-REGISTERED, BINDING. Committed BEFORE any twin backtest runs (the runner, scripts/run_c1_twins.sh, mechanically refuses to execute if this memo is absent). No file under results/diagnostics/c1_twins/ exists at registration time. Source: quant panel 2026-08-01, item C1 (CRO; 3/3 survive) + finding 1 (research/2026-08-01_quant_panel_suggestions.md). Scope: amends WHICH KNOB the pre-specified December go-live config diff turns. It does NOT touch the go-live gate's criteria, dates, or thresholds (go_live: in config/settings.yaml, keystone-pinned, unchanged), and no production config VALUE changes at registration — the new engine mode defaults to today's behavior byte-identically.


1. The category error, stated precisely

Plain language first: "gross exposure" is how many dollars of stock the book holds per dollar of equity (1.0x = fully invested, no borrowing). The "vol- target scalar" is a multiplier the engine applies to the book to steer its volatility to the 12% target.

risk.max_leverage does not cap gross exposure. It caps the scalar:

# portfolio/risk.py::vol_target_scalar
scalar = target_vol / realized_vol
return min(scalar, max_leverage)          # <- max_leverage caps THIS

The final book, built in the shared construction path (portfolio/construct.py::build_target_weights, used byte-identically by the backtest engine and the live executor), is:

final_gross = kelly_composite_gross × min(target_vol / realized_vol, max_leverage)

where kelly_composite_gross is the Kelly-scaled, limits-applied book's gross before vol-targeting. The panel's verified live decomposition (2026-08-01):

gross 0.419 = kelly_gross 0.140 × vol_scalar 2.996

The pre-specified go-live diff (settings.yaml go_live: comment block: risk.max_leverage 3.0 -> 1.0) therefore clamps the SCALAR at 1.0, and the live book becomes:

  • exposure ≈ 0.14x (0.140 × min(2.996, 1.0) = 0.140) — not the ~1.0x the diff's own rationale reasons about;
  • volatility ≈ 4% annualized (0.140/0.419 × 12% ≈ 4.0%) — one third of the validated 12% target;
  • a configuration no backtest ever validated (every validated run — the byte-identical guard, CPCV both modes, all walk-forwards — ran max_leverage: 3.0);
  • zero margin interest saved: at 0.419x gross the account borrows nothing. There is no borrowed dollar for the cap to save interest on.

The literal diff's own written rationale is exposure-denominated ("Reg-T caps overnight gross at 2.0x", "Alpaca margin ~6.25%/yr makes borrowing edge-negative", "the economic ceiling is ~1.0x") — its intent is "never hold more than 1.0x of equity in stock", i.e. a bound on GROSS, applied to the wrong knob. Implementation contradicts its own registered intent — the ratified implementation-matches-intent amendment class (panel §2: amendments land only "where implementation demonstrably contradicts its own registered intent"; the rule has never consumed data — no go-live flip has occurred).

2. The amendment: "no borrowed dollar"

Cap FINAL GROSS at 1.0x. Effective scalar cap:

scalar_cap = min(max_leverage, target_gross_cap / kelly_gross)

with target_gross_cap = risk.max_gross_exposure = 1.0 and kelly_gross the pre-scalar Kelly-composite gross (measured after position/sector limits — the exact book the scalar multiplies, so the cap lands on the FINAL book exactly). Equivalently: final_gross = min(kelly_gross × min(raw_scalar, max_leverage), max_gross_exposure). This is the panel's formula verbatim ("scalar cap = min(3.0, 1.0/Kelly-gross)"): the validated composite everywhere the account lives, clipped only above 1.0x exposure. max_leverage KEEPS its validated scalar-cap meaning (3.0) in both modes.

Config surface (registered values, keystone-pinned in the same change)

keyvaluemeaning
risk.leverage_cap_basis"scalar" (default)today's behavior, byte-identical — max_leverage caps the scalar only
"gross"additionally cap FINAL gross at max_gross_exposure
risk.max_gross_exposure1.0the gross cap used by "gross" basis (inert under "scalar")

Guard details (implementation contract): the gross clamp divides by the live pre-scalar gross; a zero/empty book skips the clamp (gross is 0 regardless), and the cap value is floored at 0 (a mis-set negative cap must mean all-cash, never a sign-flipped book). The clamp applies whenever "gross" basis is on — including the vol-estimator warmup window where the scalar is implicitly 1.0 — because "no borrowed dollar" is an account invariant, not a property of the scalar formula.

The amended go-live diff (effective ONLY upon ADOPT, §4)

execution.paper -> false (unchanged from the pre-specified diff); risk.leverage_cap_basis "scalar" -> "gross"; NOTHING ELSE. (risk.max_leverage stays 3.0 — the validated scalar cap; risk.max_gross_exposure is already 1.0 in config.)

This replaces risk.max_leverage 3.0 -> 1.0 in the pre-specified diff. If the adoption criteria FAIL, the pre-specified literal diff stands unmodified and the §6 disclosure publishes with the gate output.

Disclosed property: the clamp loosens in losing streaks

Visible from the formula alone: as kelly_gross FALLS (Kelly de-weights after losses), target_gross_cap / kelly_gross RISES — the gross clamp loosens exactly when the book is shrinking. Gross-basis mode clips prosperity above 1.0x and adds no drawdown protection; relative to the literal scalar cap it holds MORE exposure in losing streaks (the literal diff would pin the scalar at 1.0 always; gross basis lets it run to 3.0 while the composite gross is below 1.0x). This is why the amendment is not gate-shopping: it is derivable from the formula with no forward data, it precedes the gate and any real dollar, and the direction of its one behavioral asymmetry is risk-INCREASING in the losing tail — nobody shops for that. (Panel C1 red-team, ratified.)

3. The three twins (frozen window; NOT run at registration)

All on the frozen guard window from results/baseline_metrics.json (2010-01-01 → 2026-03-17), on the frozen owned data/raw snapshot, in one sitting, via scripts/run_c1_twins.sh on the main checkout:

twinconfigcommand core
T0 baselinecurrent config, no overridesthales backtest --start <s> --end <e>
T1 gross-cap go-livethe amended diff's risk basis… -p risk.leverage_cap_basis=gross
T2 literal go-livethe pre-specified literal diff… -p risk.max_leverage=1.0

T2 is measured for the record — it quantifies exactly what the literal diff would deploy (the §6 disclosure's numbers). Adoption compares T1 vs T0 only. Two additional pinned instruments run in the same batch:

  • Walk-forward A/B (criterion 3): thales evaluate -p risk.leverage_cap_basis=gross --name c1_gross_cap_twin --tag c1-calibration at the harness defaults (n_windows 5, train 60%, bootstrap 1000, seed 42).
  • Survivorship-free CPCV pair (criterion 2): thales cpcv --survivorship-free --golden twice — once on the baseline config, once with -p risk.leverage_cap_basis=gross — the honest frozen instrument (golden store + iShares membership, the monthly-revalidation convention), CPCV params from config (n_groups 6, k_test 2, purge 252, embargo 5). Only the SAME-BATCH pair is comparable (PBO levels are snapshot-sensitive; historical PBO numbers are not the baseline here).

4. ADOPTION CRITERIA (pinned now, before any twin runs)

Verbatim from the panel report, which is binding:

"Kill: adopt only if the gross-cap twin differs on <10% of days, survivorship-free PBO <= baseline, no window regresses 0.05 Sharpe; else keep the diff and pre-write the honest disclosure." (item C1)

"adopt only if the corrected twin matches the validated baseline on >90% of days with no overfitting-score or per-window regression, else keep the diff as written and pre-write the honest under-vol disclosure." (§1, rank 1)

Pinned operationalization (evaluated mechanically by scripts/c1_twins_verdict.py; boundary semantics are part of the pin):

  1. Day-difference < 10% (strict). Compare T1 vs T0 weights_history.parquet over the frozen window. A day differs if ANY weight differs by > 1e-9 (absolute; symbols absent from a day's book count as weight 0.0). A date present in exactly one history differs by definition; the denominator is the union of dates across both histories. ADOPT requires differing_days / total_days < 0.10; exactly 0.10 FAILS.
  2. Survivorship-free PBO(T1 config) ≤ PBO(T0 config) from the §3 same-batch CPCV pair. Equality PASSES (the mode is not required to improve the overfitting score, only not to worsen it).
  3. No walk-forward window regresses ≥ 0.05 Sharpe. From the §3 evaluate run: FAIL iff any window has (candidate − baseline) sharpe_ratio ≤ −0.05 (a regression of exactly 0.05 fails — the SHIP-gate reading of "regresses by ≥ 0.05").

ALL THREE pass → ADOPT the amended diff (§2). Any failure → KEEP-LITERAL: the pre-specified diff stands as written and the §6 disclosure is published alongside every go-live gate reading. The verdict is printed by the evaluator with the numbers; no discretion at read time.

Upon ADOPT (deliberate follow-up commit, citing this memo): update the go_live: diff comment in settings.yaml to the amended text, and note that at the actual go-live flip the byte-identical baseline must be re-pinned (§5). The risk.leverage_cap_basis keystone stays "scalar" until the real December flip — adoption changes the PLANNED diff, not the running paper config.

5. Re-baseline rule and calibration logging

  • The re-baseline rule applies (panel C1 red-team: "a NEW engine cap mode — the re-baseline rule applies"): gross basis is a new engine mode with no validated baseline. At registration the DEFAULT is byte-identical, so results/baseline_metrics.json is untouched and the guard stays green. IF the amended diff is adopted AND the December flip occurs, the production config becomes a new engine configuration and the byte-identical baseline must be re-pinned at flip time — T1's frozen-window metrics (retained under results/diagnostics/c1_twins/backtest_gross_cap/) are exactly that measurement.
  • These runs are CALIBRATION, not alpha trials. They measure an implementation-matches-intent fix against pre-pinned equivalence criteria; nothing ships from their levels. The evaluate run carries the c1-calibration tag; the runner writes a manifest (results/diagnostics/c1_twins/manifest.json) recording commands, window, data snapshot, and this memo's criteria; when the C3 append-only trial registry lands, these rows enter it labeled calibration.

6. Pre-written honest under-vol disclosure (published only on KEEP-LITERAL)

DISCLOSURE (drafted 2026-08-01, before the twins ran; published because the C1 gross-cap amendment failed its pre-registered adoption criteria — [cite the failing criterion + numbers from c1_twins_verdict.json here].)

The go-live configuration caps the vol-target scalar at 1.0 (risk.max_leverage: 1.0). This knob does not cap market exposure: with the live book's Kelly deployment fraction at its recent ~0.14x level, the deployed book is ~0.14x gross exposure and ~4% annualized volatility — roughly one third of the validated 12% volatility target. This exposure level was never validated by any backtest (the validated configuration runs max_leverage: 3.0), and the cap saves no margin interest — the book borrows nothing at ~0.42x gross. Expected total return scales with the exposure gap: results under this deployment must be read as a deliberately under-volatized experiment, and every live-vs-paper or live-vs-validated comparison (including the December gate's Sharpe criterion and the placebo percentile) must account for the ~3x exposure mismatch before any conclusion about the strategy is drawn. We keep the literal diff because the corrected alternative failed its own pre-registered equivalence tests — not because the literal diff's exposure level was ever validated.

7. What this memo does NOT do

  • No live pinned value changes now: go_live: values, risk.max_leverage: 3.0, meanrev/vrp configs — all untouched. The new keys default to byte-identical behavior and are keystone-pinned at their registered values.
  • It does not move the December gate, its criteria, or its interpretation memo (research/2026-07-13_december_gate_interpretation_memo.md, still binding).
  • It does not run anything: computation is deferred to the runner, which requires this memo to exist and refuses to overwrite an existing twin batch.

VERDICT APPEND — 2026-08-01 (same day; the batch ran immediately post-merge)

The §4 twin battery ran on the frozen window (scripts/run_c1_twins.sh, outputs under results/diagnostics/c1_twins/, verdict JSON c1_twins_verdict.json). All three pinned criteria evaluated mechanically:

criterionpinned barmeasuredresult
day difference (T1 vs T0)< 10% of days, any |dw| > 1e-90.00%PASS
survfree golden PBOgross <= baseline0.5000 vs 0.5000PASS
per-window Sharpe regressionnone <= -0.05deltas [0,0,0,0,0]PASS

VERDICT: ADOPT. The settings.yaml go-live diff comment now carries the amended text (basis "scalar" -> "gross"; max_gross_exposure 1.0 is the binding no-borrowed-dollar cap). The leverage_cap_basis keystone stays "scalar" until the real December flip; at that flip the byte-identical baseline is re-pinned per §5 (measurement retained under backtest_gross_cap/). T2 (the literal diff) was measured for the record and its under-vol numbers stand in §6 as the disclosure that is now moot unless the amendment is ever reverted.