Files
Adriano Dal Pastro 14522262e6 chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera
libreria "validata OOS" era artefatto di feed contaminato (print fantasma del
feed Cerbero TESTNET + storico Binance/USDT).

- Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e
  CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia
  (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample
  (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE
  50-82% barre flat; XRP/BNB non certificabili).
- Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni
  portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST
  con segnale residuo, da ri-validare in isolamento.
- Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio,
  runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/
  portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/
  (preservati, non cancellati). Diario consolidato in un unico documento.
- Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal +
  src/backtest/engine + load_data; tool dati certificati (rebuild_history,
  certify_feed, audit_feed, multi_source_check).
- Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico
  (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:20:59 +00:00

45 KiB
Raw Permalink Blame History

Cartella portfolios/ — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Introdurre portafogli come oggetti di prima classe (capitale-pool condiviso) capaci sia di backtest/report sia di gestione live (sizing, ribilancio, ledger PnL), usando il data layer Cerbero v2.

Architecture: Una classe Portfolio (definizione: sleeve + schema pesi) con due facce sulla stessa definizione: backtest() riusa l'unico builder di equity-per-sleeve esistente (parità per costruzione col report); il live (PortfolioRunner) costruisce i worker esistenti come esecutori, alloca peso×capitale, ribilancia giornalmente e aggrega nel PortfolioLedger. Codice nuovo in src/portfolio/, definizioni concrete in scripts/portfolios/, config live in portfolios.yml.

Tech Stack: Python 3.11, uv, pandas/numpy, scipy (clustering già usato), pytest, requests (Cerbero MCP v2). Riusa scripts/analysis/{combine_portfolio,report_families,pairs_research,tsmom_research,shape_ml_validate}.py e src/live/{multi_runner,strategy_worker,pairs_worker,cerbero_client}.py.

Spec di riferimento: docs/superpowers/specs/2026-05-29-portfolios-design.md


File structure

File Responsabilità
src/portfolio/__init__.py package marker
src/portfolio/sleeves.py all_sleeve_equities() — unico builder di equity-per-sleeve (delega a report_families.build_everything), sleeve_returns_df()
src/portfolio/weighting.py family_of, equal, manual, cap, inverse_vol, cluster_rp, weight_vector
src/portfolio/base.py SleeveSpec, PortfolioResult, Portfolio (.backtest(), .weight_vector())
src/portfolio/ledger.py PortfolioLedger (capitale, alloc, equity, PnL, peak/DD, persistenza/resume)
src/portfolio/runner.py PortfolioRunner (live: data v2, build worker, sizing, ribilancio, aggregazione)
src/live/cerbero_client.py modifica: aggiunge metodi v2 get_historical_v2, get_instruments, get_ticker_batch
scripts/portfolios/PORT0{1..6}_*.py definizioni concrete + run() report
portfolios.yml config live (portafoglio attivo, capitale, pesi, cap, leva, cadenza)
tests/portfolio/test_*.py unit + parità + smoke

Sleeve id canonici (devono combaciare con le chiavi di build_everything per la parità): MR01_BTC MR02_BTC MR07_BTC MR01_ETH MR02_ETH MR07_ETH · DIP01_BTC TR01_basket ROT02_rot · PR_ETHBTC PR_LTCETH PR_ADAETH PR_BTCLTC PR_ETHSOL · TSM01 · SH_BTC SH_ETH.


Task 1: Metodi Cerbero v2 nel client

Files:

  • Modify: src/live/cerbero_client.py

  • Test: tests/portfolio/test_cerbero_v2.py (richiede rete; marcato network)

  • Step 1: Crea il package di test

mkdir -p tests/portfolio && touch tests/portfolio/__init__.py
  • Step 2: Aggiungi i metodi v2 al client

In src/live/cerbero_client.py, dentro la classe CerberoClient, dopo get_historical (riga ~50) aggiungi:

    def get_historical_v2(self, instrument: str, start_date: str, end_date: str,
                          interval: str = "1h", exchange: str = "deribit") -> list[dict]:
        """Endpoint unificato v2: /mcp/tools/get_historical (exchange deribit|hyperliquid).
        Stesso shape candele del legacy: [{timestamp(ms), open, high, low, close, volume}]."""
        data = self._post("/mcp/tools/get_historical", {
            "exchange": exchange, "instrument": instrument,
            "interval": interval, "start_date": start_date, "end_date": end_date,
        })
        return data.get("candles", [])

    def get_instruments(self, currency: str, kind: str = "future",
                        exchange: str = "deribit", limit: int = 100) -> list[dict]:
        """Enumera gli strumenti reali (v2). Usato per risolvere il naming senza hardcoding."""
        data = self._post("/mcp/tools/get_instruments", {
            "exchange": exchange, "currency": currency, "kind": kind, "limit": limit,
        })
        return data.get("instruments", data if isinstance(data, list) else [])

    def get_ticker_batch(self, instruments: list[str]) -> dict:
        """Prezzi correnti di N strumenti in una sola chiamata (v2, Deribit)."""
        return self._post("/mcp-deribit/tools/get_ticker_batch", {"instruments": instruments})
  • Step 3: Scrivi lo smoke test di rete

tests/portfolio/test_cerbero_v2.py:

import pytest
from src.live.cerbero_client import CerberoClient


@pytest.mark.network
def test_get_historical_v2_shape():
    cli = CerberoClient()
    candles = cli.get_historical_v2("BTC-PERPETUAL", "2026-05-25", "2026-05-27", "1h")
    assert len(candles) > 0
    c0 = candles[0]
    assert {"timestamp", "open", "high", "low", "close", "volume"} <= set(c0)


@pytest.mark.network
def test_get_instruments_returns_list():
    cli = CerberoClient()
    inst = cli.get_instruments("ETH", "future")
    assert isinstance(inst, list) and len(inst) > 0
  • Step 4: Esegui (con rete)

Run: uv run pytest tests/portfolio/test_cerbero_v2.py -v -m network Expected: 2 passed (se la rete/token è disponibile). Senza rete: uv run pytest -m "not network" li salta.

  • Step 5: Registra il marker network

In pyproject.toml, sotto [tool.pytest.ini_options] (riga ~27) aggiungi:

markers = ["network: test che richiede Cerbero MCP (rete+token)"]
  • Step 6: Commit
git add src/live/cerbero_client.py tests/portfolio/ pyproject.toml
git commit -m "feat(portfolio): metodi Cerbero v2 (get_historical_v2, get_instruments, get_ticker_batch)"

Task 2: Schemi di peso (weighting.py)

Files:

  • Create: src/portfolio/__init__.py, src/portfolio/weighting.py

  • Test: tests/portfolio/test_weighting.py

  • Step 1: Crea il package

mkdir -p src/portfolio && touch src/portfolio/__init__.py
  • Step 2: Scrivi i test (falliscono)

tests/portfolio/test_weighting.py:

import numpy as np
import pandas as pd
import pytest
from src.portfolio import weighting as W


def test_family_of():
    assert W.family_of("PR_ETHBTC") == "PAIRS"
    assert W.family_of("SH_BTC") == "SHAPE"
    assert W.family_of("TSM01") == "TSM"
    assert W.family_of("MR01_BTC") == "FADE"
    assert W.family_of("DIP01_BTC") == "HONEST"


def test_equal_sums_to_one():
    w = W.equal(["a", "b", "c", "d"])
    assert pytest.approx(sum(w.values())) == 1.0
    assert all(abs(v - 0.25) < 1e-9 for v in w.values())


def test_manual_normalizes():
    w = W.manual(["a", "b"], {"a": 3, "b": 1})
    assert pytest.approx(w["a"]) == 0.75 and pytest.approx(w["b"]) == 0.25


def test_cap_limits_family_and_redistributes():
    ids = ["PR_ETHBTC", "PR_LTCETH", "MR01_BTC", "MR02_BTC"]
    w = W.cap(ids, caps={"PAIRS": 0.30})
    pairs_w = w["PR_ETHBTC"] + w["PR_LTCETH"]
    assert pytest.approx(pairs_w, abs=1e-9) == 0.30          # cap rispettato
    assert pytest.approx(sum(w.values())) == 1.0             # resto ridistribuito
    assert w["MR01_BTC"] > 0.25                              # non-pairs sovrappesati


def test_inverse_vol_prefers_low_vol():
    idx = pd.date_range("2024-01-01", periods=100, freq="D", tz="UTC")
    rng = np.random.default_rng(0)
    df = pd.DataFrame({"lo": rng.normal(0, 0.01, 100), "hi": rng.normal(0, 0.05, 100)}, index=idx)
    w = W.inverse_vol(["lo", "hi"], df, lookback=90)
    assert w["lo"] > w["hi"]
    assert pytest.approx(sum(w.values())) == 1.0
  • Step 3: Run — verifica fallimento

Run: uv run pytest tests/portfolio/test_weighting.py -v Expected: FAIL (ModuleNotFoundError: weighting).

  • Step 4: Implementa weighting.py
"""Schemi di peso per i portafogli. Ogni funzione ritorna {sleeve_id: peso} con somma 1."""
from __future__ import annotations

import numpy as np
import pandas as pd

_PREFIX = [("PR_", "PAIRS"), ("SH_", "SHAPE"), ("TSM", "TSM"), ("MR", "FADE")]


def family_of(sleeve_id: str) -> str:
    for pre, fam in _PREFIX:
        if sleeve_id.startswith(pre):
            return fam
    return "HONEST"


def _normalize(w: dict[str, float]) -> dict[str, float]:
    tot = sum(w.values())
    return {k: (v / tot if tot > 0 else 0.0) for k, v in w.items()}


def equal(ids: list[str]) -> dict[str, float]:
    n = len(ids)
    return {i: 1.0 / n for i in ids} if n else {}


def manual(ids: list[str], weights: dict[str, float]) -> dict[str, float]:
    return _normalize({i: float(weights.get(i, 0.0)) for i in ids})


def cap(ids: list[str], caps: dict[str, float]) -> dict[str, float]:
    """Equal-weight con tetto al peso AGGREGATO di una famiglia; l'eccesso ridistribuito
    pro-quota alle famiglie non cappate (iterativo finché tutti i cap sono rispettati)."""
    w = equal(ids)
    fam = {i: family_of(i) for i in ids}
    for _ in range(10):
        over = {}
        for f, lim in caps.items():
            members = [i for i in ids if fam[i] == f]
            cur = sum(w[i] for i in members)
            if cur > lim + 1e-12 and members:
                over[f] = (members, lim, cur)
        if not over:
            break
        free_ids = [i for i in ids if fam[i] not in caps]
        freed = 0.0
        for f, (members, lim, cur) in over.items():
            scale = lim / cur
            for i in members:
                freed += w[i] * (1 - scale)
                w[i] *= scale
        if free_ids and freed > 0:
            add = freed / len(free_ids)
            for i in free_ids:
                w[i] += add
        else:
            break
    return _normalize(w)


def inverse_vol(ids: list[str], returns_df: pd.DataFrame, lookback: int) -> dict[str, float]:
    sub = returns_df[ids].iloc[-lookback:]
    vol = sub.std()
    inv = {i: (1.0 / vol[i] if vol[i] and vol[i] > 0 else 0.0) for i in ids}
    return _normalize(inv)


def cluster_rp(ids: list[str], clusters: dict[str, str],
               returns_df: pd.DataFrame, lookback: int) -> dict[str, float]:
    """Equal fra i cluster naturali, poi inverse-vol dentro ogni cluster."""
    groups: dict[str, list[str]] = {}
    for i in ids:
        groups.setdefault(clusters.get(i, i), []).append(i)
    per = 1.0 / len(groups) if groups else 0.0
    w: dict[str, float] = {}
    for members in groups.values():
        iv = inverse_vol(members, returns_df, lookback)
        for i in members:
            w[i] = per * iv[i]
    return _normalize(w)


def weight_vector(scheme: str, ids: list[str], returns_df: pd.DataFrame | None = None,
                  *, weights: dict | None = None, caps: dict | None = None,
                  clusters: dict | None = None, lookback: int = 90) -> dict[str, float]:
    if scheme == "equal":
        return equal(ids)
    if scheme == "manual":
        return manual(ids, weights or {})
    if scheme == "cap":
        return cap(ids, caps or {})
    if scheme == "inverse_vol":
        return inverse_vol(ids, returns_df, lookback)
    if scheme == "cluster_rp":
        return cluster_rp(ids, clusters or {}, returns_df, lookback)
    raise ValueError(f"schema peso sconosciuto: {scheme}")
  • Step 5: Run — verifica passaggio

Run: uv run pytest tests/portfolio/test_weighting.py -v Expected: 5 passed.

  • Step 6: Commit
git add src/portfolio/__init__.py src/portfolio/weighting.py tests/portfolio/test_weighting.py
git commit -m "feat(portfolio): schemi di peso (equal/manual/cap/inverse_vol/cluster_rp)"

Task 3: Builder unificato delle equity-per-sleeve (sleeves.py)

Files:

  • Create: src/portfolio/sleeves.py

  • Test: tests/portfolio/test_sleeves.py

  • Step 1: Scrivi il test (fallisce)

tests/portfolio/test_sleeves.py:

import pandas as pd
from src.portfolio import sleeves as S

ALL_IDS = {"MR01_BTC", "MR02_BTC", "MR07_BTC", "MR01_ETH", "MR02_ETH", "MR07_ETH",
           "DIP01_BTC", "TR01_basket", "ROT02_rot",
           "PR_ETHBTC", "PR_LTCETH", "PR_ADAETH", "PR_BTCLTC", "PR_ETHSOL",
           "TSM01", "SH_BTC", "SH_ETH"}


def test_all_sleeve_equities_keys_and_index():
    eq = S.all_sleeve_equities()
    assert ALL_IDS <= set(eq)
    s = eq["MR01_BTC"]
    assert isinstance(s, pd.Series) and len(s) > 100
    assert str(s.index.tz) == "UTC"


def test_returns_df_aligned():
    df = S.sleeve_returns_df(["MR01_BTC", "PR_ETHBTC", "SH_BTC"])
    assert list(df.columns) == ["MR01_BTC", "PR_ETHBTC", "SH_BTC"]
    assert df.isna().sum().sum() == 0
  • Step 2: Run — verifica fallimento

Run: uv run pytest tests/portfolio/test_sleeves.py -v Expected: FAIL (ModuleNotFoundError: sleeves).

  • Step 3: Implementa sleeves.py
"""Unico builder delle equity GIORNALIERE per sleeve (fonte di verità del backtest).

Delega a scripts/analysis/report_families.build_everything (che a sua volta usa
combine_portfolio + pairs_research + tsmom_research + shape_ml_validate), così le
metriche del Portfolio coincidono per costruzione con report_families."""
from __future__ import annotations

import pandas as pd

_CACHE: dict[str, pd.Series] | None = None


def all_sleeve_equities() -> dict[str, pd.Series]:
    """{sleeve_id: equity giornaliera normalizzata su IDX comune}. Cache di processo."""
    global _CACHE
    if _CACHE is None:
        from scripts.analysis.report_families import build_everything
        S, pairs, tsm, shape = build_everything()
        _CACHE = {**S, **pairs, **tsm, **shape}
    return _CACHE


def sleeve_returns_df(ids: list[str]) -> pd.DataFrame:
    """Rendimenti giornalieri allineati per gli sleeve richiesti."""
    eq = all_sleeve_equities()
    return pd.DataFrame({i: eq[i].pct_change().fillna(0.0) for i in ids})
  • Step 4: Run — verifica passaggio

Run: uv run pytest tests/portfolio/test_sleeves.py -v Expected: 2 passed (richiede i parquet in data/raw/; ~2-3 min per la build).

  • Step 5: Commit
git add src/portfolio/sleeves.py tests/portfolio/test_sleeves.py
git commit -m "feat(portfolio): builder unificato equity-per-sleeve (parità con report_families)"

Task 4: SleeveSpec, Portfolio, PortfolioResult + backtest (base.py)

Files:

  • Create: src/portfolio/base.py

  • Test: tests/portfolio/test_backtest_parity.py

  • Step 1: Scrivi il test di parità (fallisce)

tests/portfolio/test_backtest_parity.py:

import pytest
from src.portfolio.base import Portfolio, SleeveSpec
from scripts.analysis.report_families import build_everything
from scripts.analysis.combine_portfolio import port_returns, metrics, SPLIT


def _master9_specs():
    fade = [SleeveSpec(kind="single", name=f"{c}", sid=f"{c}_{a}", asset=a, cluster=f"{a}-rev")
            for a in ("BTC", "ETH") for c in ("MR01", "MR02", "MR07")]
    honest = [SleeveSpec(kind="single", name="DIP01", sid="DIP01_BTC", asset="BTC", cluster="BTC-rev"),
              SleeveSpec(kind="single", name="TR01", sid="TR01_basket", cluster="trend"),
              SleeveSpec(kind="single", name="ROT02", sid="ROT02_rot", cluster="rotation")]
    return fade + honest


def test_master9_backtest_matches_report():
    p = Portfolio(code="PORT03", label="Master", sleeves=_master9_specs(), weighting="equal")
    res = p.backtest()
    # riferimento: equal-weight degli stessi 9 sleeve via la macchina del report
    S, _, _, _ = build_everything()
    dr_ref = port_returns(S)
    ref_full, ref_oos = metrics(dr_ref), metrics(dr_ref, lo=SPLIT)
    assert res.full["sharpe"] == pytest.approx(ref_full["sharpe"], abs=1e-6)
    assert res.full["dd"] == pytest.approx(ref_full["dd"], abs=1e-6)
    assert res.oos["sharpe"] == pytest.approx(ref_oos["sharpe"], abs=1e-6)
  • Step 2: Run — verifica fallimento

Run: uv run pytest tests/portfolio/test_backtest_parity.py -v Expected: FAIL (ModuleNotFoundError: base).

  • Step 3: Implementa base.py
"""Portfolio: definizione (sleeve + schema pesi) con faccia di backtest.
La faccia live è in runner.py."""
from __future__ import annotations

from dataclasses import dataclass, field

import pandas as pd

from src.portfolio import weighting as W
from src.portfolio.sleeves import all_sleeve_equities, sleeve_returns_df
from scripts.analysis.combine_portfolio import port_returns, metrics, yearly_returns, SPLIT


@dataclass
class SleeveSpec:
    kind: str                       # "single" | "pairs" | "ml"
    name: str                       # codice strategia per il live (MR01/DIP01/PR01.../SH01)
    sid: str                        # id canonico (= chiave in all_sleeve_equities)
    asset: str | None = None
    a: str | None = None
    b: str | None = None
    tf: str = "1h"
    params: dict = field(default_factory=dict)
    cluster: str = ""


@dataclass
class PortfolioResult:
    code: str
    weights: dict
    full: dict                      # ret/cagr/dd/sharpe (FULL)
    oos: dict                       # ret/cagr/dd/sharpe (OOS)
    yearly: dict                    # anno -> ret%
    risk: dict                      # sid -> % contributo al rischio (equal informativo)


@dataclass
class Portfolio:
    code: str
    label: str
    sleeves: list[SleeveSpec]
    weighting: str = "equal"
    weights: dict | None = None
    caps: dict | None = None
    total_capital: float = 1000.0
    leverage: float = 3.0
    rebalance: str = "1D"
    vol_lookback: int = 90

    @property
    def sleeve_ids(self) -> list[str]:
        return [s.sid for s in self.sleeves]

    @property
    def clusters(self) -> dict[str, str]:
        return {s.sid: (s.cluster or s.sid) for s in self.sleeves}

    def weight_vector(self, returns_df: pd.DataFrame | None = None) -> dict[str, float]:
        return W.weight_vector(
            self.weighting, self.sleeve_ids, returns_df,
            weights=self.weights, caps=self.caps,
            clusters=self.clusters, lookback=self.vol_lookback,
        )

    def backtest(self) -> PortfolioResult:
        eq = all_sleeve_equities()
        members = {sid: eq[sid] for sid in self.sleeve_ids}
        dr = sleeve_returns_df(self.sleeve_ids)
        w = self.weight_vector(dr)
        port_dr = port_returns(members, w)
        full, oos = metrics(port_dr), metrics(port_dr, lo=SPLIT)
        # contributo al rischio (equal-weight, informativo)
        cov = dr.cov().values
        import numpy as np
        we = np.ones(len(self.sleeve_ids)) / len(self.sleeve_ids)
        pv = float(we @ cov @ we)
        rc = we * (cov @ we)
        risk = {sid: float(rc[k] / pv * 100) if pv > 0 else 0.0
                for k, sid in enumerate(self.sleeve_ids)}
        return PortfolioResult(self.code, w, full, oos, yearly_returns(port_dr), risk)
  • Step 4: Run — verifica passaggio

Run: uv run pytest tests/portfolio/test_backtest_parity.py -v Expected: 1 passed.

  • Step 5: Commit
git add src/portfolio/base.py tests/portfolio/test_backtest_parity.py
git commit -m "feat(portfolio): SleeveSpec/Portfolio/backtest con parità verso report_families"

Task 5: Definizioni concrete scripts/portfolios/PORT01..06

Files:

  • Create: scripts/portfolios/__init__.py, scripts/portfolios/_defs.py, PORT01..06_*.py

  • Test: tests/portfolio/test_definitions.py

  • Step 1: Test (fallisce)

tests/portfolio/test_definitions.py:

from scripts.portfolios._defs import PORTFOLIOS


def test_six_portfolios_defined():
    assert set(PORTFOLIOS) == {"PORT01", "PORT02", "PORT03", "PORT04", "PORT05", "PORT06"}


def test_port06_is_master_shape_cap():
    p = PORTFOLIOS["PORT06"]
    sids = set(p.sleeve_ids)
    assert {"SH_BTC", "SH_ETH", "TSM01", "PR_ETHBTC"} <= sids
    assert len(sids) == 17
    assert p.weighting == "cap" and p.caps == {"PAIRS": 0.33}


def test_default_leverage_sober():
    assert PORTFOLIOS["PORT06"].leverage == 2.0
  • Step 2: Run — verifica fallimento

Run: uv run pytest tests/portfolio/test_definitions.py -v Expected: FAIL (ModuleNotFoundError: _defs).

  • Step 3: Crea il package e le definizioni condivise
touch scripts/portfolios/__init__.py

scripts/portfolios/_defs.py:

"""Definizioni canoniche dei portafogli (tutti i tipi visti finora)."""
from __future__ import annotations

import sys
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))

