Files
PythagorasGoal/docs/diary/2026-06-13-rebalance-conservation.md
T
Adriano Dal Pastro 432c1ad790 fix(ledger): ribilancio conserva l'equity (no doppio conteggio in-position)
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>
2026-06-13 10:09:36 +00:00

2.8 KiB
Raw Blame History

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:

  1. total_capital = Σ capital di tutti i worker
  2. alloc[sid] = peso × total_capital per OGNI sid
  3. ai worker in posizione NON applicava l'alloc (la posizione tiene il suo notional)
  4. 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.