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>
This commit is contained in:
+22
-3
@@ -37,12 +37,31 @@ class PortfolioLedger:
|
||||
self.alloc = s.get("alloc", {})
|
||||
self.last_rebalance = s.get("last_rebalance", "")
|
||||
|
||||
def allocate(self, weights: dict[str, float]) -> dict[str, float]:
|
||||
def allocate(self, weights: dict[str, float],
|
||||
reserved: dict[str, float] | None = None) -> dict[str, float]:
|
||||
"""alloc per sid = peso × total_capital. `reserved` {sid: capitale trattenuto}
|
||||
per i worker con posizione APERTA: quel capitale e' deployato, non
|
||||
ridistribuibile -> i flat si dividono (total − Σreserved) per peso
|
||||
RINORMALIZZATO sui soli flat. Cosi' Σalloc == total_capital ed equity e'
|
||||
CONSERVATA dal ribilancio. Senza reserved (default): comportamento storico
|
||||
(alloc = peso×total per tutti), corretto solo se tutti i worker sono flat
|
||||
(allocazione iniziale). Fix 2026-06-13: prima i flat si dividevano l'INTERO
|
||||
total includendo il capitale degli in-position -> doppio conteggio, equity
|
||||
gonfiata di Σ(capital_in_pos − alloc_in_pos) (caso MR02_BTC 15m seedato e in
|
||||
posizione al ribilancio: +4.77). Vedi docs/diary/2026-06-13-rebalance-conservation.md."""
|
||||
self.weights = dict(weights)
|
||||
self.alloc = {sid: round(self.total_capital * w, 6) for sid, w in weights.items()}
|
||||
reserved = reserved or {}
|
||||
distributable = self.total_capital - sum(reserved.values())
|
||||
flat = {sid: w for sid, w in weights.items() if sid not in reserved}
|
||||
wsum = sum(flat.values())
|
||||
self.alloc = {sid: round(cap, 6) for sid, cap in reserved.items()}
|
||||
for sid, w in flat.items():
|
||||
share = (w / wsum) if wsum > 0 else (1.0 / len(flat) if flat else 0.0)
|
||||
self.alloc[sid] = round(distributable * share, 6)
|
||||
self.last_rebalance = datetime.now(timezone.utc).isoformat()
|
||||
self._append(self.events_path, {"event": "rebalance", "weights": self.weights,
|
||||
"total_capital": self.total_capital})
|
||||
"total_capital": self.total_capital,
|
||||
"reserved": sorted(reserved)})
|
||||
return self.alloc
|
||||
|
||||
def update_equity(self, sleeve_equity: dict[str, float], pnl_day: float = 0.0):
|
||||
|
||||
Reference in New Issue
Block a user