Files
quant-trading/strategies/base.py
Gahow Wang 722cade974 fix: unify backtest execution and add forward validation candidates
Share point-in-time signals and a cash-funded order ledger across backtests and trading. Correct eligibility, missing-price fills, quality factors, regime inputs, warmup windows, and cash-injection accounting. Add reproducible registered-strategy comparisons and paired forward candidates with a 20% resize band.

Validation: 125 Python tests, 23 web tests, TypeScript/Vite build, 87 strategy causality checks, and real-data target/order/equity parity for 10 strategies.
2026-10-01 02:24:01 -07:00

58 lines
2.0 KiB
Python

from abc import ABC, abstractmethod
import pandas as pd
class Strategy(ABC):
"""
Abstract base class for all strategies.
"""
@abstractmethod
def generate_signals(self, data):
"""
Generates trading signals for each stock.
:param data: A pandas DataFrame of historical price data.
:return: A pandas Series with the same index as the input data,
containing the trading signals (e.g., 'buy', 'hold', 'sell').
"""
pass
def mask_scores(scores: pd.DataFrame, eligible: pd.DataFrame | None) -> pd.DataFrame:
return scores if eligible is None else scores.where(eligible.reindex_like(scores).fillna(False))
class StockStrategy(Strategy):
accepts_eligibility = True
class BarStrategy(Strategy):
"""Strategy on completed daily bars with several fields.
``fields`` names the panels it reads among close/open/high/low/volume.
``generate_signals(bars)`` takes ``{field: DataFrame}`` (a bare close
DataFrame is accepted when only "close" is needed) and returns weights
whose row t uses bars through t-1, like every Strategy. Backtests may add
``bars["eligible"]`` (bool, point-in-time index membership): prices stay
unmasked so signals see full histories, and only eligible names are held.
Timing differs from close-price strategies in the live trader: those read
the session's open as their latest price, while a BarStrategy only uses
completed bars, so the weights of row t trade at the close of t.
"""
fields: tuple[str, ...] = ("close",)
def generate_signals(self, data):
bars = data if isinstance(data, dict) else {"close": data}
missing = [f for f in self.fields if f not in bars]
if missing:
raise ValueError(f"{type(self).__name__} needs bar fields {missing}")
return self.generate_weights(bars)
@abstractmethod
def generate_weights(self, bars: dict[str, pd.DataFrame]) -> pd.DataFrame:
"""Weights (dates x assets), row t using bars through t-1."""