auto-sync: 2026-05-11 09:02:47

This commit is contained in:
cfdaily
2026-05-11 09:02:47 +08:00
parent a8cd876c8e
commit 8c1d4b76d8
2 changed files with 362 additions and 0 deletions
+317
View File
@@ -0,0 +1,317 @@
"""
Dual Moving Average (双均线) Strategy
Classic trend-following strategy:
- Golden Cross: Short MA crosses above Long MA → BUY
- Death Cross: Short MA crosses below Long MA → SELL
Author: Zhang Fei
Date: 2026-05-11
"""
import numpy as np
import pandas as pd
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
from datetime import datetime
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
@dataclass
class Trade:
code: str
entry_date: datetime
exit_date: Optional[datetime]
entry_price: float
exit_price: Optional[float]
direction: int
shares: int
entry_value: float
exit_value: Optional[float]
profit: Optional[float]
profit_pct: Optional[float]
hold_days: Optional[int]
strategy: str
@dataclass
class BacktestResult:
strategy: str
start_date: datetime
end_date: datetime
initial_capital: float
final_capital: float
total_return: float
annual_return: float
max_drawdown: float
sharpe_ratio: float
win_rate: float
total_trades: int
win_trades: int
loss_trades: int
avg_profit_pct: float
avg_win_pct: float
avg_loss_pct: float
trades: List[Trade]
class DualMAStrategy:
def __init__(self, short_period: int = 5, long_period: int = 20):
self.short_period = short_period
self.long_period = long_period
self.name = f"Dual_MA_{short_period}_{long_period}"
def calculate_indicators(self, df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df['ma_short'] = df['close'].rolling(window=self.short_period, min_periods=1).mean()
df['ma_long'] = df['close'].rolling(window=self.long_period, min_periods=1).mean()
return df
def generate_signals(self, df: pd.DataFrame) -> pd.DataFrame:
df = self.calculate_indicators(df)
# Generate cross signals
df['ma_prev_short'] = df['ma_short'].shift(1)
df['ma_prev_long'] = df['ma_long'].shift(1)
# Golden Cross: Short MA crosses above Long MA
df['golden_cross'] = (
(df['ma_prev_short'] <= df['ma_prev_long']) &
(df['ma_short'] > df['ma_long'])
).astype(int)
# Death Cross: Short MA crosses below Long MA
df['death_cross'] = (
(df['ma_prev_short'] >= df['ma_prev_long']) &
(df['ma_short'] < df['ma_long'])
).astype(int)
df['signal'] = 0
df.loc[df['golden_cross'] == 1, 'signal'] = 1
df.loc[df['death_cross'] == 1, 'signal'] = -1
return df
class DualMABacktester:
def __init__(self, initial_capital: float = 100000.0,
short_period: int = 5,
long_period: int = 20,
commission: float = 0.0003,
slipage: float = 0.001):
self.initial_capital = initial_capital
self.commission = commission
self.slipage = slipage
self.strategy = DualMAStrategy(short_period, long_period)
def run_single_stock(self, df: pd.DataFrame, code: str) -> BacktestResult:
df = df.sort_values('date').reset_index(drop=True)
df = self.strategy.generate_signals(df)
capital = self.initial_capital
position = 0
trades = []
current_trade = None
for i, row in df.iterrows():
# Entry
if row['signal'] == 1 and position == 0:
entry_price = row['close'] * (1 + self.slipage)
shares = int(capital / entry_price)
if shares > 0:
entry_value = shares * entry_price
commission_fee = entry_value * self.commission
capital -= (entry_value + commission_fee)
position = shares
current_trade = Trade(
code=code,
entry_date=row['date'],
exit_date=None,
entry_price=entry_price,
exit_price=None,
direction=1,
shares=shares,
entry_value=entry_value,
exit_value=None,
profit=None,
profit_pct=None,
hold_days=None,
strategy=self.strategy.name
)
# Exit
elif row['signal'] == -1 and position > 0 and current_trade is not None:
exit_price = row['close'] * (1 - self.slipage)
exit_value = position * exit_price
commission_fee = exit_value * self.commission
capital += (exit_value - commission_fee)
current_trade.exit_date = row['date']
current_trade.exit_price = exit_price
current_trade.exit_value = exit_value
current_trade.profit = exit_value - current_trade.entry_value - commission_fee - (current_trade.entry_value * self.commission)
current_trade.profit_pct = (exit_price - current_trade.entry_price) / current_trade.entry_price
current_trade.hold_days = (row['date'] - current_trade.entry_date).days
trades.append(current_trade)
position = 0
current_trade = None
# Close remaining position at end
if position > 0 and current_trade is not None:
last_row = df.iloc[-1]
exit_price = last_row['close']
exit_value = position * exit_price
commission_fee = exit_value * self.commission
capital += (exit_value - commission_fee)
current_trade.exit_date = last_row['date']
current_trade.exit_price = exit_price
current_trade.exit_value = exit_value
current_trade.profit = exit_value - current_trade.entry_value - commission_fee - (current_trade.entry_value * self.commission)
current_trade.profit_pct = (exit_price - current_trade.entry_price) / current_trade.entry_price
current_trade.hold_days = (last_row['date'] - current_trade.entry_date).days
trades.append(current_trade)
return self._calculate_metrics(trades, df)
def _calculate_metrics(self, trades: List[Trade], df: pd.DataFrame) -> BacktestResult:
if len(trades) == 0:
return BacktestResult(
strategy=self.strategy.name,
start_date=df['date'].min(),
end_date=df['date'].max(),
initial_capital=self.initial_capital,
final_capital=self.initial_capital,
total_return=0.0,
annual_return=0.0,
max_drawdown=0.0,
sharpe_ratio=0.0,
win_rate=0.0,
total_trades=0,
win_trades=0,
loss_trades=0,
avg_profit_pct=0.0,
avg_win_pct=0.0,
avg_loss_pct=0.0,
trades=[]
)
final_capital = self.initial_capital + sum(t.profit or 0 for t in trades)
total_return = (final_capital - self.initial_capital) / self.initial_capital
days = (df['date'].max() - df['date'].min()).days
annual_return = (1 + total_return) ** (365 / days) - 1 if days > 0 else 0
win_trades = [t for t in trades if (t.profit or 0) > 0]
loss_trades = [t for t in trades if (t.profit or 0) <= 0]
win_rate = len(win_trades) / len(trades) if trades else 0
avg_profit_pct = np.mean([t.profit_pct or 0 for t in trades])
avg_win_pct = np.mean([t.profit_pct or 0 for t in win_trades]) if win_trades else 0
avg_loss_pct = np.mean([t.profit_pct or 0 for t in loss_trades]) if loss_trades else 0
# Simple max drawdown calculation
cumulative = self.initial_capital
max_cumulative = cumulative
max_drawdown = 0.0
for trade in trades:
cumulative += trade.profit or 0
if cumulative > max_cumulative:
max_cumulative = cumulative
drawdown = (max_cumulative - cumulative) / max_cumulative
if drawdown > max_drawdown:
max_drawdown = drawdown
# Sharpe ratio (simplified)
daily_returns = [t.profit_pct / t.hold_days for t in trades if t.hold_days and t.hold_days > 0]
sharpe_ratio = np.sqrt(252) * np.mean(daily_returns) / np.std(daily_returns) if len(daily_returns) > 1 and np.std(daily_returns) > 0 else 0
return BacktestResult(
strategy=self.strategy.name,
start_date=df['date'].min(),
end_date=df['date'].max(),
initial_capital=self.initial_capital,
final_capital=final_capital,
total_return=total_return,
annual_return=annual_return,
max_drawdown=max_drawdown,
sharpe_ratio=sharpe_ratio,
win_rate=win_rate,
total_trades=len(trades),
win_trades=len(win_trades),
loss_trades=len(loss_trades),
avg_profit_pct=avg_profit_pct,
avg_win_pct=avg_win_pct,
avg_loss_pct=avg_loss_pct,
trades=trades
)
def load_sample_data(code: str = '510300.SH',
start_date: str = '2023-01-01',
end_date: str = '2024-12-31') -> pd.DataFrame:
"""Load sample data for testing"""
np.random.seed(42)
dates = pd.date_range(start=start_date, end=end_date, freq='B')
n = len(dates)
# Generate synthetic price data with trend
base_price = 4.0
returns = np.random.normal(0.0005, 0.015, n)
prices = base_price * np.cumprod(1 + returns)
df = pd.DataFrame({
'date': dates,
'open': prices * (1 + np.random.normal(0, 0.002, n)),
'high': prices * (1 + np.random.normal(0.005, 0.003, n)),
'low': prices * (1 - np.random.normal(0.005, 0.003, n)),
'close': prices,
'volume': np.random.randint(1000000, 10000000, n)
})
return df
def main():
# Sample backtest
logger.info("Starting Dual MA Strategy Backtest...")
df = load_sample_data()
logger.info(f"Loaded {len(df)} days of data")
backtester = DualMABacktester(
initial_capital=100000.0,
short_period=5,
long_period=20
)
result = backtester.run_single_stock(df, '510300.SH')
logger.info(f"Strategy: {result.strategy}")
logger.info(f"Period: {result.start_date.date()} to {result.end_date.date()}")
logger.info(f"Initial Capital: {result.initial_capital:.2f}")
logger.info(f"Final Capital: {result.final_capital:.2f}")
logger.info(f"Total Return: {result.total_return*100:.2f}%")
logger.info(f"Annual Return: {result.annual_return*100:.2f}%")
logger.info(f"Max Drawdown: {result.max_drawdown*100:.2f}%")
logger.info(f"Sharpe Ratio: {result.sharpe_ratio:.2f}")
logger.info(f"Win Rate: {result.win_rate*100:.2f}%")
logger.info(f"Total Trades: {result.total_trades}")
logger.info(f"Win/Loss: {result.win_trades}/{result.loss_trades}")
logger.info(f"Avg Profit %: {result.avg_profit_pct*100:.2f}%")
# Print first 5 trades
logger.info("\nFirst 5 Trades:")
for i, trade in enumerate(result.trades[:5]):
logger.info(f" {i+1}. {trade.entry_date.date()} -> {trade.exit_date.date()}: "
f"{trade.profit_pct*100:+.2f}% (hold {trade.hold_days} days)")
if __name__ == '__main__':
main()
@@ -0,0 +1,45 @@
# 双均线策略回测报告
## 策略信息
- **策略名称**: Dual_MA_5_20
- **回测期间**: 2023-01-02 至 2024-12-31
- **初始资金**: 100000.00 元
## 绩效指标
| 指标 | 值 |
|------|-----|
| 最终资金 | 104436.44 元 |
| 总收益率 | 4.44% |
| 年化收益率 | 2.20% |
| 最大回撤 | 14.31% |
| 夏普比率 | -3.22 |
| 胜率 | 44.44% |
| 总交易次数 | 18 |
| 盈利/亏损 | 8/10 |
| 平均盈亏比 | 0.46% |
| 平均盈利 | 5.75% |
| 平均亏损 | -3.77% |
## 交易明细
| 序号 | 入场日期 | 出场日期 | 持仓天数 | 收益率 |
|------|----------|----------|----------|--------|
| 1 | 2023-01-09 | 2023-01-23 | 14 | -3.46% |
| 2 | 2023-03-24 | 2023-03-31 | 7 | -3.17% |
| 3 | 2023-04-05 | 2023-04-26 | 21 | +1.62% |
| 4 | 2023-05-09 | 2023-05-16 | 7 | -3.79% |
| 5 | 2023-06-12 | 2023-07-05 | 23 | -1.28% |
| 6 | 2023-08-09 | 2023-09-28 | 50 | +6.52% |
| 7 | 2023-10-16 | 2023-11-15 | 30 | +11.41% |
| 8 | 2023-11-30 | 2023-12-01 | 1 | +1.13% |
| 9 | 2023-12-20 | 2024-01-05 | 16 | -6.63% |
| 10 | 2024-01-31 | 2024-02-16 | 16 | -1.30% |
| 11 | 2024-02-21 | 2024-04-22 | 61 | +13.10% |
| 12 | 2024-05-22 | 2024-06-25 | 34 | +4.83% |
| 13 | 2024-06-28 | 2024-07-04 | 6 | +0.09% |
| 14 | 2024-07-05 | 2024-07-19 | 14 | -3.20% |
| 15 | 2024-08-08 | 2024-09-02 | 25 | +7.29% |
| 16 | 2024-09-03 | 2024-09-06 | 3 | -4.70% |
| 17 | 2024-10-31 | 2024-11-13 | 13 | -7.47% |
| 18 | 2024-11-20 | 2024-12-10 | 20 | -2.65% |