
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含300篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
很多数据科学家在从分析脚本转向生产级交易系统时,都会遇到同样的问题:最初的脚本往往是单体式的,依赖全局状态,当需要处理多资产、模拟交易成本或者整夜运行时,这些脚本就会崩溃。
本文将带你从零开始,使用面向对象编程(OOP)的思想,构建一个高度解耦、有状态的量化回测系统。这个系统能够在股票池中运行复杂策略,同时精确模拟资金部署和交易摩擦成本。
最终,我们的 50/20 均线交叉策略取得了 14.84% 的年化收益率(CAGR),略微跑赢了基准的 14.82%。
整个回测流程分为五个核心模块,每个模块都封装在一个独立的类中:
这种严格的依赖倒置设计,确保了数据处理逻辑与信号逻辑的完全解耦。
量化回测中一个常见的陷阱是混淆收盘价(Close)和复权收盘价(Adjusted Close)。我们的 DataManager 类强制使用 auto_adjust=False 获取原始数据,然后显式选择 Adj Close 列。
class DataManager: def _download_data(self) -> pd.DataFrame: """下载数据,使用 auto_adjust=False 以便显式管理复权价格""" tickers = self.config.TICKERS + [self.config.BENCHMARK_TICKER] # 关键:设置 auto_adjust=False,后续手动选择 Adj Close data = yf.download( tickers, start=self.config.START_DATE, end=self.config.CURRENT_DATE, interval="1d", auto_adjust=False, progress=False ) return data def _clean_and_structure_data(self, raw_data: pd.DataFrame) -> pd.DataFrame: """清洗数据并转换为干净的结构:日期为索引,股票代码为列""" # 关键步骤:只选择复权收盘价 price_df = raw_data['Adj Close'].copy() price_df.index.name = 'Date' # 检查缺失值并使用前向填充 if price_df.isnull().values.any(): logger.warning( f"DataManager 发现 {price_df.isnull().sum().sum()} 个缺失值,正在前向填充..." ) price_df = price_df.ffill() return price_df在信号生成环节,最重要的是避免前视偏差(Lookahead Bias)。交叉信号只有在交叉确实发生并在交易日收盘时得到确认后才能生成。我们通过 shift(1) 方法来实现这一点。
class MomentumStrategy(StrategyBase): def generate_signals(self) -> pd.DataFrame: """生成交易信号,严格避免前视偏差""" for ticker in self.price_data.columns: prices = self.price_data[ticker] # 计算短期和长期均线 short_ma = prices.rolling(window=self.short_window).mean() long_ma = prices.rolling(window=self.long_window).mean() # 信号生成逻辑:使用 shift(1) 确保基于前一日指标状态 # 买入信号:短期均线从下方突破长期均线 buy_signal = (short_ma.shift(1) < long_ma.shift(1)) & (short_ma > long_ma) # 卖出信号:短期均线从上方跌破长期均线 sell_signal = (short_ma.shift(1) > long_ma.shift(1)) & (short_ma < long_ma) signals = pd.Series(0, index=prices.index) signals[buy_signal] = 1 signals[sell_signal] = -1 # 持仓传递:保持仓位直到下一个明确信号 signals = signals.replace(0, method='ffill').fillna(0) # 最终步骤:将信号再偏移一天 # 确保 T 日生成的信号在 T+1 日执行 return final_signals.shift(1).fillna(0)Portfolio 类管理系统状态,包括现金、当前持仓和交易记录。关键的结构性检查是成本建模,交易佣金(0.1%)会立即计入现金流计算。
class Portfolio: def execute_trade(self, date: pd.Timestamp, ticker: str, signal: int, price: float): """根据信号执行交易""" if signal == 1: # 买入 # 计算可买入的股数 # 佣金计入总支出 cost = shares_to_trade * price * (1 + self.commission) if cost > self.cash: # 资金不足时重新计算 shares_to_trade = int(self.cash / (price * (1 + self.commission))) if shares_to_trade == 0: return cost = shares_to_trade * price * (1 + self.commission) self.cash -= cost elif signal == -1: # 卖出 # 佣金从收入中扣除 proceeds = shares_to_trade * price * (1 - self.commission) self.cash += proceedsBacktestEngine 遍历每个交易日,先根据当日收盘价计算投资组合价值,记录权益曲线,然后执行当日的交易信号。
class BacktestEngine: def run(self): """运行回测""" logger.info("--- 阶段 2:回测执行 ---") signals = self.strategy.generate_signals() for date in trading_dates: current_prices = self.price_data.loc[date].to_dict() # 1. 投资组合估值 equity = self.portfolio.get_equity(current_prices, date) self.history.loc[date, 'TotalEquity'] = equity # 2. 信号应用与执行 day_signals = signals.loc[date] for ticker in self.config.TICKERS: signal = day_signals.get(ticker, 0) price = current_prices.get(ticker) if price is not None and signal != 0: # 基于当日收盘价执行 self.portfolio.execute_trade(date, ticker, signal, price) logger.info(f"回测完成,共模拟 {len(trading_dates)} 个交易日")PerformanceEvaluator 类将原始收益转换为可操作的统计指标,包括年化收益率、夏普比率和胜率。
class PerformanceEvaluator: def evaluate(self): """评估策略表现""" logger.info("--- 阶段 3:绩效评估 ---") strategy_equity = self.equity_curve['TotalEquity'] # 计算策略收益 strategy_daily_returns, strategy_cagr = self._calculate_returns(strategy_equity) # 计算交易指标 total_trades = len(self.trades_df) winning_trades = len(self.trades_df[self.trades_df['Cash_Change'] > 0]) self.results = { "CAGR (%)": strategy_cagr * 100, "Total Trades": total_trades, "Winning Trades": winning_trades, "Win Rate (%)": (winning_trades / total_trades) * 100 if total_trades > 0 else 0, "Benchmark CAGR (%)": bench_cagr * 100, "Outperformance vs Benchmark (%)": (strategy_cagr - bench_cagr) * 100 }最大回撤(Maximum Drawdown)是衡量风险的关键指标,通过比较当前权益与历史峰值来计算。
class PerformanceEvaluator: def evaluate(self): # 计算最大回撤 cumulative_returns = strategy_equity / strategy_equity.cummax() mdd = (cumulative_returns.min() - 1) * 100 # 转换为百分比运行完整回测后,我们得到以下关键指标:
4.84% 的胜率是最值得关注的统计数据。每 100 笔交易中,只有不到 5 笔是净盈利的。策略之所以能够成功,是因为少数盈利交易的收益足够大,能够覆盖 95% 亏损交易的累计损失以及交易佣金。
这个框架是一个稳健的起点。要迈向生产级系统,可以从以下几个方向进行优化:
本文通过面向对象编程的思想,构建了一个完整的多资产量化回测系统。系统将数据获取、信号生成、交易执行和绩效评估解耦为独立的模块,这种模块化设计是系统最大的优势,允许进行有针对性的安全迭代。
50/20 均线交叉策略在 2020 年至 2025 年间取得了 14.84% 的年化收益率,略微跑赢基准。然而,4.84% 的胜率揭示了策略的结构性约束:它本质上是嘈杂的,依赖少数大赢来覆盖众多小亏损的成本。26.15% 的最大回撤表明,即使净收益与基准持平,这种方法的固有波动性也需要相当的心理承受能力。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