from src.portfolio.base import Portfolio, SleeveSpec  # noqa: E402

FADE = [SleeveSpec(kind="single", name=c, sid=f"{c}_{a}", asset=a, cluster=f"{a}-rev")
        for a in ("BTC", "ETH") for c in ("MR01", "MR02", "MR07")]
HONEST = [
    SleeveSpec(kind="single", name="DIP01", sid="DIP01_BTC", asset="BTC", cluster="BTC-rev"),
    SleeveSpec(kind="single", name="TR01", sid="TR01_basket", cluster="trend"),
    SleeveSpec(kind="single", name="ROT02", sid="ROT02_rot", cluster="rotation"),
]
PAIRS = [
    SleeveSpec(kind="pairs", name="PR01", sid="PR_ETHBTC", a="ETH", b="BTC", cluster="ETH-rev"),
    SleeveSpec(kind="pairs", name="PR01", sid="PR_LTCETH", a="LTC", b="ETH", cluster="ETH-rev"),
    SleeveSpec(kind="pairs", name="PR01", sid="PR_ADAETH", a="ADA", b="ETH", cluster="ETH-rev"),
    SleeveSpec(kind="pairs", name="PR01", sid="PR_BTCLTC", a="BTC", b="LTC", cluster="BTC-rev"),
    SleeveSpec(kind="pairs", name="PR01", sid="PR_ETHSOL", a="ETH", b="SOL", cluster="ETH-rev"),
]
TSM = [SleeveSpec(kind="single", name="TSM01", sid="TSM01", cluster="trend")]
SHAPE = [SleeveSpec(kind="ml", name="SH01", sid=f"SH_{a}", asset=a, cluster="shape")
         for a in ("BTC", "ETH")]

