fix(gtaa): costo IB reale (pavimento fisso) + esecuzione a banda — lo sleeve era cieco al capitale
DIFETTO. src/portfolio/gtaa.py modellava il costo come 2bps PROPORZIONALI e il modulo si
dichiarava "eseguibile a basso capitale, switch mensile/basso turnover". Entrambe false:
il vol-target e' CONTINUO (l'esposizione cambia ogni giorno, su 6 gambe) e IB non ha una
fee proporzionale ma un PAVIMENTO FISSO per ordine, min(max($0.35, $0.0035/az), 1% del
controvalore) = ~$530/anno indipendenti dal capitale.
Il modello vecchio era quindi CIECO AL CAPITALE: dava Sharpe 0.64 sia a $50.000 sia a $600.
Alla taglia grande era sostanzialmente giusto (0.64 vs 0.59-0.66 reali); a $600 la realta'
e' -0.55 di Sharpe e -3.5%/anno di CAGR. L'errore era tutto concentrato dove lo sleeve
verrebbe realmente deployato.
FIX. Il modulo ora:
* modella la commissione IB reale (ib_commission);
* esegue a BANDA + CADENZA (settimanale, $50/gamba) — config scelta sul PLATEAU del sweep
(weekly/monthly x banda 25-100 -> Sh 0.45-0.64 a ogni capitale), non sull'argmax
in-sample (banda $50 daily a $600 = cella isolata che crolla a banda 100);
* prende il CAPITALE allocato come parametro: gtaa_returns(capital=...);
* dichiara la soglia di deployabilita': GTAA_MIN_CAPITAL=$3.000 + gtaa_is_deployable();
* espone gtaa_rebalance_plan(held, capital) per l'esecutore — salta le gambe il cui
nozionale non supera la banda (un ordine da $12 costa $0.35 = 2.9%).
sleeves.py dichiara esplicitamente il capitale assunto (GTAA_DEFAULT_CAPITAL=$10.000 allocati
= book ~$50k al peso 20%) invece di nasconderlo.
IMPATTO SUL BOOK (misurato sostituendo la sola gamba GTAA, non citato):
GTAA01 standalone Sh 0.64 -> 0.61 CAGR 3.76% -> 3.65%
book 5 sleeve FULL 2.22 -> 2.22 HOLD 2.36 -> 2.38 maxDD 6.2% -> 6.0%
Trascurabile alla taglia assunta: il fix conta per il DEPLOY (a $600-2k passa da -3.5%/anno
a +3.0%/anno). PESI INVARIATI -> nessun weights_tilt_null richiesto.
CORREZIONE A UN ERRORE DI ANALISI DELLA SESSIONE. Il diario citava il modello vecchio a
"Sharpe 0.77 / CAGR 5.5%": artefatto di annualizzazione: la serie GTAA grezza ha ~252 barre
/anno (soli giorni di borsa) e metrics() annualizza a 365 con years=n/365.25 -> Sharpe x1.20
e CAGR x1.45. Le righe per-capitale erano gia' su calendario 365, quindi era falsata solo la
riga di riferimento. Corretti diario, CLAUDE.md e r0725_capcurve.py.
LEZIONE: una serie su giorni di borsa non si passa a metrics() senza to_daily().
Test: 221 pass (+4 in tests/test_gtaa_sleeve.py: pavimento non proporzionale, dipendenza dal
capitale + soglia, senza-banda-e-distruttivo, piano che salta le gambe sotto banda).
Smoke: scripts/live/paper_combo.py gira invariato.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,3 +40,55 @@ def test_gtaa_weekend_zero_e_attivazione():
|
||||
def test_book_live_deribit_non_include_gtaa():
|
||||
names = [s.name for s in deribit_book_sleeves()]
|
||||
assert names == ["TP01_trend_1d", "SKH01_skyhook"] # esecuzione live invariata
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# COSTO IB REALE + BANDA (fix 2026-07-25, diario 2026-07-25-rendita-capcurve-prop.md §1)
|
||||
# ---------------------------------------------------------------------------
|
||||
from src.portfolio.gtaa import (GTAA_DEFAULT_CAPITAL, GTAA_MIN_CAPITAL, gtaa_is_deployable,
|
||||
gtaa_rebalance_plan, gtaa_returns, ib_commission)
|
||||
from src.portfolio.portfolio import metrics, to_daily
|
||||
|
||||
|
||||
def test_commissione_ib_ha_un_PAVIMENTO_non_e_proporzionale():
|
||||
"""Il difetto originale: modellare IB come fee proporzionale. Su un ordine piccolo la
|
||||
commissione e' il MINIMO FISSO ($0.35), cioe' una percentuale enorme; su uno grande e'
|
||||
per-azione. Se questo test cade, il modello e' tornato proporzionale."""
|
||||
c_small = ib_commission(20.0, 100.0) # ordine da $20
|
||||
c_big = ib_commission(20_000.0, 100.0) # ordine da $20.000
|
||||
assert c_small == pytest.approx(0.20) # cap 1% del controvalore morde sui micro-ordini
|
||||
assert c_small / 20.0 > 0.005 # >0.5% dell'ordine: il piccolo paga carissimo
|
||||
assert c_big / 20_000.0 < 0.001 # il grande paga pochi bps
|
||||
assert ib_commission(200.0, 100.0) == pytest.approx(0.35) # pavimento fisso
|
||||
|
||||
|
||||
def test_lo_sleeve_dipende_dal_CAPITALE_e_non_e_deployabile_sotto_soglia():
|
||||
"""Il modello vecchio era CIECO AL CAPITALE: dava lo stesso Sharpe a $600 e a $50k. Ora no.
|
||||
E sotto GTAA_MIN_CAPITAL lo sleeve si dichiara non deployabile."""
|
||||
assert not gtaa_is_deployable(600.0)
|
||||
assert not gtaa_is_deployable(GTAA_MIN_CAPITAL - 1)
|
||||
assert gtaa_is_deployable(GTAA_DEFAULT_CAPITAL)
|
||||
piccolo = metrics(to_daily(gtaa_returns(capital=600.0)))
|
||||
grande = metrics(to_daily(gtaa_returns(capital=50_000.0)))
|
||||
assert grande["cagr"] > piccolo["cagr"], "il pavimento fisso deve penalizzare il capitale piccolo"
|
||||
|
||||
|
||||
def test_senza_banda_a_basso_capitale_lo_sleeve_e_DISTRUTTIVO():
|
||||
"""La ragione per cui la banda esiste: a $600, ribilanciando in continuo (banda 0, ogni
|
||||
giorno), il pavimento di commissione rende lo sleeve NEGATIVO. E' il difetto trovato."""
|
||||
nudo = metrics(to_daily(gtaa_returns(capital=600.0, band_usd=0.0, every=1)))
|
||||
bandato = metrics(to_daily(gtaa_returns(capital=600.0)))
|
||||
assert nudo["cagr"] < 0.0, "senza banda a $600 lo sleeve deve perdere (pavimento IB)"
|
||||
assert bandato["cagr"] > 0.0, "con banda+cadenza a $600 deve tornare positivo"
|
||||
|
||||
|
||||
def test_piano_di_ribilanciamento_salta_le_gambe_sotto_banda():
|
||||
"""L'esecutore deve passare da qui: le gambe il cui nozionale non supera la banda NON si
|
||||
toccano (un ordine da $12 costa $0.35 = 2.9%)."""
|
||||
grande = gtaa_rebalance_plan({}, 10_000.0)
|
||||
assert grande["deployable"] and grande["orders"], "a $10k da flat si deve poter operare"
|
||||
assert all(o["notional"] >= 50.0 for o in grande["orders"].values())
|
||||
piccolo = gtaa_rebalance_plan({}, 300.0)
|
||||
assert not piccolo["deployable"]
|
||||
# a $300 su 6 gambe gli ordini sono ~$50 o meno -> quasi tutto sotto banda
|
||||
assert len(piccolo["orders"]) < len(grande["orders"])
|
||||
|
||||
Reference in New Issue
Block a user