模拟盘数据统计与月度复盘方法
模拟盘跑了一段时间,交易记录攒了几百条,但很多人只是看看总盈亏就结束了。这样跑模拟盘的意义不大——真正有价值的是从交易记录中提取统计指标,做系统性复盘,找到策略的衰减点和改进方向。
这篇文章分享一套完整的模拟盘数据统计与月度复盘方法,包含可复用的Python代码。
一、数据准备
假设你的模拟盘交易记录已经导出为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 ...先做统一的数据加载和预处理:
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的策略。
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 最大连亏与最大回撤
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 资金曲线绘制
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 资金曲线的形态判断
看资金曲线不只是看涨跌,要关注形态:
- 平滑上升:策略稳定,回撤可控
- 阶梯式上升:策略有周期性,某些行情下表现好
- 高位震荡:策略可能已经失效,需要警惕
- 持续新低:策略逻辑有问题,应立即停止
可以用线性回归斜率来量化资金曲线的趋势:
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 按月分组统计
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 月度对比可视化
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 复盘报告模板
每月复盘时,按以下结构输出:
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')))五、策略表现趋势追踪
单月数据可能有偶然性,需要追踪指标的月度变化趋势,判断策略是否在衰减。
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 衰减信号识别
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 按品种/方向拆分
如果策略同时交易多个品种,需要拆分统计,找出哪些品种在赚钱、哪些在亏钱:
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分钟更新。
以上数据由量化扫描系统自动生成,如有兴趣了解更多技术细节,请关注本站后续文章。