PORTFOLIOS = {
    "PORT01": Portfolio("PORT01", "Honest", HONEST, weighting="equal"),
    "PORT02": Portfolio("PORT02", "Fade master", FADE, weighting="equal"),
    "PORT03": Portfolio("PORT03", "Master", FADE + HONEST, weighting="equal"),
    "PORT04": Portfolio("PORT04", "Master + pairs", FADE + HONEST + PAIRS,
                        weighting="cap", caps={"PAIRS": 0.33}),
    "PORT05": Portfolio("PORT05", "Master esteso", FADE + HONEST + PAIRS + TSM,
                        weighting="cap", caps={"PAIRS": 0.33}),
    "PORT06": Portfolio("PORT06", "Master + shape", FADE + HONEST + PAIRS + TSM + SHAPE,
                        weighting="cap", caps={"PAIRS": 0.33}, leverage=2.0),
}
  • Step 4: Crea i 6 script con run() (report)

Per ciascun code in PORT01..PORT06, crea scripts/portfolios/<code>_<slug>.py. Esempio scripts/portfolios/PORT06_master_shape.py:

"""PORT06 — Master + shape (default). Report backtest del portafoglio."""
import sys
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))

from scripts.portfolios._defs import PORTFOLIOS  # noqa: E402

CODE = "PORT06"


