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:
Adriano Dal Pastro
2026-07-25 17:25:06 +00:00
parent 3266f3a09a
commit 607cd94ecd
6 changed files with 248 additions and 42 deletions
+52
View File
@@ -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"])