feat: signal abstraction layer + sizer + HS300 universe + PnL/IC reports
This commit is contained in:
+29
-13
@@ -4,7 +4,7 @@ import backtrader as bt
|
||||
from typing import Optional
|
||||
|
||||
from backtest.config import BacktestConfig
|
||||
from backtest.feed import df_to_bt_feed
|
||||
from backtest.feed import df_to_bt_feed, df_to_signal_feed
|
||||
from data.downloader import download_daily
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,33 +30,49 @@ class BacktestRunner:
|
||||
self.cerebro.adddata(feed, name=symbol)
|
||||
logger.info(f"Added {symbol}: {len(df)} bars")
|
||||
|
||||
def add_signal_data(self, df, name: str) -> None:
|
||||
"""Add a pre-built OHLCV+signal DataFrame as a SignalPandasData feed."""
|
||||
feed = df_to_signal_feed(df)
|
||||
self.cerebro.adddata(feed, name=name)
|
||||
logger.info(f"Added signal feed {name}: {len(df)} bars")
|
||||
|
||||
def add_strategy(self, strategy_cls, **kwargs) -> None:
|
||||
"""Add a strategy class to cerebro."""
|
||||
self.cerebro.addstrategy(strategy_cls, **kwargs)
|
||||
|
||||
def setup(self, strategy_cls, strategy_kwargs: Optional[dict] = None) -> None:
|
||||
"""Full setup: load data for all symbols, configure cerebro, add strategy."""
|
||||
# Load data for all symbols
|
||||
for sym in self.config.symbols:
|
||||
self.add_data(sym)
|
||||
|
||||
# Configure cerebro
|
||||
def _configure(self) -> None:
|
||||
"""Configure broker, sizer, and analyzers (independent of data feeds)."""
|
||||
self.cerebro.broker.setcash(self.config.initial_cash)
|
||||
self.cerebro.broker.setcommission(commission=self.config.commission)
|
||||
self.cerebro.addsizer(bt.sizers.PercentSizer, percents=self.config.sizer_percent * 100)
|
||||
|
||||
# Add analyzers
|
||||
self.cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe", riskfreerate=0.02)
|
||||
self.cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
|
||||
self.cerebro.addanalyzer(bt.analyzers.Returns, _name="returns")
|
||||
self.cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")
|
||||
self.cerebro.addanalyzer(
|
||||
bt.analyzers.TimeReturn, _name="timereturn", timeframe=bt.TimeFrame.Days
|
||||
)
|
||||
|
||||
# Add strategy
|
||||
strategy_kwargs = strategy_kwargs or {}
|
||||
self.cerebro.addstrategy(strategy_cls, **strategy_kwargs)
|
||||
def setup(self, strategy_cls, strategy_kwargs: Optional[dict] = None) -> None:
|
||||
"""Full setup: load data for all symbols, configure cerebro, add strategy."""
|
||||
for sym in self.config.symbols:
|
||||
self.add_data(sym)
|
||||
self._configure()
|
||||
self.cerebro.addstrategy(strategy_cls, **(strategy_kwargs or {}))
|
||||
|
||||
def run(self, strategy_cls, strategy_kwargs: Optional[dict] = None) -> list:
|
||||
"""Setup and run the backtest. Returns cerebro run results."""
|
||||
"""Setup (downloading all symbols) and run the backtest."""
|
||||
self.setup(strategy_cls, strategy_kwargs)
|
||||
return self._execute()
|
||||
|
||||
def run_prepared(self, strategy_cls, strategy_kwargs: Optional[dict] = None) -> list:
|
||||
"""Run a backtest using feeds already added via ``add_signal_data``."""
|
||||
self._configure()
|
||||
self.cerebro.addstrategy(strategy_cls, **(strategy_kwargs or {}))
|
||||
return self._execute()
|
||||
|
||||
def _execute(self) -> list:
|
||||
start_val = self.cerebro.broker.getvalue()
|
||||
logger.info(f"Starting portfolio value: {start_val:,.2f}")
|
||||
self._results = self.cerebro.run()
|
||||
|
||||
Reference in New Issue
Block a user