feat(portfolio): SleeveSpec/Portfolio/backtest con parità verso report_families
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
"""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
|
||||||
|
name: str
|
||||||
|
sid: str
|
||||||
|
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
|
||||||
|
oos: dict
|
||||||
|
yearly: dict
|
||||||
|
risk: dict
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
import numpy as np
|
||||||
|
we = np.ones(len(self.sleeve_ids)) / len(self.sleeve_ids)
|
||||||
|
cov = dr.cov().values
|
||||||
|
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)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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()
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user