feat: phase 1 — data downloader, backtrader runner, SMA strategy
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
|
||||
|
||||
@dataclass
|
||||
class BacktestConfig:
|
||||
symbols: list[str] = field(default_factory=lambda: ["sh600000"])
|
||||
start_date: str = "2023-01-01"
|
||||
end_date: str = "2024-12-31"
|
||||
initial_cash: float = 1_000_000.0
|
||||
commission: float = 0.0003 # 0.03% for Chinese A-shares
|
||||
stamp_duty: float = 0.001 # 0.1% stamp duty on sells only (handled in strategy)
|
||||
adjust: str = "qfq"
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Convert pandas DataFrames to backtrader data feeds."""
|
||||
import backtrader as bt
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def df_to_bt_feed(df: pd.DataFrame) -> bt.feeds.PandasData:
|
||||
"""Convert a standardized OHLCV DataFrame to a backtrader PandasData feed."""
|
||||
df = df.copy()
|
||||
df["date"] = pd.to_datetime(df["date"])
|
||||
df = df.set_index("date")
|
||||
df = df[["open", "high", "low", "close", "volume"]]
|
||||
return bt.feeds.PandasData(dataname=df)
|
||||
@@ -0,0 +1,68 @@
|
||||
"""BacktestRunner: orchestrates data loading, cerebro setup, and execution."""
|
||||
import logging
|
||||
import backtrader as bt
|
||||
from typing import Optional
|
||||
|
||||
from backtest.config import BacktestConfig
|
||||
from backtest.feed import df_to_bt_feed
|
||||
from data.downloader import download_daily
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BacktestRunner:
|
||||
"""Run backtrader backtests with Chinese equity data."""
|
||||
|
||||
def __init__(self, config: BacktestConfig):
|
||||
self.config = config
|
||||
self.cerebro = bt.Cerebro()
|
||||
self._results: Optional[list] = None
|
||||
|
||||
def add_data(self, symbol: str) -> None:
|
||||
"""Download data for a symbol and add to cerebro as a feed."""
|
||||
df = download_daily(
|
||||
symbol=symbol,
|
||||
start=self.config.start_date,
|
||||
end=self.config.end_date,
|
||||
adjust=self.config.adjust,
|
||||
)
|
||||
feed = df_to_bt_feed(df)
|
||||
self.cerebro.adddata(feed, name=symbol)
|
||||
logger.info(f"Added {symbol}: {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
|
||||
self.cerebro.broker.setcash(self.config.initial_cash)
|
||||
self.cerebro.broker.setcommission(commission=self.config.commission)
|
||||
|
||||
# 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")
|
||||
|
||||
# Add strategy
|
||||
strategy_kwargs = strategy_kwargs or {}
|
||||
self.cerebro.addstrategy(strategy_cls, **strategy_kwargs)
|
||||
|
||||
def run(self, strategy_cls, strategy_kwargs: Optional[dict] = None) -> list:
|
||||
"""Setup and run the backtest. Returns cerebro run results."""
|
||||
self.setup(strategy_cls, strategy_kwargs)
|
||||
start_val = self.cerebro.broker.getvalue()
|
||||
logger.info(f"Starting portfolio value: {start_val:,.2f}")
|
||||
self._results = self.cerebro.run()
|
||||
end_val = self.cerebro.broker.getvalue()
|
||||
logger.info(f"Ending portfolio value: {end_val:,.2f}")
|
||||
return self._results
|
||||
|
||||
def get_results(self) -> Optional[list]:
|
||||
return self._results
|
||||
Reference in New Issue
Block a user