def run():
    p = PORTFOLIOS[CODE]
    r = p.backtest()
    print("=" * 80)
    print(f"  {p.code}{p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x")
    print("=" * 80)
    print(f"  FULL ret {r.full['ret']:+.0f}%  CAGR {r.full['cagr']:.0f}%  "
          f"DD {r.full['dd']:.1f}%  Sharpe {r.full['sharpe']:.2f}")
    print(f"  OOS  ret {r.oos['ret']:+.0f}%  DD {r.oos['dd']:.1f}%  Sharpe {r.oos['sharpe']:.2f}")
    print("  per anno:", {y: round(v) for y, v in sorted(r.yearly.items())})
    print("  rischio % per sleeve:", {k: round(v, 1) for k, v in
          sorted(r.risk.items(), key=lambda x: -x[1])})


if __name__ == "__main__":
    run()

Gli altri 5 file sono identici cambiando solo CODE (PORT01..PORT05) e il nome file: PORT01_honest.py, PORT02_fade.py, PORT03_master.py, PORT04_master_pairs.py, PORT05_master_esteso.py.

  • Step 5: Run test + uno smoke report

Run: uv run pytest tests/portfolio/test_definitions.py -v Expected: 3 passed. Run: uv run python scripts/portfolios/PORT06_master_shape.py Expected: stampa FULL/OOS/per-anno coerenti col report (Sharpe FULL ~6, OOS più alto).

  • Step 6: Commit
