432c1ad790
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>
57 lines
2.8 KiB
Markdown
57 lines
2.8 KiB
Markdown
# 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.
|