模拟盘数据统计与月度复盘方法
模拟盘跑了一段时间,交易记录攒了几百条,但很多人只是看看总盈亏就结束了。这样跑模拟盘的意义不大------真正有价值的是从交易记录中提取统计指标,做系统性复盘,找到策略的衰减点和改进方向。
这篇文章分享一套完整的模拟盘数据统计与月度复盘方法,包含可复用的Python代码。
一、数据准备
假设你的模拟盘交易记录已经导出为CSV,字段大致如下:
csv
trade_id,symbol,direction,open_time,close_time,open_price,close_price,volume,pnl,commission
1,RB2405,long,2024-01-02 09:35,2024-01-02 14:20,3980,4010,10,300,12
2,RB2405,short,2024-01-03 10:10,2024-01-03 11:05,4020,4035,10,-150,12
...
先做统一的数据加载和预处理:
python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
def load_trades(filepath):
df = pd.read_csv(filepath, parse_dates=['open_time', 'close_time'])
# 净盈亏 = 毛利 - 手续费
df['net_pnl'] = df['pnl'] - df['commission']
# 持仓时长(分钟)
df['hold_minutes'] = (df['close_time'] - df['open_time']).dt.total_seconds() / 60
# 按平仓时间排序
df = df.sort_values('close_time').reset_index(drop=True)
return df
trades = load_trades('sim_trades.csv')
print(f"总交易数: {len(trades)}")
print(f"时间范围: {trades['close_time'].min()} ~ {trades['close_time'].max()}")
二、核心统计指标
2.1 胜率与盈亏比
胜率单独看没有意义,必须和盈亏比结合。一个胜率40%但盈亏比3:1的策略,远好于胜率70%但盈亏比0.5:1的策略。
python
def calc_basic_stats(df):
wins = df[df['net_pnl'] > 0]
losses = df[df['net_pnl'] < 0]
win_rate = len(wins) / len(df) if len(df) > 0 else 0
avg_win = wins['net_pnl'].mean() if len(wins) > 0 else 0
avg_loss = abs(losses['net_pnl'].mean()) if len(losses) > 0 else 0
profit_loss_ratio = avg_win / avg_loss if avg_loss > 0 else np.inf
# 期望值 = 胜率 * 平均盈利 - 败率 * 平均亏损
expectancy = win_rate * avg_win - (1 - win_rate) * avg_loss
stats = {
'总交易数': len(df),
'盈利笔数': len(wins),
'亏损笔数': len(losses),
'胜率': round(win_rate, 4),
'平均盈利': round(avg_win, 2),
'平均亏损': round(avg_loss, 2),
'盈亏比': round(profit_loss_ratio, 2),
'单笔期望值': round(expectancy, 2),
'总净盈亏': round(df['net_pnl'].sum(), 2),
'最大单笔盈利': round(df['net_pnl'].max(), 2),
'最大单笔亏损': round(df['net_pnl'].min(), 2),
}
return stats
stats = calc_basic_stats(trades)
for k, v in stats.items():
print(f"{k}: {v}")
2.2 期望值的意义
单笔期望值是策略能否持续盈利的核心判断标准。如果期望值 ≤ 0,无论胜率多高,长期跑下去必然亏损。这个指标比总盈亏更能反映策略质量,因为它不受交易次数的影响。
2.3 最大连亏与最大回撤
python
def calc_drawdown_series(df):
"""计算资金曲线和回撤序列"""
df = df.copy()
df['cum_pnl'] = df['net_pnl'].cumsum()
df['peak'] = df['cum_pnl'].cummax()
df['drawdown'] = df['cum_pnl'] - df['peak']
df['drawdown_pct'] = df['drawdown'] / (df['peak'].abs() + 1e-9)
return df
def calc_streak(df):
"""计算最大连胜和最大连亏"""
pnl_sign = np.sign(df['net_pnl'].values)
max_win_streak = max_loss_streak = 0
cur_win = cur_loss = 0
for s in pnl_sign:
if s > 0:
cur_win += 1
cur_loss = 0
elif s < 0:
cur_loss += 1
cur_win = 0
else:
cur_win = cur_loss = 0
max_win_streak = max(max_win_streak, cur_win)
max_loss_streak = max(max_loss_streak, cur_loss)
return max_win_streak, max_loss_streak
trades = calc_drawdown_series(trades)
max_win_streak, max_loss_streak = calc_streak(trades)
max_dd = trades['drawdown'].min()
print(f"最大连胜: {max_win_streak}")
print(f"最大连亏: {max_loss_streak}")
print(f"最大回撤(绝对值): {max_dd:.2f}")
最大连亏决定了你需要的心理承受能力和资金储备。如果一个策略历史上出现过连续12笔亏损,那么你至少要做好连亏15笔的准备。
三、资金曲线分析
3.1 资金曲线绘制
python
def plot_equity_curve(df, save_path='equity_curve.png'):
fig, axes = plt.subplots(2, 1, figsize=(14, 8),
gridspec_kw={'height_ratios': [3, 1]})
# 资金曲线
axes[0].plot(df['close_time'], df['cum_pnl'],
color='#2196F3', linewidth=1.5)
axes[0].fill_between(df['close_time'], df['cum_pnl'],
alpha=0.15, color='#2196F3')
axes[0].set_title('模拟盘资金曲线', fontsize=14)
axes[0].set_ylabel('累计净盈亏')
axes[0].grid(True, alpha=0.3)
# 回撤
axes[1].fill_between(df['close_time'], df['drawdown'], 0,
color='#F44336', alpha=0.5)
axes[1].set_title('回撤曲线', fontsize=12)
axes[1].set_ylabel('回撤')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(save_path, dpi=150)
plt.close()
plot_equity_curve(trades)
3.2 资金曲线的形态判断
看资金曲线不只是看涨跌,要关注形态:
- 平滑上升:策略稳定,回撤可控
- 阶梯式上升:策略有周期性,某些行情下表现好
- 高位震荡:策略可能已经失效,需要警惕
- 持续新低:策略逻辑有问题,应立即停止
可以用线性回归斜率来量化资金曲线的趋势:
python
from scipy import stats as sp_stats
def equity_trend(df):
"""用线性回归斜率衡量资金曲线趋势"""
x = np.arange(len(df))
y = df['cum_pnl'].values
slope, intercept, r_value, p_value, std_err = sp_stats.linregress(x, y)
return {
'slope': round(slope, 4),
'r_squared': round(r_value**2, 4),
'p_value': round(p_value, 6)
}
trend = equity_trend(trades)
print(f"资金曲线斜率: {trend['slope']} (每笔交易平均盈亏)")
print(f"R²: {trend['r_squared']}")
斜率代表每笔交易的平均贡献,R²代表资金曲线的稳定性。R²越高,说明盈利越稳定。
四、月度复盘报告
4.1 按月分组统计
python
def monthly_report(df):
df = df.copy()
df['month'] = df['close_time'].dt.to_period('M')
monthly = df.groupby('month').agg(
trades=('net_pnl', 'count'),
total_pnl=('net_pnl', 'sum'),
avg_pnl=('net_pnl', 'mean'),
win_count=('net_pnl', lambda x: (x > 0).sum()),
max_win=('net_pnl', 'max'),
max_loss=('net_pnl', 'min'),
avg_hold=('hold_minutes', 'mean'),
).reset_index()
monthly['win_rate'] = (monthly['win_count'] / monthly['trades']).round(4)
monthly['total_pnl'] = monthly['total_pnl'].round(2)
monthly['avg_pnl'] = monthly['avg_pnl'].round(2)
return monthly
monthly = monthly_report(trades)
print(monthly.to_string(index=False))
4.2 月度对比可视化
python
def plot_monthly_comparison(monthly_df, save_path='monthly_report.png'):
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
months = monthly_df['month'].astype(str)
# 月度盈亏
colors = ['#4CAF50' if x > 0 else '#F44336' for x in monthly_df['total_pnl']]
axes[0].bar(months, monthly_df['total_pnl'], color=colors)
axes[0].set_title('月度净盈亏')
axes[0].tick_params(axis='x', rotation=45)
axes[0].axhline(y=0, color='black', linewidth=0.8)
# 月度胜率
axes[1].plot(months, monthly_df['win_rate'],
marker='o', color='#2196F3', linewidth=2)
axes[1].axhline(y=0.5, color='gray', linestyle='--', alpha=0.5)
axes[1].set_title('月度胜率')
axes[1].set_ylim(0, 1)
axes[1].tick_params(axis='x', rotation=45)
# 月度交易次数
axes[2].bar(months, monthly_df['trades'], color='#FF9800', alpha=0.7)
axes[2].set_title('月度交易次数')
axes[2].tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.savefig(save_path, dpi=150)
plt.close()
plot_monthly_comparison(monthly)
4.3 复盘报告模板
每月复盘时,按以下结构输出:
python
def generate_monthly_review(df, target_month):
"""生成指定月份的复盘报告"""
df['month'] = df['close_time'].dt.to_period('M')
mdf = df[df['month'] == target_month]
if len(mdf) == 0:
return f"{target_month} 无交易记录"
stats = calc_basic_stats(mdf)
max_win_streak, max_loss_streak = calc_streak(mdf)
# 分方向统计
long_trades = mdf[mdf['direction'] == 'long']
short_trades = mdf[mdf['direction'] == 'short']
report = f"""
{'='*50}
{target_month} 月度复盘报告
{'='*50}
【整体表现】
交易次数: {stats['总交易数']}
净盈亏: {stats['总净盈亏']}
胜率: {stats['胜率']*100:.1f}%
盈亏比: {stats['盈亏比']}
单笔期望值: {stats['单笔期望值']}
【风险指标】
最大连胜: {max_win_streak}
最大连亏: {max_loss_streak}
最大单笔盈利: {stats['最大单笔盈利']}
最大单笔亏损: {stats['最大单笔亏损']}
【分方向】
多头: {len(long_trades)}笔, 盈亏 {long_trades['net_pnl'].sum():.2f}
空头: {len(short_trades)}笔, 盈亏 {short_trades['net_pnl'].sum():.2f}
【持仓分析】
平均持仓时长: {mdf['hold_minutes'].mean():.0f} 分钟
盈利单平均持仓: {mdf[mdf['net_pnl']>0]['hold_minutes'].mean():.0f} 分钟
亏损单平均持仓: {mdf[mdf['net_pnl']<0]['hold_minutes'].mean():.0f} 分钟
"""
return report
# 使用示例
print(generate_monthly_review(trades, pd.Period('2024-01', 'M')))
五、策略表现趋势追踪
单月数据可能有偶然性,需要追踪指标的月度变化趋势,判断策略是否在衰减。
python
def track_performance_trend(df):
"""追踪策略关键指标的月度趋势"""
df = df.copy()
df['month'] = df['close_time'].dt.to_period('M')
monthly = df.groupby('month').apply(
lambda x: pd.Series({
'win_rate': (x['net_pnl'] > 0).mean(),
'avg_win': x[x['net_pnl'] > 0]['net_pnl'].mean() if (x['net_pnl'] > 0).any() else 0,
'avg_loss': abs(x[x['net_pnl'] < 0]['net_pnl'].mean()) if (x['net_pnl'] < 0).any() else 0,
'expectancy': x['net_pnl'].mean(),
'trade_count': len(x),
})
).reset_index()
monthly['pl_ratio'] = monthly['avg_win'] / monthly['avg_loss'].replace(0, np.nan)
# 计算滚动均值(3个月)
for col in ['win_rate', 'pl_ratio', 'expectancy']:
monthly[f'{col}_ma3'] = monthly[col].rolling(3, min_periods=1).mean()
return monthly
trend_df = track_performance_trend(trades)
print(trend_df.to_string(index=False))
5.1 衰减信号识别
python
def detect_decay(trend_df, lookback=3):
"""检测策略是否出现衰减"""
if len(trend_df) < lookback + 1:
return "数据不足,无法判断"
recent = trend_df.tail(lookback)
signals = []
# 期望值连续为负
if (recent['expectancy'] < 0).all():
signals.append("⚠️ 近{}个月期望值持续为负".format(lookback))
# 胜率持续下降
if len(trend_df) >= lookback * 2:
prev_wr = trend_df['win_rate'].iloc[-lookback*2:-lookback].mean()
curr_wr = recent['win_rate'].mean()
if curr_wr < prev_wr * 0.8:
signals.append(f"⚠️ 胜率下降超过20% ({prev_wr:.2%} → {curr_wr:.2%})")
# 盈亏比恶化
if len(trend_df) >= lookback * 2:
prev_pl = trend_df['pl_ratio'].iloc[-lookback*2:-lookback].mean()
curr_pl = recent['pl_ratio'].mean()
if curr_pl < prev_pl * 0.7:
signals.append(f"⚠️ 盈亏比恶化 ({prev_pl:.2f} → {curr_pl:.2f})")
if not signals:
return "✅ 策略表现正常,未检测到明显衰减"
return "\n".join(signals)
print(detect_decay(trend_df))
5.2 按品种/方向拆分
如果策略同时交易多个品种,需要拆分统计,找出哪些品种在赚钱、哪些在亏钱:
python
def breakdown_by_symbol(df):
"""按品种拆分统计"""
result = df.groupby('symbol').agg(
trades=('net_pnl', 'count'),
total_pnl=('net_pnl', 'sum'),
win_rate=('net_pnl', lambda x: (x > 0).mean()),
avg_pnl=('net_pnl', 'mean'),
).round(4).sort_values('total_pnl', ascending=False)
return result
symbol_stats = breakdown_by_symbol(trades)
print(symbol_stats.to_string())
如果一个品种连续三个月亏损,考虑是否要把它从策略池中剔除。如果某个品种贡献了80%的利润,要警惕过度集中。
六、自动化复盘流程
把上面的逻辑串起来,每月初自动生成上月复盘:
```python
def auto_monthly_review(filepath):
"""一键生成月度复盘"""
trades = load_trades(filepath)
trades = calc_drawdown_series(trades)
# 上月
last_month = (datetime.now() - pd.DateOffset(months=1)).to_period('
⭐ 今日观察列表
以下标的来自多周期趋势扫描系统(Gate.io合约),仅供技术分析学习参考,不构成任何投资建议。读者请自行判断。
数据更新时间:2026-10-06 09:01:31
| 标的 | 方向 | 信号类型 | 现价 | 参考止盈 | 参考止损 | 盈亏比 |
|---|---|---|---|---|---|---|
| NEAR 🔼 | 做多 | 高质量标的做多 | $5.2992 | $5.5059 | $5.1932 | 1:1.9 |
共 1 个标的。扫描频率:每15分钟更新。
以上数据由量化扫描系统自动生成,如有兴趣了解更多技术细节,请关注本站后续文章。