git add scripts/portfolios/ tests/portfolio/test_definitions.py
git commit -m "feat(portfolio): definizioni PORT01-06 + report run() (default PORT06)"

Task 6: PortfolioLedger (stato/PnL/persistenza)

Files:

  • Create: src/portfolio/ledger.py

  • Test: tests/portfolio/test_ledger.py

  • Step 1: Test (fallisce)

tests/portfolio/test_ledger.py:

from pathlib import Path
from src.portfolio.ledger import PortfolioLedger


def test_alloc_split_by_weights(tmp_path):
    L = PortfolioLedger("PORTX", total_capital=1000.0, data_dir=tmp_path)
    alloc = L.allocate({"a": 0.6, "b": 0.4})
    assert alloc == {"a": 600.0, "b": 400.0}


def test_update_tracks_equity_and_dd(tmp_path):
    L = PortfolioLedger("PORTX", total_capital=1000.0, data_dir=tmp_path)
    L.update_equity({"a": 700.0, "b": 500.0})    # equity 1200
    assert L.equity == 1200.0 and L.peak == 1200.0 and L.max_dd == 0.0
    L.update_equity({"a": 500.0, "b": 400.0})    # equity 900 -> dd 25%
    assert L.equity == 900.0
    assert abs(L.max_dd - 25.0) < 1e-9


def test_persist_and_resume(tmp_path):
    L = PortfolioLedger("PORTX", total_capital=1000.0, data_dir=tmp_path)
    L.update_equity({"a": 1100.0})
    L.save()
    L2 = PortfolioLedger("PORTX", total_capital=1000.0, data_dir=tmp_path)
    assert L2.equity == 1100.0 and L2.peak == 1100.0
    assert (tmp_path / "PORTX" / "equity.jsonl").exists()
  • Step 2: Run — verifica fallimento

Run: uv run pytest tests/portfolio/test_ledger.py -v Expected: FAIL (ModuleNotFoundError: ledger).

  • Step 3: Implementa ledger.py
"""Ledger aggregato del portafoglio: capitale, allocazioni, equity, PnL, peak/DD, persistenza."""
from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path


class PortfolioLedger:
    def __init__(self, code: str, total_capital: float = 1000.0,
                 data_dir: Path = Path("data/portfolios")):
        self.code = code
        self.initial_capital = total_capital
        self.total_capital = total_capital
        self.work_dir = Path(data_dir) / code
        self.work_dir.mkdir(parents=True, exist_ok=True)
        self.status_path = self.work_dir / "status.json"
        self.equity_path = self.work_dir / "equity.jsonl"
        self.events_path = self.work_dir / "events.jsonl"
        self.equity = total_capital
        self.peak = total_capital
        self.max_dd = 0.0
        self.weights: dict[str, float] = {}
        self.alloc: dict[str, float] = {}
        self.last_rebalance = ""
        self._load()

    def _load(self):
        if not self.status_path.exists():
            return
        s = json.loads(self.status_path.read_text())
        self.total_capital = s.get("total_capital", self.total_capital)
        self.equity = s.get("equity", self.equity)
        self.peak = s.get("peak", self.peak)
        self.max_dd = s.get("max_dd", self.max_dd)
        self.weights = s.get("weights", {})
        self.alloc = s.get("alloc", {})
        self.last_rebalance = s.get("last_rebalance", "")

    def allocate(self, weights: dict[str, float]) -> dict[str, float]:
        self.weights = dict(weights)
        self.alloc = {sid: round(self.total_capital * w, 6) for sid, w in weights.items()}
        self.last_rebalance = datetime.now(timezone.utc).isoformat()
        self._append(self.events_path, {"event": "rebalance", "weights": self.weights,
                                        "total_capital": self.total_capital})
        return self.alloc

    def update_equity(self, sleeve_equity: dict[str, float], pnl_day: float = 0.0):
        self.equity = float(sum(sleeve_equity.values()))
        if self.equity > self.peak:
            self.peak = self.equity
        dd = (self.peak - self.equity) / self.peak * 100 if self.peak > 0 else 0.0
        self.max_dd = max(self.max_dd, dd)
        self._append(self.equity_path, {
            "ts": datetime.now(timezone.utc).isoformat(),
            "equity": round(self.equity, 2), "dd": round(dd, 3),
            "pnl_day": round(pnl_day, 2),
            "pnl_total": round(self.equity - self.initial_capital, 2),
        })

    def save(self):
        self.status_path.write_text(json.dumps({
            "code": self.code, "total_capital": round(self.total_capital, 2),
            "equity": round(self.equity, 2), "peak": round(self.peak, 2),
            "max_dd": round(self.max_dd, 3), "weights": self.weights,
            "alloc": self.alloc, "last_rebalance": self.last_rebalance,
            "ts": datetime.now(timezone.utc).isoformat(),
        }, indent=2))

    @staticmethod
    def _append(path: Path, row: dict):
        with open(path, "a") as f:
            f.write(json.dumps(row) + "\n")
  • Step 4: Run — verifica passaggio

