feat(forward): cabla STATARB-RESID nel forward-monitor PAPER (lead ortogonale ETH/BTC)

Forward-monitor del LEAD dello sweep 2026-06-29 (relative-value ETH/BTC, dollar-neutral 2 gambe),
il primo stream insieme ORTOGONALE (corr->book 0.027, beta-mkt 0.013) ED eseguibile a $600.

- scripts/live/paper_statarb.py: forward-only, doppio libro MODELED($2000)/REAL-$600 (haircut fill),
  riusa il segnale ESATTO di orthogonal_signals.py (niente reimplementazione). Config CONGELATA
  W=45 sgn=+1.
- Cablato in scripts/cron_daily.sh accanto a paper_prevday. Stato runtime in data/paper_statarb/
  (gitignored).
- test tests/test_paper_statarb.py (frozen config + advance forward/idempotente + haircut $600 basso).

Correzione di etichetta (verificata): la cella vincente e' sgn=+1 -> NON mean-reversion ma
relative-MOMENTUM sul residuo (dislocazioni ETH-vs-BTC continuano a 1d; sgn=-1 perde -1.4 IS).
Diario + CLAUDE.md aggiornati. Test 146/146. Nessun deploy, forward-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-29 20:58:29 +00:00
parent cff5fa2bf5
commit c52e0ab3f8
6 changed files with 258 additions and 11 deletions
+1
View File
@@ -66,3 +66,4 @@ scripts/research/blind/leaderboard.json
# forward-monitor runtime state (regenerable, forward-only) # forward-monitor runtime state (regenerable, forward-only)
data/paper_prevday/ data/paper_prevday/
data/paper_combo/ data/paper_combo/
data/paper_statarb/
+10 -6
View File
@@ -139,12 +139,16 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
candidato ucciso dal gate giusto (deflated-Sharpe, is_hedge, selection-on-holdout, sostituzione-XS01, candidato ucciso dal gate giusto (deflated-Sharpe, is_hedge, selection-on-holdout, sostituzione-XS01,
multi-cut). Niente batte/diversifica XS01 (varianti = REDUNDANT); meta-allocazione < pesi fissi (i 4 multi-cut). Niente batte/diversifica XS01 (varianti = REDUNDANT); meta-allocazione < pesi fissi (i 4
sleeve già quasi-risk-parity); 1-gamba a segnale = TP01 travestito (trend) o hedge a DSR<0.95. sleeve già quasi-risk-parity); 1-gamba a segnale = TP01 travestito (trend) o hedge a DSR<0.95.
**LEAD forward-monitor:** **STATARB-RESID** (mean-reversion del residuo ETH−β·BTC, β OLS rolling, 2 gambe) **LEAD forward-monitor:** **STATARB-RESID** (relative-MOMENTUM del residuo ETH−β·BTC, β OLS rolling, 2 gambe,
— primo stream **insieme ortogonale (corr→book 0.027, β-mkt 0.013) ED eseguibile a $600** (haircut ~0, cella vincente **sgn=+1**: le dislocazioni ETH-vs-BTC CONTINUANO a 1d, la MR pura sgn=1 perde) — primo
NON STAT-MODE come XS01/opzioni): marginal ADDS, robust_oos, fee-survive 0.30%/gamba; resta sotto soglia stream **insieme ortogonale (corr→book 0.027, β-mkt 0.013) ED eseguibile a $600** (haircut ~0, NON
solo sull'**edge** (Sharpe 0.84, DSR 0.929 same-sign <0.95). Se una finestra forward conferma l'edge è STAT-MODE come XS01/opzioni): marginal ADDS, robust_oos, fee-survive 0.30%/gamba; resta sotto soglia solo
*deployabile*. Altri LEAD: IVOL (idio-vol XS, STAT-MODE), DVOLSPREAD (storia DVOL corta). Diario sull'**edge** (Sharpe 0.84, DSR 0.929 same-sign <0.95). **CABLATO in forward-monitor PAPER:**
`2026-06-29-strategy-search-5threads.md`. Script `scripts/research/{xsec_v3_lowrisk,xsec_v3_momstruct,meta_allocation,orthogonal_signals,signal_inout_1leg}.py`. `scripts/live/paper_statarb.py` (W=45/sgn=+1 congelati, doppio libro MODELED/REAL-$600), nel cron
giornaliero accanto a `paper_prevday`; test `tests/test_paper_statarb.py`. Se la finestra forward conferma
l'edge è *deployabile* (2 gambe BTC/ETH perp). Altri LEAD: IVOL (idio-vol XS, STAT-MODE), DVOLSPREAD
(storia DVOL corta). Diario `2026-06-29-strategy-search-5threads.md`. Script
`scripts/research/{xsec_v3_lowrisk,xsec_v3_momstruct,meta_allocation,orthogonal_signals,signal_inout_1leg}.py`.
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
@@ -51,7 +51,7 @@ scelta in-sample-only.
| segnale | FULL/HOLD | corr→book | β-mkt | marginal | DSR (grid/same-sign) | exec $600 | verdetto | | segnale | FULL/HOLD | corr→book | β-mkt | marginal | DSR (grid/same-sign) | exec $600 | verdetto |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| **STATARB-RESID** (mean-rev residuo ETH−β·BTC, W45) | 0.84/0.56 | **0.027** | 0.013 | **ADDS** | 0.056 / 0.929 | haircut ~0 | **LEAD** | | **STATARB-RESID** (residual relative-mom ETH−β·BTC, W45 **sgn+1**) | 0.84/0.56 | **0.027** | 0.013 | **ADDS** | 0.056 / 0.929 | haircut ~0 | **LEAD** |
| DVOLSPREAD (IV relativa, W60) | 0.74/0.77 | 0.017 | 0.012 | ADDS | 0.082 / 0.907 | haircut ~0 | LEAD | | DVOLSPREAD (IV relativa, W60) | 0.74/0.77 | 0.017 | 0.012 | ADDS | 0.082 / 0.907 | haircut ~0 | LEAD |
| RATIO-MOM/REV/ACCEL | 0.25-0.68 / ≤0 | ~0 | ~0 | NEUTRAL | <0.23 | ok | NEUTRAL (diversification-math) | | RATIO-MOM/REV/ACCEL | 0.25-0.68 / ≤0 | ~0 | ~0 | NEUTRAL | <0.23 | ok | NEUTRAL (diversification-math) |
| VOLSPREAD | 0.24/1.47 | ~0 | 0.013 | DILUTES | — | — | SCARTATO | | VOLSPREAD | 0.24/1.47 | ~0 | 0.013 | DILUTES | — | — | SCARTATO |
@@ -61,10 +61,15 @@ mercato 0.013 — meglio di SKH ~0.09) **ED eseguibile a $600** (book a 2 gambe
haircut ≈ 0, fee-survive fino a 0.30%/gamba → **NON STAT-MODE**, a differenza di XS01 e delle opzioni). Il haircut ≈ 0, fee-survive fino a 0.30%/gamba → **NON STAT-MODE**, a differenza di XS01 e delle opzioni). Il
muro che **resta** è l'**edge**: Sharpe 0.84 / DSR 0.929 same-sign (ottimistico) ma comunque <0.95. muro che **resta** è l'**edge**: Sharpe 0.84 / DSR 0.929 same-sign (ottimistico) ma comunque <0.95.
Verificato indipendentemente dal coordinatore: residuo causale (β OLS rolling backward, decisione a Verificato indipendentemente dal coordinatore: residuo causale (β OLS rolling backward, decisione a
close[i], return in i+1), fee a 2 gambe corretta. **LEAD forward-monitor** (monitorabile a costo reale close[i], return in i+1), fee a 2 gambe corretta. **⚠ Correzione di etichetta:** la cella vincente è
~0 grazie all'eseguibilità), NON deploy. **DVOLSPREAD** ri-valida l'ex-lead `dvol_spread`: ADDS ma storia **sgn=+1**, NON sgn=1 → NON è mean-reversion ma **momentum relativo sul residuo** (long lo spread quando
DVOL corta (2021+) → resta forward-monitor come già noto. Secondario crypto-vs-macro = "TSMOM travestito" ETH è ricco vs il suo fit su BTC: le dislocazioni ETH-vs-BTC *continuano* a 1d; la MR pura sgn=1 perde
(corr→book 0.17-0.20), non ortogonale. 1.4 in-sample). L'ortogonalità (β~0) viene dalla struttura dollar-neutral, non dal verso. → **LEAD
forward-monitor** (monitorabile a costo reale ~0), NON deploy. **CABLATO:** `scripts/live/paper_statarb.py`
(config CONGELATA W=45/sgn=+1, doppio libro MODELED/REAL-$600, riusa il segnale esatto di
`orthogonal_signals.py`), aggiunto al cron giornaliero accanto a `paper_prevday`; test `tests/test_paper_statarb.py`.
**DVOLSPREAD** ri-valida l'ex-lead `dvol_spread`: ADDS ma storia DVOL corta (2021+) → resta forward-monitor
come già noto. Secondario crypto-vs-macro = "TSMOM travestito" (corr→book 0.17-0.20), non ortogonale.
### Thread 5 — 1-gamba a segnale (eseguibile) ### Thread 5 — 1-gamba a segnale (eseguibile)
12 famiglie (MACD/RSI/Supertrend/Donchian/BBands/EMA, ±ADX) su 1d/12h/8h. **0/12 earns_slot_honest.** 12 famiglie (MACD/RSI/Supertrend/Donchian/BBands/EMA, ±ADX) su 1d/12h/8h. **0/12 earns_slot_honest.**
+1
View File
@@ -10,6 +10,7 @@ mkdir -p logs
uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni) uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni)
uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01 uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01
uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy) uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy)
uv run python scripts/live/paper_statarb.py # forward-monitor lead STATARB-RESID ETH/BTC ortogonale (PAPER, non deploy)
# NB: l'esecuzione Deribit e' passata al BOOK (TP01+SKH01 nettati) via scripts/cron_book.sh a # NB: l'esecuzione Deribit e' passata al BOOK (TP01+SKH01 nettati) via scripts/cron_book.sh a
# cadenza ORARIA (SKH01 e' a 230m: il daily mancherebbe gli ingressi). live_execute.py # cadenza ORARIA (SKH01 e' a 230m: il daily mancherebbe gli ingressi). live_execute.py
# (TP01-only) NON va piu' eseguito qui, sennò i due farebbero a pugni sullo stesso strumento. # (TP01-only) NON va piu' eseguito qui, sennò i due farebbero a pugni sullo stesso strumento.
+176
View File
@@ -0,0 +1,176 @@
"""FORWARD-MONITOR — STATARB-RESID (relative-value ETH/BTC, dollar-neutral 2 gambe), PAPER.
NON è esecuzione reale. È il monitoraggio forward-only del LEAD dello sweep 2026-06-29 (thread
"segnali ortogonali"): il PRIMO stream visto che è INSIEME ortogonale al book (corr→book 0.027,
beta-mkt 0.013) ED eseguibile a $600 (book a 2 gambe BTC+ETH perp, NON STAT-MODE come XS01). Resta
sotto la soglia solo sull'EDGE (Sharpe ~0.84, deflated-Sharpe <0.95 su storia ~2.5 anni). Questo
monitor accumula la finestra OUT-OF-SAMPLE VERA per decidere se l'edge regge — stesso trattamento di
PREVDAY/STA05/XS01 STAT-MODE. Nessun edge creduto prima.
ⓘ NATURA DEL SEGNALE (onesto): la cella vincente è W=45 **sgn=+1**, NON sgn=-1. Cioè il residuo di
ETH dopo β·BTC (β OLS rolling causale) viene seguito in MOMENTUM (long lo spread quando ETH è ricco
vs il suo fit su BTC), non in mean-reversion: le dislocazioni relative ETH-vs-BTC CONTINUANO a 1d
(la MR pura, sgn=-1, perde -1.4 in-sample). È relative-MOMENTUM, dollar-neutral → ortogonale al
trend assoluto di TP01.
CONFIG CONGELATA (dallo sweep, selezione IN-SAMPLE-only, mai sul max hold-out):
W=45, sgn=+1, tf=1d, vol-target 20%, leva cap 2x. Riusa il segnale ESATTO di
scripts/research/orthogonal_signals.py (nessuna reimplementazione → niente drift).
DESIGN (onesto, come paper_prevday):
- Legge BTC/ETH 1d certificati (build_joint), calcola il segnale CAUSALE su tutta la storia.
- Alla prima esecuzione fissa l'INCEPTION all'ultima barra 1d chiusa (forward-only: lo storico
NON entra nel PnL di paper).
- Ogni run processa le NUOVE barre 1d chiuse: applica il rendimento dello spread tenuto,
addebita le fee su 2 GAMBE (ogni Δpos muove ETH e BTC → 2×|Δpos|), registra i flip di segno.
- DUE libri in parallelo (onestà sull'esecuzione a piccolo capitale):
* MODELED : capitale nominale $2000, ribilanciamento continuo (fee su ogni |Δpos|).
* REAL-$600: capitale reale $600, salta i ribilanci di nozionale-per-gamba < min_order ($5)
→ il gap MODELED-REAL = l'haircut di fill reale (qui ~0: turnover bassissimo a 1d).
- Per barre fresche aggiornare prima i dati: uv run python scripts/analysis/rebuild_history.py --asset BTC ETH
Stato: data/paper_statarb/{state.json, trades.jsonl, returns.jsonl} (append-only).
uv run python scripts/live/paper_statarb.py # avanza col dato disponibile
uv run python scripts/live/paper_statarb.py --status # solo stato, non avanza
uv run python scripts/live/paper_statarb.py --reset # azzera (riparte da ora)
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
# Segnale ESATTO dello sweep (nessuna reimplementazione → identità garantita col backtest).
from orthogonal_signals import build_joint, f_statarb_resid, spread_ret # noqa: E402
STATE_DIR = PROJECT_ROOT / "data" / "paper_statarb"
STATE_FILE = STATE_DIR / "state.json"
TRADES_FILE = STATE_DIR / "trades.jsonl"
RETURNS_FILE = STATE_DIR / "returns.jsonl"
# --- CONFIG CONGELATA (frozen) -----------------------------------------------------------
W_FROZEN = 45
SGN_FROZEN = +1
FEE_SIDE = 0.0005 # 0.05%/side = 0.10% round-trip (Deribit taker)
MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo
REAL_CAPITAL = 600.0 # capitale mainnet reale
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
def _signal(j: pd.DataFrame):
"""(ts, dt, pos, sr): posizione vol-targeted sullo spread (decisa <= close[i]) + ritorno spread."""
pos = np.nan_to_num(f_statarb_resid(W=W_FROZEN, sgn=SGN_FROZEN)(j))
return (j["timestamp"].values.astype("int64"),
pd.to_datetime(j["datetime"]).values, pos, spread_ret(j))
def _state_io(write: dict | None = None):
if write is not None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(write, indent=2))
return write
return json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else None
def _append(path: Path, rec: dict):
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(path, "a") as f:
f.write(json.dumps(rec) + "\n")
def init_state(j: pd.DataFrame) -> dict:
ts, _, pos, _ = _signal(j)
last = int(ts[-1])
return dict(start_ts=last, last_ts=last, n_bars=0,
pos_modeled=float(pos[-1]), pos_real=float(pos[-1]),
cap_modeled=MODELED_CAPITAL, cap_real=REAL_CAPITAL,
peak_modeled=MODELED_CAPITAL, peak_real=REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
def advance(st: dict, j: pd.DataFrame) -> dict:
ts, dt, pos, sr = _signal(j)
new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]]
if not new:
return st
pm, pr = st["pos_modeled"], st["pos_real"] # posizioni TENUTE (decise alla barra precedente)
cm, cr = st["cap_modeled"], st["cap_real"]
pkm, pkr, ddm, ddr = st["peak_modeled"], st["peak_real"], st["dd_modeled"], st["dd_real"]
ntr = st.get("n_trades", 0)
for i in new:
s = float(sr[i])
tgt = float(pos[i]) # bersaglio deciso a close[i] (eseguito da i+1)
# MODELED: rendimento dello spread tenuto durante la barra i, poi ribilancia al target
net_m = pm * s - FEE_SIDE * 2.0 * abs(tgt - pm)
# REAL-$600: salta i ribilanci sub-min_order (nozionale per-gamba |Δpos|*cap < $5)
executed = abs(tgt - pr) * REAL_CAPITAL >= MIN_ORDER
new_pr = tgt if executed else pr
net_r = pr * s - FEE_SIDE * 2.0 * abs(new_pr - pr)
if np.sign(new_pr) != np.sign(pr):
_append(TRADES_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])),
action="FLIP", from_pos=round(pr, 4), to_pos=round(new_pr, 4)))
ntr += 1
cm *= (1.0 + max(net_m, -0.99)); cr *= (1.0 + max(net_r, -0.99))
pkm = max(pkm, cm); pkr = max(pkr, cr)
ddm = max(ddm, (pkm - cm) / pkm if pkm > 0 else 0.0)
ddr = max(ddr, (pkr - cr) / pkr if pkr > 0 else 0.0)
pm, pr = tgt, new_pr
_append(RETURNS_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])),
net_modeled=round(net_m, 6), net_real=round(net_r, 6),
pos=round(new_pr, 4), cap_modeled=round(cm, 2), cap_real=round(cr, 2)))
st.update(last_ts=int(ts[new[-1]]), n_bars=st.get("n_bars", 0) + len(new),
pos_modeled=pm, pos_real=pr, cap_modeled=cm, cap_real=cr,
peak_modeled=pkm, peak_real=pkr, dd_modeled=ddm, dd_real=ddr, n_trades=ntr)
return st
def print_status(st: dict, j: pd.DataFrame):
last_data = int(j["timestamp"].iloc[-1])
days = (last_data - st["start_ts"]) / 86400_000
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
rr = st["cap_real"] / REAL_CAPITAL - 1
print("\n STATARB-RESID forward-monitor (PAPER — relative-value ETH/BTC, ortogonale, NON deploy)")
print(f" config CONGELATA: W={W_FROZEN} sgn={SGN_FROZEN:+d} (residual relative-momentum, dollar-neutral)")
print(f" forward da {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} "
f"({st['n_bars']} barre 1d ~{days:.0f}g) flip di segno: {st['n_trades']}")
print(f" posizione spread corrente (long ETH / short BTC per unità): {st['pos_real']:+.3f}")
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (atteso ~0: turnover basso a 1d)")
print(f" log: {RETURNS_FILE}\n")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--status", action="store_true")
ap.add_argument("--reset", action="store_true")
args = ap.parse_args()
j = build_joint("1d")
if args.reset:
for p in (STATE_FILE, TRADES_FILE, RETURNS_FILE):
if p.exists():
p.unlink()
st = init_state(j); _state_io(st)
print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).")
print_status(st, j); return
st = _state_io()
if st is None:
st = init_state(j); _state_io(st)
print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).")
print_status(st, j); return
if not args.status:
st = advance(st, j); _state_io(st)
print_status(st, j)
if __name__ == "__main__":
main()
+60
View File
@@ -0,0 +1,60 @@
"""Lock del forward-monitor STATARB-RESID (scripts/live/paper_statarb.py):
* config CONGELATA (W=45, sgn=+1) — non deve cambiare silenziosamente;
* advance() processa SOLO le barre forward ed è IDEMPOTENTE alla punta;
* a 1d il libro REAL-$600 traccia il MODELED (haircut di fill piccolo).
Il segnale è quello esatto dello sweep (orthogonal_signals) → niente reimplementazione.
"""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts" / "live"))
import paper_statarb as ps # noqa: E402
def _fresh_state(j, back):
ts, _, pos, _ = ps._signal(j)
start = int(ts[-back - 1])
return dict(start_ts=start, last_ts=start, n_bars=0,
pos_modeled=float(pos[-back - 1]), pos_real=float(pos[-back - 1]),
cap_modeled=ps.MODELED_CAPITAL, cap_real=ps.REAL_CAPITAL,
peak_modeled=ps.MODELED_CAPITAL, peak_real=ps.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
def _redirect(tmp_path, monkeypatch):
monkeypatch.setattr(ps, "STATE_DIR", tmp_path)
monkeypatch.setattr(ps, "STATE_FILE", tmp_path / "state.json")
monkeypatch.setattr(ps, "RETURNS_FILE", tmp_path / "returns.jsonl")
monkeypatch.setattr(ps, "TRADES_FILE", tmp_path / "trades.jsonl")
def test_frozen_config():
"""La cella vincente in-sample è W=45 sgn=+1 (relative-momentum, non mean-reversion)."""
assert ps.W_FROZEN == 45
assert ps.SGN_FROZEN == +1
def test_advance_processes_forward_then_idempotent(tmp_path, monkeypatch):
_redirect(tmp_path, monkeypatch)
j = ps.build_joint("1d")
ts, *_ = ps._signal(j)
st = _fresh_state(j, back=60)
st = ps.advance(st, j)
assert st["n_bars"] == 60 # ha processato le 60 barre forward
assert st["last_ts"] == int(ts[-1])
assert st["cap_modeled"] != ps.MODELED_CAPITAL # l'equity si è mossa
# idempotente: ri-avanzare alla punta non aggiunge barre né muove l'equity
eq_before = st["cap_modeled"]
st2 = ps.advance(dict(st), j)
assert st2["n_bars"] == 60
assert st2["cap_modeled"] == eq_before
def test_real600_tracks_modeled_low_turnover(tmp_path, monkeypatch):
_redirect(tmp_path, monkeypatch)
j = ps.build_joint("1d")
st = ps.advance(_fresh_state(j, back=250), j)
rm = st["cap_modeled"] / ps.MODELED_CAPITAL - 1
rr = st["cap_real"] / ps.REAL_CAPITAL - 1
assert abs(rm - rr) < 0.05 # haircut di fill piccolo a 1d ($600)