File size: 2,913 Bytes
3339913 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | """Core data types shared across the algotrader 2.0 stack.
Everything downstream (engine, validation, UI) speaks these types, so they are
deliberately small, immutable-ish and free of framework dependencies.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
import pandas as pd
OHLCV_COLUMNS = ("open", "high", "low", "close", "volume")
@dataclass(frozen=True)
class MarketData:
"""A validated OHLCV series plus provenance.
Provenance matters here: the app is about honesty, so the UI always tells
the user whether they are looking at real prices or a simulation.
"""
symbol: str
df: pd.DataFrame
source: str # "yfinance" | "bundled" | "synthetic"
interval: str = "1d"
note: str = ""
@property
def is_real(self) -> bool:
return self.source in ("yfinance", "bundled")
@property
def start(self) -> pd.Timestamp:
return self.df.index[0]
@property
def end(self) -> pd.Timestamp:
return self.df.index[-1]
def __len__(self) -> int: # pragma: no cover - trivial
return len(self.df)
@dataclass(frozen=True)
class CostModel:
"""Round-trip friction. All values are one-way, in basis points."""
commission_bps: float = 1.0
slippage_bps: float = 2.0
short_borrow_bps: float = 50.0 # annualised, charged on short exposure
@property
def one_way_bps(self) -> float:
return self.commission_bps + self.slippage_bps
@dataclass
class BacktestResult:
"""Output of a single backtest run."""
equity: pd.Series
returns: pd.Series # net of costs
gross_returns: pd.Series
position: pd.Series # exposure actually held during each bar
target: pd.Series # exposure requested by the strategy
costs: pd.Series
benchmark_equity: pd.Series
metrics: Dict[str, float] = field(default_factory=dict)
benchmark_metrics: Dict[str, float] = field(default_factory=dict)
meta: Dict[str, Any] = field(default_factory=dict)
@property
def sharpe(self) -> float:
return float(self.metrics.get("sharpe", 0.0))
@property
def n_trades(self) -> int:
return int(self.metrics.get("n_trades", 0))
@dataclass
class ValidationReport:
"""Everything we know about how much of a backtest is luck."""
permutation_p_value: Optional[float] = None
permutation_null: Optional[Any] = None # np.ndarray of null Sharpes
deflated_sharpe: Optional[float] = None
probabilistic_sharpe: Optional[float] = None
min_track_record_years: Optional[float] = None
n_trials: int = 1
pbo: Optional[float] = None
pbo_detail: Dict[str, Any] = field(default_factory=dict)
walkforward: Dict[str, Any] = field(default_factory=dict)
reality_score: float = 0.0
grade: str = "?"
verdict: str = ""
flags: list = field(default_factory=list)
|