Run: uv run pytest tests/portfolio/test_ledger.py -v Expected: 3 passed.

  • Step 5: Commit
git add src/portfolio/ledger.py tests/portfolio/test_ledger.py
git commit -m "feat(portfolio): PortfolioLedger (alloc, equity/DD, persistenza+resume)"

Task 7: portfolios.yml + loader della config live

Files:

  • Create: portfolios.yml

  • Modify: src/portfolio/base.py (aggiunge Portfolio.from_active_config)

  • Test: tests/portfolio/test_config.py

  • Step 1: Crea portfolios.yml

# Config LIVE del paper trader a portafoglio. Seleziona UN portafoglio attivo
# (definito in scripts/portfolios/_defs.py) e ne fa l'override dei parametri operativi.
active: PORT06            # default raccomandato: master + shape
overrides:
  total_capital: 1000
  weighting: cap          # equal | cap | inverse_vol | cluster_rp | manual
  caps: {PAIRS: 0.33}
  leverage: 2             # sobrio per il live reale
  rebalance: 1D
  poll_seconds: 60
  • Step 2: Test (fallisce)

tests/portfolio/test_config.py:

from src.portfolio.base import load_active_portfolio


def test_load_active_applies_overrides(tmp_path):
    cfg = tmp_path / "portfolios.yml"
    cfg.write_text("active: PORT06\noverrides:\n  leverage: 2\n  total_capital: 500\n")
    p = load_active_portfolio(cfg)
    assert p.code == "PORT06"
    assert p.leverage == 2.0
    assert p.total_capital == 500
  • Step 3: Run — verifica fallimento

Run: uv run pytest tests/portfolio/test_config.py -v Expected: FAIL (ImportError: load_active_portfolio).

  • Step 4: Implementa il loader in base.py

Aggiungi in fondo a src/portfolio/base.py:

def load_active_portfolio(config_path) -> "Portfolio":
    """Carica il portafoglio attivo da portfolios.yml applicando gli override."""
    import yaml
    from pathlib import Path
    from scripts.portfolios._defs import PORTFOLIOS

    cfg = yaml.safe_load(Path(config_path).read_text())
    p = PORTFOLIOS[cfg["active"]]
    ov = cfg.get("overrides", {})
    for k in ("total_capital", "weighting", "caps", "leverage", "rebalance", "vol_lookback"):
        if k in ov and ov[k] is not None:
            setattr(p, k, ov[k])
    return p
  • Step 5: Run — verifica passaggio

Run: uv run pytest tests/portfolio/test_config.py -v Expected: 1 passed.

  • Step 6: Commit
git add portfolios.yml src/portfolio/base.py tests/portfolio/test_config.py
git commit -m "feat(portfolio): portfolios.yml + load_active_portfolio (override operativi)"

Task 8: PortfolioRunner — costruzione worker + sizing pool

Files:

  • Create: src/portfolio/runner.py

  • Test: tests/portfolio/test_runner_build.py

  • Step 1: Test (fallisce)

tests/portfolio/test_runner_build.py:

from src.portfolio.runner import build_worker_for
from src.portfolio.base import SleeveSpec
from src.live.strategy_worker import StrategyWorker
from src.live.pairs_worker import PairsWorker


def test_build_single_worker_capital_from_alloc(tmp_path):
    spec = SleeveSpec(kind="single", name="MR01", sid="MR01_BTC", asset="BTC",
                      params={"bb_window": 50, "k": 2.5, "sl_atr": 2.0, "max_bars": 24})
    w = build_worker_for(spec, alloc_capital=300.0, leverage=2.0, data_dir=tmp_path)
    assert isinstance(w, StrategyWorker)
    assert w.capital == 300.0 and w.leverage == 2.0


def test_build_pairs_worker(tmp_path):
    spec = SleeveSpec(kind="pairs", name="PR01", sid="PR_ETHBTC", a="ETH", b="BTC",
                      params={"n": 50, "z_in": 2.0, "z_exit": 0.75, "max_bars": 72})
    w = build_worker_for(spec, alloc_capital=200.0, leverage=2.0, data_dir=tmp_path)
    assert isinstance(w, PairsWorker)
    assert w.capital == 200.0
  • Step 2: Run — verifica fallimento

Run: uv run pytest tests/portfolio/test_runner_build.py -v Expected: FAIL (ModuleNotFoundError: runner).

  • Step 3: Implementa la parte di build in runner.py
"""PortfolioRunner: faccia live del portafoglio (capitale pool, sizing, ribilancio, ledger).
Riusa i worker esistenti come esecutori e il data layer Cerbero v2."""
from __future__ import annotations

from pathlib import Path

from src.portfolio.base import SleeveSpec, Portfolio
from src.portfolio.ledger import PortfolioLedger
from src.live.strategy_worker import StrategyWorker
from src.live.pairs_worker import PairsWorker
from src.live.multi_runner import MLWorkerWrapper
from src.live.strategy_loader import load_strategy

# Codice-breve sleeve -> nome modulo Strategy in scripts/strategies/
_STRAT_MODULE = {
    "MR01": "MR01_bollinger_fade", "MR02": "MR02_donchian_fade",
    "MR07": "MR07_return_reversal", "SH01": "SH01_shape_ml",
    # DIP01/TR01/ROT02 sono honest a sé: vedi nota nel design (worker dedicati in fase 2)
}
DATA_DIR = Path("data/paper_trades")


