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):
|
||||
|
||||
@@ -130,7 +130,12 @@ def rebalance_allocations(ledger: PortfolioLedger, workers: dict, weights: dict[
|
||||
il suo notional, come da approssimazione dichiarata): il nuovo capitale-base si applica
|
||||
alla prossima posizione, quando il worker è flat."""
|
||||
ledger.total_capital = sum(_worker_equity(w) for w in workers.values())
|
||||
alloc = ledger.allocate(weights)
|
||||
# i worker in posizione TRATTENGONO il loro capitale (deployato): vanno passati
|
||||
# come `reserved` ad allocate, cosi' i flat si dividono solo il RESTO e l'equity
|
||||
# totale e' conservata (fix doppio conteggio 2026-06-13, vedi ledger.allocate).
|
||||
reserved = {sid: _worker_equity(w) for sid, w in workers.items()
|
||||
if getattr(getattr(w, "worker", w), "in_position", False)}
|
||||
alloc = ledger.allocate(weights, reserved=reserved)
|
||||
for sid, w in workers.items():
|
||||
inner = getattr(w, "worker", w)
|
||||
if getattr(inner, "in_position", False):
|
||||
|
||||
Reference in New Issue
Block a user