Strategies - BarStrategy protocol (close/open/high/low/volume bars; row d uses bars through d-1) and RobustMomentumStrategy: overnight_recovery_top10 (US), lowrisk_momentum_top15 and lowbeta_sharpe_mom_top20 (CN), top-N with rank hysteresis, 10-day rebalance, weights drift between rebalances. - trader: strategy_fields/strategy_bars/bar_session_weights; one signal path (_signals_for_sessions) replaces generate_target_weights. Live fixes - CSI 300 constituents come from China Securities Index's file; the Wikipedia table was 122 names off the real index. - S&P 500 change log moved to "Historical components of the S&P 500". - data_manager: tickers missing from a partial Yahoo batch are retried alone before being marked unavailable (CEG, GEV, SNDK ... were dropped). update() takes fields=[...] and stores high/low/volume panels. - Whole-lot orders go to the nearest lot within cash (CN books held ~62% cash); held names are resized only beyond RESIZE_BAND (10%), so books no longer pay a ticket fee to trade daily drift. Web backtests - backtest_runner.py replaces the duplicated bridge/precompute code: PIT membership in both markets (data/csi300_history.json), stock universe limited to index members (ETFs and benchmarks in the shared panel were pickable), and the book kept like the live trader (simulate_live_book: signal timing, drift, order floor, resize band, fees on orders placed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
50 lines
1.8 KiB
Python
50 lines
1.8 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
|
|
|
|
|
|
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."""
|