def build_worker_for(spec: SleeveSpec, alloc_capital: float, leverage: float,
                     data_dir: Path = DATA_DIR, position_size: float = 0.15):
    """Costruisce il worker esecutore per uno sleeve con capitale = quota allocata."""
    if spec.kind == "pairs":
        return PairsWorker(
            asset_a=spec.a, asset_b=spec.b, tf=spec.tf, params=spec.params,
            capital=alloc_capital, position_size=position_size, leverage=leverage,
            fee_rt=0.001, name="PR01_pairs_reversion", data_dir=data_dir,
        )
    module = _STRAT_MODULE.get(spec.name)
    if module is None:
        raise ValueError(f"sleeve live non ancora supportato: {spec.name} "
                         f"(honest DIP01/TR01/ROT02 richiedono worker dedicati, fase 2)")
    strategy = load_strategy(module)
    worker = StrategyWorker(
        strategy=strategy, asset=spec.asset, tf=spec.tf, capital=alloc_capital,
        position_size=position_size, leverage=leverage, params=spec.params, data_dir=data_dir,
    )
    if spec.kind == "ml":                       # SH01: retraining periodico
        return MLWorkerWrapper(worker, {"retrain_hours": 24})
    return worker
  • Step 4: Run — verifica passaggio

Run: uv run pytest tests/portfolio/test_runner_build.py -v Expected: 2 passed.

  • Step 5: Commit
git add src/portfolio/runner.py tests/portfolio/test_runner_build.py
git commit -m "feat(portfolio): build_worker_for (worker esecutori con capitale da alloc pool)"

Task 9: PortfolioRunner — loop live (data v2, ribilancio, aggregazione)

Files:

  • Modify: src/portfolio/runner.py

  • Test: tests/portfolio/test_runner_rebalance.py, scripts/analysis/smoke_portfolio.py

  • Step 1: Test del ribilancio (fallisce)

tests/portfolio/test_runner_rebalance.py:

from src.portfolio.runner import rebalance_allocations
from src.portfolio.ledger import PortfolioLedger


def test_rebalance_resizes_to_total(tmp_path):
    L = PortfolioLedger("PX", total_capital=1000.0, data_dir=tmp_path)

    class FakeWorker:
        def __init__(self, cap): self.capital = cap
    workers = {"a": FakeWorker(700.0), "b": FakeWorker(500.0)}   # equity 1200
    rebalance_allocations(L, workers, {"a": 0.5, "b": 0.5})
    assert L.total_capital == 1200.0
    assert workers["a"].capital == 600.0 and workers["b"].capital == 600.0
  • Step 2: Run — verifica fallimento

Run: uv run pytest tests/portfolio/test_runner_rebalance.py -v Expected: FAIL (ImportError: rebalance_allocations).

  • Step 3: Implementa ribilancio + loop in runner.py

Aggiungi a src/portfolio/runner.py:

def _worker_equity(w) -> float:
    inner = getattr(w, "worker", w)              # smonta MLWorkerWrapper
    return float(getattr(inner, "capital", 0.0))


def rebalance_allocations(ledger: PortfolioLedger, workers: dict, weights: dict[str, float]):
    """Ribilancio: total_capital = Σ equity sleeve; riallinea il capitale-base di ogni worker
    a peso×total. Le posizioni APERTE restano sul loro notional (approssimazione dichiarata)."""
    ledger.total_capital = sum(_worker_equity(w) for w in workers.values())
    alloc = ledger.allocate(weights)
    for sid, w in workers.items():
        inner = getattr(w, "worker", w)
        inner.capital = alloc.get(sid, inner.capital)
    ledger.save()


def run(config_path: str = "portfolios.yml"):
    """Loop live a portafoglio. Data layer Cerbero v2; ribilancio a fine giornata UTC."""
    import time
    from datetime import datetime, timezone, timedelta
    import pandas as pd
    from src.portfolio.base import load_active_portfolio
    from src.portfolio.sleeves import sleeve_returns_df
    from src.portfolio.weighting import weight_vector
    from src.live.cerbero_client import CerberoClient

    p: Portfolio = load_active_portfolio(config_path)
    ledger = PortfolioLedger(p.code, total_capital=p.total_capital)
    client = CerberoClient()

    # pesi iniziali (vol-based dai rendimenti storici degli sleeve; statici per equal/cap/manual)
    dr = sleeve_returns_df(p.sleeve_ids)
    weights = p.weight_vector(dr)
    alloc = ledger.allocate(weights)

    # costruisci i worker esecutori con capitale = quota allocata
    workers = {s.sid: build_worker_for(s, alloc[s.sid], p.leverage) for s in p.sleeves}

    # risolvi i nomi strumento via get_instruments (fallback alla mappa legacy)
    from src.live.multi_runner import INSTRUMENT_MAP
    inst_map = dict(INSTRUMENT_MAP)            # TODO opzionale: arricchire via client.get_instruments

    last_day = ""
    poll = 60
    while True:
        try:
            # fetch candele (v2 unificato) per ogni asset/tf richiesto dagli sleeve
            keys = set()
            for s in p.sleeves:
                if s.kind == "pairs":
                    keys.add((s.a, s.tf)); keys.add((s.b, s.tf))
                else:
                    keys.add((s.asset, s.tf))
            cache = {}
            end = datetime.now(timezone.utc); start = end - timedelta(days=60)
            for asset, tf in keys:
                inst = inst_map.get(asset, f"{asset}-PERPETUAL")
                candles = client.get_historical_v2(inst, start.strftime("%Y-%m-%d"),
                                                   end.strftime("%Y-%m-%d"), tf)
                if candles:
                    df = pd.DataFrame(candles)
                    df["timestamp"] = df["timestamp"].astype("int64")
                    cache[(asset, tf)] = df.sort_values("timestamp").reset_index(drop=True)

            # tick di ogni worker (esecutore)
            for s in p.sleeves:
                w = workers[s.sid]
                if s.kind == "pairs":
                    ka, kb = (s.a, s.tf), (s.b, s.tf)
                    if ka in cache and kb in cache:
                        w.tick(cache[ka], cache[kb])
                else:
                    key = (s.asset, s.tf)
                    if key in cache:
                        inner = getattr(w, "worker", w)
                        if hasattr(w, "needs_training") and w.needs_training():
                            w.train(cache[key], hold=inner.hold_bars)
                        w.tick(cache[key])

            # aggrega equity nel ledger
            ledger.update_equity({sid: _worker_equity(w) for sid, w in workers.items()})

            # ribilancio a cambio giorno UTC
            today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
            if today != last_day and last_day:
                dr = sleeve_returns_df(p.sleeve_ids)
                rebalance_allocations(ledger, workers, p.weight_vector(dr))
            last_day = today
            ledger.save()

        except KeyboardInterrupt:
            ledger.save()
            print("shutdown")
            break
        except Exception as e:
            print(f"[runner] errore: {e}")
        time.sleep(poll)


