@dataclass
class BacktestResult:
"""All outputs required for analysis, audit, visualization, and export."""
prices: pd.DataFrame
asset_returns: pd.DataFrame
target_weights: pd.DataFrame
effective_weights: pd.DataFrame
gross_returns: pd.Series
net_returns: pd.Series
equity: pd.Series
turnover: pd.Series
costs: pd.Series
cost_breakdown: pd.DataFrame
spec: BacktestSpec
metadata: dict[str, Any]
@property
def fingerprint(self) -> str:
return str(self.metadata["experiment_fingerprint"])
@property
def trades(self) -> pd.DataFrame:
"""Return an auditable target-weight change ledger."""
changes = self.effective_weights.diff().fillna(self.effective_weights)
records = []
for timestamp, row in changes.iterrows():
for asset, change in row.items():
if abs(change) > 1e-15:
records.append({
"timestamp": timestamp,
"asset": asset,
"weight_change": float(change),
"direction": "buy" if change > 0 else "sell",
"price": float(self.prices.loc[timestamp, asset]),
"target_weight": float(self.effective_weights.loc[timestamp, asset]),
})
if not records:
return pd.DataFrame(columns=["asset", "weight_change", "direction", "price", "target_weight"]).rename_axis("timestamp")
return pd.DataFrame(records).set_index("timestamp")
@property
def metrics(self) -> pd.Series:
return summary_metrics(
self.net_returns,
annualization=self.spec.annualization,
risk_free_rate=self.spec.risk_free_rate,
turnover=self.turnover,
)
@property
def summary(self) -> pd.Series:
"""Canonical compact result summary."""
return self.metrics
def to_dict(self) -> dict[str, Any]:
"""Serialize the analytical summary and reproducibility metadata."""
return {
"result_type": "backtest",
"summary": self.metrics.to_dict(),
"fingerprint": self.fingerprint,
"metadata": dict(self.metadata),
"spec": self.spec.to_dict(),
}
def compare(self, benchmark_returns: pd.Series) -> pd.Series:
return summary_metrics(
self.net_returns,
annualization=self.spec.annualization,
risk_free_rate=self.spec.risk_free_rate,
benchmark=benchmark_returns,
turnover=self.turnover,
)
def to_frame(self) -> pd.DataFrame:
return pd.concat(
{
"gross_return": self.gross_returns,
"net_return": self.net_returns,
"equity": self.equity,
"turnover": self.turnover,
"cost": self.costs,
},
axis=1,
)
def report(self, output: str | None = None, title: str | None = None) -> str:
from .report import create_html_report
return create_html_report(self, output=output, title=title)
def plot(self, kind: str = "dashboard", **kwargs: Any):
from .viz.performance import PerformanceVisualizer
viz = PerformanceVisualizer()
if kind == "dashboard":
return viz.dashboard(self, **kwargs)
if not hasattr(viz, kind):
raise ValueError(f"unknown plot kind: {kind}")
return getattr(viz, kind)(self, **kwargs)