Bug: i flat si dividevano l'INTERO total includendo il capitale dei worker in
posizione, che lo trattenevano in piu' -> equity gonfiata di Sigma(capital-alloc)
sugli in-pos. Caso MR02_BTC 15m seedato (181.19) e in posizione al ribilancio
00:01: +4.77 fantasma. allocate(weights, reserved={sid:cap}): gli in-pos
trattengono il loro, i flat si dividono total-Sigma(reserved) per peso
rinormalizzato -> Sigma alloc == total sempre. Parita' runner intatta
(5.8e-08). Test conservazione (ledger + runner). Suite verde.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2.8 KiB
2026-06-13 — Equity gonfiata dal ribilancio: +4.77 fantasma (causa + fix)
Sintomo
Report "dall'ultimo reset": equity 2020.53 → 2025.80 (+5.27), ma un solo trade chiuso (MR02_BTC 15m, reale +0.50). Il +4.77 residuo è comparso di colpo a 00:01:52 (primo poll dopo mezzanotte UTC = ribilancio giornaliero), senza alcuna chiusura dietro.
Causa — doppio conteggio del capitale degli in-position al ribilancio
rebalance_allocations faceva:
total_capital = Σ capital di tutti i workeralloc[sid] = peso × total_capitalper OGNI sid- ai worker in posizione NON applicava l'alloc (la posizione tiene il suo notional)
- ai flat:
capital = alloc[sid]
update_equity (ogni poll) = Σ capital. Dopo il ribilancio la somma diventa:
Σcapital = Σ(peso_i × total) [flat] + Σ(capital_j) [in-pos]
= total − Σalloc_j + Σcapital_j
= total + Σ(capital_j − alloc_j) ← inflazione
Cioè i flat si dividevano l'INTERO total (che includeva anche il capitale degli in-position), e gli in-position lo tenevano in più → doppio conteggio.
Al ribilancio delle 00:01, MR02_BTC 15m era in posizione col capitale seedato 181.19 (eredità del gemello 1h, INIT_LINEAGE di ieri) mentre la sua allocazione era ~176.42 → +4.77 di equity dal nulla. Lo scatenante è stato il seed (capitale ben sopra l'allocazione media) combinato con la posizione aperta al ribilancio; ma il bug era latente da sempre (ogni worker in-pos che devia dall'alloc gonfiava/sgonfiava l'equity al ribilancio successivo).
Fix — conservazione (ledger.allocate reserved)
allocate(weights, reserved={sid: capitale}): i worker in posizione TRATTENGONO il
loro capitale (deployato, non spostabile); i flat si dividono total − Σreserved
per peso rinormalizzato sui soli flat. Così Σalloc == total_capital sempre →
l'equity è conservata dal ribilancio (un ribilancio sposta capitale fra sleeve,
non crea valore). runner.rebalance_allocations calcola reserved dai worker
in_position e lo passa. Senza reserved (default) = comportamento storico
(corretto solo a worker tutti flat, es. allocazione iniziale).
Test: tests/portfolio/test_ledger.py::test_alloc_conserves_total_with_reserved.
Correzione dello stato (rimozione del +4.77 già accumulato)
Equity riportata a 2021.03 = baseline post-seed verificata (2020.53) + unico trade reale (MR02_BTC 15m +0.50). Capitali dei worker del pool scalati × (2021.03/2025.79) e peak del ledger resettato a 2021.03 (il 2025.80 non è mai stato un massimo legittimo → niente DD fantasma). L'orfano MR02_BTC 1h chiuso a parte (+0.85) resta sul worker ritirato, fuori dal pool.
Esito
Equity = soldi veri: dal reset il trading ha prodotto +0.50 (1 trade), non +5.27. Deploy con codice fixato (rebuild) così il prossimo ribilancio conserva.