if __name__ == "__main__":
    run()
  • Step 4: Run — verifica passaggio

Run: uv run pytest tests/portfolio/test_runner_rebalance.py -v Expected: 1 passed.

  • Step 5: Smoke live (un tick reale, niente ordini)

scripts/analysis/smoke_portfolio.py:

"""Smoke reale: un giro di fetch v2 + build worker + un tick del portafoglio attivo.
NON apre ordini reali (paper). Verifica data layer v2 + sizing + ledger."""
import sys, shutil, tempfile
from pathlib import Path
from datetime import datetime, timezone, timedelta
import pandas as pd

PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))

from src.portfolio.base import load_active_portfolio
from src.portfolio.ledger import PortfolioLedger
from src.portfolio.runner import build_worker_for, _worker_equity
from src.live.cerbero_client import CerberoClient
from src.live.multi_runner import INSTRUMENT_MAP


def main():
    tmp = Path(tempfile.mkdtemp())
    p = load_active_portfolio(PROJECT_ROOT / "portfolios.yml")
    ledger = PortfolioLedger(p.code, total_capital=p.total_capital, data_dir=tmp)
    alloc = ledger.allocate({s.sid: 1.0 / len(p.sleeves) for s in p.sleeves})
    client = CerberoClient()
    print(f"Portafoglio attivo: {p.code} ({p.label}) — {len(p.sleeves)} sleeve, leva {p.leverage}x")
    end = datetime.now(timezone.utc); start = end - timedelta(days=60)
    ok = 0
    for s in p.sleeves[:3]:                       # 3 sleeve campione per lo smoke
        asset = s.asset or s.a
        inst = INSTRUMENT_MAP.get(asset, f"{asset}-PERPETUAL")
        candles = client.get_historical_v2(inst, start.strftime("%Y-%m-%d"),
                                           end.strftime("%Y-%m-%d"), s.tf)
        print(f"  {s.sid:<12s} {inst:<18s} candele={len(candles)}")
        ok += len(candles) > 0
    print(f"OK: {ok}/3 sleeve con feed v2 fresco. Ledger equity iniziale={ledger.equity}")
    shutil.rmtree(tmp, ignore_errors=True)


if __name__ == "__main__":
    main()

Run: uv run python scripts/analysis/smoke_portfolio.py Expected: stampa il portafoglio attivo (PORT06) e 3/3 sleeve con candele v2 > 0.

  • Step 6: Commit
git add src/portfolio/runner.py tests/portfolio/test_runner_rebalance.py scripts/analysis/smoke_portfolio.py
git commit -m "feat(portfolio): PortfolioRunner live (data v2, tick, ribilancio giornaliero, ledger)"

Task 10: Documentazione (CLAUDE.md, README, comandi)

Files:

  • Modify: CLAUDE.md, README.md

  • Step 1: Aggiorna CLAUDE.md

Nella struttura aggiungi src/portfolio/ e scripts/portfolios/; in "Comandi" aggiungi:

uv run python scripts/portfolios/PORT06_master_shape.py   # report backtest portafoglio
uv run python -m src.portfolio.runner                      # paper trading a PORTAFOGLIO (capitale pool)
uv run python scripts/analysis/smoke_portfolio.py          # smoke live data layer v2

Aggiungi una sezione "Portafogli" che riassume: oggetto Portfolio (pool, backtest+live), schemi pesi, default PORT06 (cap pairs 33%, leva 2x), data layer Cerbero v2, limite noto (posizioni aperte non travasate al ribilancio).

  • Step 2: Aggiorna README.md

Aggiungi la cartella portfolios/ alla struttura e una riga d'uso del nuovo paper trader a portafoglio. Prosa italiana completa (artefatto pubblico).

  • Step 3: Esegui l'intera suite

Run: uv run pytest -m "not network" -v Expected: tutti i test (weighting, sleeves, backtest_parity, definitions, ledger, config, runner_build, runner_rebalance) passano.

  • Step 4: Commit
git add CLAUDE.md README.md
git commit -m "docs(portfolio): documenta cartella portfolios, comandi e default PORT06"

Self-review (svolta in fase di scrittura)

  • Copertura spec: §3 layout → Task 2-9; §4 schema → Task 4; §5 backtest → Task 4-5; §6 live → Task 8-9; §7 persistenza → Task 6; §8 portafogli/default → Task 5,7; §9 test → ogni task TDD + suite finale; §2.6 data v2 → Task 1,9. Tutte coperte.
  • Limite noto (posizioni aperte non travasate): implementato in rebalance_allocations e documentato (Task 9 docstring + Task 10).
  • Honest DIP01/TR01/ROT02 nel live: build_worker_for solleva un errore esplicito (worker dedicati in fase 2) — coerente con lo scope: backtest li include, il live v1 esegue fade/pairs/shape che hanno worker pronti. Nota: se il default PORT06 deve girare live al primo colpo servono i worker honest; in alternativa per il primo avvio live usare PORT04-shape senza honest, oppure aggiungere i 3 worker honest come Task 8b. Da decidere in esecuzione.
  • Consistenza tipi: sid usato come chiave ovunque (definizioni ↔ all_sleeve_equities ↔ ledger ↔ workers); weight_vector firma identica in weighting.py e Portfolio.weight_vector; _worker_equity gestisce MLWorkerWrapper.
  • Placeholder: nessun TBD nel codice; l'unico TODO è opzionale (arricchire inst_map via get_instruments) e non blocca.

Punto aperto per l'esecuzione: il default PORT06 contiene gli sleeve honest (DIP01/TR01/ROT02) che NON hanno ancora un worker live. Decidere a inizio esecuzione se (a) aggiungere Task 8b coi worker honest, oppure (b) far girare il primo live con un portafoglio senza honest (fade+pairs+shape) e tenere PORT06 completo solo in backtest finché i worker honest non esistono.