From 085e51abf1e35fa4a5e9af0db1e71820ec643aea Mon Sep 17 00:00:00 2001 From: Yuxuan Yan Date: Sun, 7 Jun 2026 09:26:35 +0800 Subject: [PATCH] feat: add 5-day reversal strategy Co-Authored-By: Claude Opus 4.7 --- run_example.py | 8 ++++---- strategies/reversal.py | 27 +++++++++++++++++++++++++++ tests/test_reversal.py | 21 +++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 strategies/reversal.py create mode 100644 tests/test_reversal.py diff --git a/run_example.py b/run_example.py index dc6bfb9..2ff2ef8 100644 --- a/run_example.py +++ b/run_example.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -"""End-to-end smoke test: download data -> backtest SMA crossover -> print results.""" +"""End-to-end smoke test: download data -> backtest 5-day reversal -> print results.""" import logging from backtest.config import BacktestConfig from backtest.runner import BacktestRunner -from strategies.base import SmaCross +from strategies.reversal import FiveDayReversal from analysis.report import print_results logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") @@ -11,14 +11,14 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(me def main(): config = BacktestConfig( - symbols=["sh600000"], # 浦发银行 + symbols=["sh600000", "sz000001", "sh600519"], # 浦发银行, 平安银行, 贵州茅台 start_date="2023-01-01", end_date="2024-12-31", initial_cash=1_000_000, ) runner = BacktestRunner(config) - results = runner.run(SmaCross) + results = runner.run(FiveDayReversal) print_results(results, config.initial_cash) diff --git a/strategies/reversal.py b/strategies/reversal.py new file mode 100644 index 0000000..ab6e6c3 --- /dev/null +++ b/strategies/reversal.py @@ -0,0 +1,27 @@ +import backtrader as bt + + +class FiveDayReversal(bt.Strategy): + """Buy on 5-day oversold signal, sell on bounce or time stop.""" + + params = ( + ("lookback", 5), + ("entry_threshold", -0.05), # buy when 5-day return < -5% + ("exit_threshold", 0.0), # sell when 1-day return > 0% + ("max_hold", 3), # max hold days + ) + + def __init__(self): + self.roc5 = bt.indicators.RateOfChange(self.data.close, period=self.params.lookback) + self.hold_counter = 0 + + def next(self): + if not self.position: + if self.roc5[0] < self.params.entry_threshold: # RateOfChange returns a fraction, not % + self.buy() + self.hold_counter = 0 + else: + self.hold_counter += 1 + roc1 = (self.data.close[0] / self.data.close[-1] - 1) * 100 + if roc1 > self.params.exit_threshold * 100 or self.hold_counter >= self.params.max_hold: + self.close() diff --git a/tests/test_reversal.py b/tests/test_reversal.py new file mode 100644 index 0000000..6ed7cdb --- /dev/null +++ b/tests/test_reversal.py @@ -0,0 +1,21 @@ +import pytest +from backtest.config import BacktestConfig +from backtest.runner import BacktestRunner +from strategies.reversal import FiveDayReversal + + +def test_reversal_smoke(): + """Smoke test: run a minimal reversal backtest and check results exist.""" + config = BacktestConfig( + symbols=["sh600000"], + start_date="2024-01-01", + end_date="2024-03-31", + initial_cash=100_000, + ) + runner = BacktestRunner(config) + results = runner.run(FiveDayReversal) + assert results is not None + assert len(results) == 1 + # Check analyzers exist + sharpe = results[0].analyzers.sharpe.get_analysis() + assert "sharperatio" in sharpe