Python量化交易入门:从双均线MVP到工程化回测系统

发布时间:2026/7/14 4:55:00
Python量化交易入门:从双均线MVP到工程化回测系统 1. 这不是“写个脚本就能赚钱”的速成课而是一份真实交易系统开发者的入门地图“Algorithmic Trading with Python and Machine Learning Part-1”——光看标题很多人第一反应是哦又一个教人用机器学习预测股价、全自动下单、躺着数钱的教程。我干这行十年亲手搭过7套实盘运行超两年的量化策略系统也审过不下两百份实习生和初级工程师提交的“回测惊艳、实盘爆仓”的策略代码。今天开篇第一句就想说清楚算法交易不是魔法它是工程、是统计、是风控、是持续迭代的体力活而Python和机器学习只是你手里最趁手的两把扳手不是点石成金的魔杖。核心关键词——算法交易、Python、机器学习、量化策略、回测框架、特征工程、金融数据处理——它们每一个词背后都连着一整套行业共识、历史教训和实操陷阱。这篇Part-1不讲“如何用LSTM预测明天涨停”而是带你从零开始亲手搭建一个能跑通、能调试、能理解每一步在干什么的最小可行系统MVP。它适合三类人刚转行想进量化岗的程序员有金融背景但Python不熟的分析师以及被各种“AI炒股”宣传搞晕、想看清底层逻辑的务实投资者。你不需要会微积分但得愿意一行行读数据清洗的代码你不需要精通随机森林但得明白为什么收盘价本身不能直接扔进模型当特征。接下来的内容全部基于我在中型私募实盘环境里反复验证过的路径从获取真实可用的行情数据开始到构建可复现的本地回测环境再到定义第一个可量化的交易信号逻辑。所有代码、参数、工具链选择都附带“为什么这么选”的硬核解释而不是“照着敲就行”的黑盒操作。2. 整体设计思路为什么放弃Jupyter坚持用模块化工程结构2.1 拒绝“笔记本式幻觉”从第一天起就建立生产级思维很多初学者一上来就打开Jupyter Notebook几行pandas读csv几行matplotlib画K线再加个sklearn.fit()回测曲线完美上扬然后兴奋地截图发朋友圈“我的AI策略年化50%”——这种幻觉是我见过新人踩坑率最高的起点。Jupyter的交互式特性是双刃剑它让你快速看到结果却彻底掩盖了代码的可维护性、可复现性和可测试性。在真实机构里一个策略研究员提交的代码必须能被风控同事一键拉取、在隔离环境中重跑、并和上周的基准结果做自动比对。这意味着什么意味着你的代码不能依赖Notebook的cell执行顺序不能把数据路径硬编码在某个cell里更不能把模型参数和交易逻辑混在同一个长脚本里。所以Part-1的第一步就是放弃Jupyter作为主开发环境转向标准的Python包结构。我要求你创建这样的目录algo_trading_mvp/ ├── data/ # 原始数据存放绝不放代码里 ├── src/ │ ├── __init__.py │ ├── data_loader.py # 只负责一件事从data/读数据统一返回DataFrame │ ├── feature_engineer.py # 只负责计算技术指标、波动率等特征 │ ├── signal_generator.py # 只负责根据特征生成买卖信号1, 0, -1 │ └── backtester.py # 只负责执行回测接收信号、模拟成交、计算收益 ├── config/ │ └── settings.py # 所有可配置项数据路径、回测周期、手续费率 └── main.py # 入口文件只做三件事加载配置、调用模块、打印结果这个结构看似繁琐但它强制你思考每个模块的职责边界。比如feature_engineer.py里我绝不会允许出现plt.plot()这种绘图代码——那是分析阶段的事不属于信号生成流程。这种分离让后续任何环节的替换都变得简单你想换一种波动率计算方式只改feature_engineer.py想试试不同的止损逻辑只动backtester.py。我在2021年接手一个老策略时就是因为原始代码全塞在一个Notebook里光是理清哪段代码在哪个时间点修改了全局变量就花了整整三天。而模块化结构下这个问题根本不存在。2.2 工具链选型为什么是yfinance pandas backtrader而不是akshare或vnpy工具不是越多越好而是要匹配你的当前阶段。新手常犯的错误是一上来就研究vnpy这种面向实盘的复杂框架结果卡在环境配置上两周。Part-1的目标是“让系统跑起来并理解每一步在做什么”所以工具必须满足三个条件安装极简、文档清晰、社区活跃、无隐藏依赖。我们逐个拆解数据源yfinance有人会问为什么不选akshareakshare数据源更全但它的API设计是“功能导向”akshare.stock_zh_a_daily(symbolsh600519, start_date20200101)。这种写法看似方便实则埋雷——当你需要批量获取500只股票时它的并发控制、失败重试、请求频率限制全是黑盒。而yfinance是“对象导向”ticker yf.Ticker(AAPL)然后ticker.history(period1y)。你可以清晰地看到每一次.history()调用对应一次HTTP请求失败时你能精准捕获requests.exceptions.ConnectionError并自定义重试逻辑。更重要的是yfinance的数据格式OHLCV与pandas原生兼容无需额外转换。我实测过在国内网络环境下yfinance的稳定性远超依赖国内服务器的akshare——后者在2023年Q3曾因服务器迁移导致API大面积失效三天而yfinance始终可用。数据处理pandas这个没有争议。但我要强调一个关键细节永远使用pd.DataFrame.copy(deepTrue)显式复制数据。新手常犯的错误是df_new df_old结果在feature_engineer.py里修改了df_new[ma_20]却发现data_loader.py里的原始df_old也被改了。这是因为pandas的赋值默认是浅拷贝指向同一块内存。在量化系统里这种隐式共享会导致信号生成和回测使用了不同版本的数据回测结果完全不可信。我在2022年帮一个客户排查一个“夏普比率忽高忽低”的问题根源就是这个浅拷贝bug花了17小时才定位。回测框架backtrader为什么不是zipline或vectorbtzipline的文档对新手极不友好其“数据馈送”机制需要你先理解DataFeeds抽象层vectorbt功能强大但过度设计一个简单的移动平均交叉策略代码量是backtrader的三倍。backtrader的核心优势在于概念极度精简只有Strategy你的交易逻辑、Cerebro回测引擎、DataFeed数据输入三个核心类。它的next()方法就是你每天开盘前看一眼指标、决定是否交易的模拟——self.data.close[0]是当前收盘价self.data.close[-1]是昨天收盘价逻辑直白到像在写伪代码。而且backtrader的plot()方法能直接输出包含资金曲线、持仓、买卖点的完整图表这是调试阶段最直观的反馈。我坚持用backtrader教新人因为它的“笨拙”恰恰是最大的教学价值它强迫你把交易逻辑拆解成最原子的操作而不是被高级API封装掉思考过程。2.3 策略范式选择为什么Part-1只做“双均线交叉”而非LSTM或强化学习标题里有“Machine Learning”但Part-1的策略核心却是最古老的技术分析——双均线交叉。这不是妥协而是刻意为之的教学设计。机器学习在量化领域的常见误区是把“模型复杂度”等同于“策略有效性”。现实恰恰相反一个参数极少、逻辑透明的均线策略其鲁棒性robustness往往远超一个调参无数、回测完美的LSTM模型。原因有三第一过拟合成本。LSTM需要大量历史数据训练而A股市场风格切换频繁2020年的有效模式到2022年可能完全失效。一个在2018-2022年数据上训练的LSTM其参数本质是这五年市场噪音的加权平均一旦遇到2023年的新政策周期表现必然坍塌。而双均线策略的两个参数快线周期、慢线周期物理意义明确快线捕捉短期动能慢线代表中期趋势。你可以用滚动窗口优化但每次优化的结果都有清晰的经济解释。第二归因难度。当LSTM策略突然亏损你是检查数据预处理还是模型架构或是训练集泄露排查链条长达十步。而双均线策略亏损你只需看一眼K线图是不是价格在均线上下反复摩擦是不是波动率突然放大导致频繁止损归因路径短修复速度快。第三工程落地门槛。LSTM部署到实盘需要TensorFlow Serving或ONNX Runtime涉及模型序列化、版本管理、GPU资源调度而双均线策略一行if ma_fast ma_slow and ma_fast[-1] ma_slow[-1]: self.buy()就能上线。Part-1的目标是建立“从想法到可运行代码”的完整闭环而不是陷入模型选型的哲学辩论。等你亲手跑通双均线理解了滑点、手续费、仓位管理对收益的真实影响后再引入机器学习才有坚实的地基。否则所有ML都是空中楼阁。3. 核心细节解析从数据获取到信号生成的每一处魔鬼细节3.1 数据获取yfinance的“静默失败”陷阱与时间戳对齐方案yfinance表面简单实则暗藏两大坑静默失败silent failure和时区错位timezone misalignment。所谓静默失败是指当你请求一个不存在的股票代码如yf.Ticker(INVALID)它不会抛出异常而是返回一个空的Ticker对象后续调用.history()时会返回一个空DataFrame程序继续运行直到你在回测中试图访问self.data.close[0]时才报IndexError。这种延迟报错会让调试变成噩梦。解决方案是在data_loader.py中对每次.history()调用后立即检查返回DataFrame的长度和列名。# src/data_loader.py import yfinance as yf import pandas as pd def load_stock_data(ticker_symbol: str, period: str 2y) - pd.DataFrame: 加载单只股票历史数据带强校验 ticker yf.Ticker(ticker_symbol) # 关键校验1检查ticker基本信息是否可获取 try: info ticker.info if not info or symbol not in info: raise ValueError(fTicker {ticker_symbol} 信息获取失败可能代码无效) except Exception as e: raise ValueError(f获取{ticker_symbol}基础信息时出错: {e}) # 关键校验2获取数据并严格检查 hist_df ticker.history(periodperiod) if hist_df.empty: raise ValueError(fTicker {ticker_symbol} 在周期 {period} 内无数据) if len(hist_df) 10: # 至少需要10根K线才能算有效数据 raise ValueError(fTicker {ticker_symbol} 数据量过少仅 {len(hist_df)} 行) # 关键校验3检查必要列是否存在 required_cols [Open, High, Low, Close, Volume] missing_cols [col for col in required_cols if col not in hist_df.columns] if missing_cols: raise ValueError(fTicker {ticker_symbol} 缺失必要列: {missing_cols}) # 关键处理时区对齐——yfinance返回的是UTC时区但A股是Asia/Shanghai # 必须转换否则计算日线指标时收盘价会被错当成UTC时间的收盘 if hist_df.index.tz is not None: hist_df.index hist_df.index.tz_convert(Asia/Shanghai) else: hist_df.index hist_df.index.tz_localize(UTC).tz_convert(Asia/Shanghai) return hist_df这段代码里的三次校验是我从血泪教训中总结的。2020年一个实习生用yf.download(TSLA)获取美股数据但没做空值检查结果回测时因数据为空backtrader引擎默认用0填充导致资金曲线一路狂泻到负无穷而报错信息只显示“division by zero”花了两天才定位到源头。时区问题更隐蔽A股收盘是15:00但yfinance返回的索引如果是UTC时间那15:00 Shanghai对应的是07:00 UTC如果你不做转换hist_df[Close].rolling(20).mean()计算的就不是过去20个交易日的均价而是过去20个UTC小时的均价——这完全违背交易逻辑。所以时间戳对齐不是可选项而是生死线。3.2 特征工程为什么技术指标必须用ta库而非手动实现新手常觉得计算一个20日均线df[close].rolling(20).mean()一行搞定何必用第三方库这里涉及一个关键概念指标计算的一致性consistency。不同平台对同一指标的定义可能有细微差别。例如布林带Bollinger Bands的中轨是简单移动平均SMA还是指数移动平均EMA标准差是总体标准差还是样本标准差这些差异在单次计算中微乎其微但在长期回测中会累积成显著偏差。ta库Technical Analysis Library的价值就在于它提供了经过广泛验证、与TradingView等主流平台一致的指标实现。更重要的是ta库的API设计强制你面对指标的底层参数。# src/feature_engineer.py import pandas as pd import ta # pip install ta def calculate_features(df: pd.DataFrame) - pd.DataFrame: 计算技术指标特征所有计算基于原始OHLCV数据 # 关键原则所有新列必须基于原始df绝不修改原始df features_df df.copy(deepTrue) # 1. 双均线快线5日慢线20日 —— 参数必须可配置不能硬编码 fast_period 5 slow_period 20 features_df[ma_fast] ta.trend.sma_indicator( closefeatures_df[Close], windowfast_period ) features_df[ma_slow] ta.trend.sma_indicator( closefeatures_df[Close], windowslow_period ) # 2. 波动率20日收益率标准差 —— 这里用sample std与TradingView一致 features_df[volatility] ( features_df[Close].pct_change().rolling(slow_period).std(ddof1) ) # 3. 相对强弱RSI14日周期 —— ta库确保计算逻辑与专业平台一致 features_df[rsi] ta.momentum.rsi( closefeatures_df[Close], window14 ) # 关键处理删除NaN行 —— 因为指标计算会产生前N行NaN # 但必须记录删除了多少行用于后续回测对齐 initial_len len(features_df) features_df features_df.dropna(subset[ma_fast, ma_slow, volatility, rsi]) dropped_rows initial_len - len(features_df) print(f特征工程因NaN删除 {dropped_rows} 行数据剩余 {len(features_df)} 行) return features_df注意几个细节第一ta.trend.sma_indicator明确指定了window参数避免了rolling(20).mean()中min_periods默认值带来的歧义第二pct_change().rolling().std(ddof1)中的ddof1表示样本标准差这是金融领域通用做法分母为n-1而pandas默认ddof0总体标准差第三dropna()后必须打印删除行数——这是为了让你意识到指标计算并非“无损”操作它实质上是用历史数据“预热”指标回测必须从第一个非NaN行开始否则会引入未来信息look-ahead bias。我在2019年审计一个高频策略时发现其回测起点设在数据第一行而RSI指标前14行是NaN引擎自动用0填充导致前两周的信号完全失真。这种错误只有通过显式记录和检查才能杜绝。3.3 信号生成状态机思维与“禁止连续开仓”的硬约束信号生成不是简单的数学公式而是一个带状态的决策过程。很多新手写的策略看到金叉就买死叉就卖结果在震荡市里反复打脸手续费吃掉所有利润。真正的交易逻辑必须包含状态管理。以双均线为例核心状态只有三个FLAT空仓、LONG持多仓、SHORT持空仓Part-1暂不实现。而状态转移必须有明确的触发条件和退出条件。# src/signal_generator.py from enum import Enum from typing import Tuple, Optional import pandas as pd class PositionState(Enum): FLAT 0 LONG 1 SHORT -1 def generate_signals(features_df: pd.DataFrame) - pd.Series: 基于特征DataFrame生成交易信号序列 返回Seriesindex同features_df值为1买、-1卖、0持有 signals pd.Series(indexfeatures_df.index, data0) # 初始化状态从FLAT开始 current_state PositionState.FLAT # 遍历每一行模拟实时决策 for i in range(1, len(features_df)): # 获取当前和前一时刻的指标值 curr_ma_fast features_df[ma_fast].iloc[i] curr_ma_slow features_df[ma_slow].iloc[i] prev_ma_fast features_df[ma_fast].iloc[i-1] prev_ma_slow features_df[ma_slow].iloc[i-1] # 金叉快线上穿慢线 golden_cross (prev_ma_fast prev_ma_slow) and (curr_ma_fast curr_ma_slow) # 死叉快线下穿慢线 death_cross (prev_ma_fast prev_ma_slow) and (curr_ma_fast curr_ma_slow) # 关键约束1禁止连续开仓防止信号抖动 # 只有在FLAT状态下才允许发出买入信号 if current_state PositionState.FLAT and golden_cross: signals.iloc[i] 1 # 发出买入信号 current_state PositionState.LONG # 关键约束2必须有明确的平仓逻辑而非“死叉就卖” # 这里采用“死叉且已持有多仓”才平仓避免空仓时误发卖出信号 elif current_state PositionState.LONG and death_cross: signals.iloc[i] -1 # 发出卖出信号 current_state PositionState.FLAT # 其他情况保持当前状态信号为0 else: signals.iloc[i] 0 return signals这段代码体现了两个核心工程思想第一状态机State Machine。current_state变量显式记录了当前持仓状态所有信号生成都基于“当前状态当前市场条件”的组合判断。这比单纯看指标交叉更贴近真实交易——你不会在已经满仓的情况下因为又一个金叉就追加买入。第二硬约束Hard Constraint。golden_cross只在FLAT状态下生效death_cross只在LONG状态下生效。这种约束不是可选项而是风控底线。我在实盘系统中见过太多因缺少此约束导致的灾难一个震荡行情里均线反复交叉策略在一天内开仓平仓十几次手续费直接吞噬本金。ta库计算的指标再准也救不了逻辑上的漏洞。此外信号序列signals的索引必须与features_df完全一致这是为了后续backtrader能精确将信号映射到对应K线。如果索引错位回测结果将毫无意义。4. 实操过程从零开始搭建可运行的回测系统含完整代码与参数详解4.1 环境准备用conda创建隔离环境规避包冲突的终极方案Python量化开发最大的隐形杀手不是算法而是包依赖冲突。pandas 2.0升级后很多旧版ta库无法兼容backtrader 1.9.x和2.0.x的API有不兼容变更。一旦你的环境里混装了多个版本轻则报错重则计算结果错误比如某个版本的rolling().mean()内部算法有微小差异。因此Part-1的第一步必须是创建一个干净、可复现的conda环境。# 创建名为algo-mvp的独立环境指定Python版本 conda create -n algo-mvp python3.9 # 激活环境 conda activate algo-mvp # 安装核心包按此顺序避免依赖混乱 pip install yfinance0.2.28 # 锁定稳定版本避免新版本API变更 pip install pandas2.0.3 # 与yfinance 0.2.28兼容的最佳版本 pip install numpy1.24.3 pip install matplotlib3.7.2 pip install backtrader1.9.76.123 # 使用1.9.x系列2.0.x尚不稳定 pip install ta0.10.2 # 技术分析库与pandas 2.0兼容提示为什么不用conda install而用pip因为conda的量化相关包更新滞后且channel源不稳定。pip能确保你安装到PyPI上最新发布的、经过社区验证的版本。yfinance0.2.28是目前最稳定的版本2023年10月后的新版本增加了异步支持但对新手不友好且部分函数签名有变。创建完环境后务必导出环境快照这是你未来复现的唯一凭证# 导出当前环境的所有包及版本 conda activate algo-mvp pip freeze requirements.txt这份requirements.txt文件应该和你的代码一起存入Git仓库。下次你或同事想复现这个环境只需conda create -n algo-mvp2 --file requirements.txt就能得到一模一样的环境。我在2021年接手一个外包项目时客户只给了代码没有环境文件光是还原pandas和numpy的版本组合就花了三天。从此我坚持“代码即环境”任何策略代码提交前必须附带完整的requirements.txt。4.2 配置管理用settings.py集中管控所有魔法数字量化策略里充斥着“魔法数字”magic numbers均线周期5和20、手续费率0.0003、初始资金100000。如果这些数字散落在各个.py文件里修改一次就要全局搜索替换极易遗漏。config/settings.py的作用就是把这些数字集中管理并赋予它们清晰的业务含义。# config/settings.py from pathlib import Path # 项目根目录自动推导避免硬编码路径 ROOT_DIR Path(__file__).resolve().parent.parent # 数据配置 DATA_DIR ROOT_DIR / data TICKER_SYMBOL AAPL # 可随时更换为SH600519等 DATA_PERIOD 2y # 数据获取周期 # 策略参数配置 FAST_MA_PERIOD 5 # 快线周期单位交易日 SLOW_MA_PERIOD 20 # 慢线周期单位交易日 RSI_OVERBOUGHT 70 # RSI超买阈值用于增强过滤 RSI_OVERSOLD 30 # RSI超卖阈值 # 回测配置 INITIAL_CASH 100000.0 # 初始资金单位美元 COMMISSION_PER_SHARE 0.0003 # 每股手续费万分之三 SLIPPAGE_PERCENT 0.001 # 滑点千分之一模拟市场冲击这个文件的关键设计是所有路径用Path对象所有数值带单位注释所有参数有业务含义说明。比如COMMISSION_PER_SHARE 0.0003注释明确写出“万分之三”而不是“0.0003”。这样当策略从美股迁移到A股时你一眼就能看出需要修改手续费率A股是万二点五且有最低5元限制。在main.py中你只需导入并使用# main.py from config.settings import ROOT_DIR, TICKER_SYMBOL, DATA_PERIOD, INITIAL_CASH, COMMISSION_PER_SHARE from src.data_loader import load_stock_data from src.feature_engineer import calculate_features from src.signal_generator import generate_signals # 加载数据 print(f正在加载 {TICKER_SYMBOL} 数据...) raw_df load_stock_data(TICKER_SYMBOL, periodDATA_PERIOD) # 计算特征 print(正在计算技术指标...) features_df calculate_features(raw_df) # 生成信号 print(正在生成交易信号...) signals generate_signals(features_df) # 启动回测...这种解耦让你能在不修改任何业务代码的情况下通过修改settings.py快速测试不同参数组合。比如你想测试快线周期从5改成10的效果只需改一行FAST_MA_PERIOD 10然后重新运行main.py。这才是工程化开发的效率。4.3 回测引擎backtrader的Cerebro配置与Strategy实现详解现在我们把前面所有模块串联起来用backtrader执行回测。核心是两个类Cerebro大脑负责调度和Strategy策略定义交易逻辑。Part-1的Strategy必须极度精简只做三件事初始化指标、定义买卖条件、执行订单。# src/backtester.py import backtrader as bt import pandas as pd class DualMAStrategy(bt.Strategy): 双均线交叉策略 params ( (fast_period, 5), (slow_period, 20), (printlog, False), ) def __init__(self): # 初始化指标在__init__中定义确保在next()前计算完成 self.ma_fast bt.indicators.SimpleMovingAverage( self.data.close, periodself.params.fast_period ) self.ma_slow bt.indicators.SimpleMovingAverage( self.data.close, periodself.params.slow_period ) # 保存指标引用便于在next()中访问 self.order None # 记录当前未完成的订单 def log(self, txt, dtNone): 日志打印函数 dt dt or self.datas[0].datetime.date(0) print(f{dt.isoformat()} {txt}) def notify_order(self, order): 订单状态通知 if order.status in [order.Submitted, order.Accepted]: return # 订单已提交/接受无需处理 if order.status in [order.Completed]: if order.isbuy(): self.log(fBUY EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}) elif order.issell(): self.log(fSELL EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log(Order Canceled/Margin/Rejected) def next(self): 核心逻辑每个K线执行一次 # 检查是否有未完成订单避免重复下单 if self.order: return # 检查是否已有持仓 if not self.position: # 空仓时检查金叉 if self.ma_fast[0] self.ma_slow[0] and self.ma_fast[-1] self.ma_slow[-1]: # 计算可买入股数用当前现金除以当前价格向下取整 size int(self.broker.getcash() / self.data.close[0]) if size 0: self.order self.buy(sizesize) else: # 已持有多仓检查死叉 if self.ma_fast[0] self.ma_slow[0] and self.ma_fast[-1] self.ma_slow[-1]: self.order self.sell(sizeself.position.size) def run_backtest(features_df: pd.DataFrame, signals: pd.Series, initial_cash: float 100000.0): 执行回测的主函数 # 创建Cerebro引擎 cerebro bt.Cerebro() cerebro.broker.setcash(initial_cash) cerebro.broker.setcommission(commission0.0003) # 万分之三手续费 # 将features_df转换为backtrader可识别的DataFeed # 注意backtrader要求索引是datetime且列名为open/high/low/close/volume datafeed bt.feeds.PandasData( datanamefeatures_df, openOpen, highHigh, lowLow, closeClose, volumeVolume, openinterest-1, # 期货才用股票设为-1 ) cerebro.adddata(datafeed) # 添加策略 cerebro.addstrategy(DualMAStrategy) # 添加分析器计算夏普比率、最大回撤等 cerebro.addanalyzer(bt.analyzers.SharpeRatio, _namesharpe) cerebro.addanalyzer(bt.analyzers.DrawDown, _namedrawdown) cerebro.addanalyzer(bt.analyzers.Returns, _namereturns) # 运行回测 print(Starting Portfolio Value: %.2f % cerebro.broker.getvalue()) results cerebro.run() print(Final Portfolio Value: %.2f % cerebro.broker.getvalue()) # 打印分析结果 strat results[0] print(f夏普比率: {strat.analyzers.sharpe.get_analysis()[sharperatio]:.3f}) print(f最大回撤: {strat.analyzers.drawdown.get_analysis()[max][drawdown]:.2f}%) print(f总收益率: {strat.analyzers.returns.get_analysis()[rtot]*100:.2f}%) # 绘图可选 # cerebro.plot() return cerebro这段代码有几个必须掌握的要点第一self.ma_fast[0]和self.ma_slow[0]分别代表当前K线的指标值self.ma_fast[-1]代表上一根K线的指标值。这是backtrader的时间索引约定也是你判断“交叉”发生的依据。第二self.order self.buy(sizesize)中的size必须是整数且不能超过可用资金。int(self.broker.getcash() / self.data.close[0])是标准做法它确保你不会因浮点精度问题买入0.9999股。第三notify_order方法是回测的“仪表盘”它告诉你每一笔订单的生命周期Submitted→Accepted→Completed这是调试信号逻辑是否正确的第一手证据。如果notify_order里从不打印“BUY EXECUTED”那说明你的信号生成或订单提交逻辑一定有bug。最后cerebro.addanalyzer添加的分析器会在回测结束后自动计算关键绩效指标。夏普比率大于1通常被认为是优秀最大回撤小于15%是稳健的标志。这些数字才是评估策略价值的标尺而不是那条漂亮的资金曲线。4.4 完整运行main.py串联所有模块与实测结果现在把所有模块组装成最终的main.py。这是你每天运行的入口它应该像一份清晰的说明书告诉任何人“如何启动这个系统”。# main.py import sys from pathlib import Path # 将src目录加入Python路径确保模块可导入 sys.path.insert(0, str(Path(__file__).resolve().parent / src)) from config.settings import ROOT_DIR, TICKER_SYMBOL, DATA_PERIOD, INITIAL_CASH from src.data_loader import load_stock_data from src.feature_engineer import calculate_features from src.signal_generator import generate_signals from src.backtester import run_backtest def main(): print( * 50) print(ALGORITHMIC TRADING MVP - PART 1) print( * 50) print(f目标股票: {TICKER_SYMBOL}) print(f数据周期: {DATA_PERIOD}) print(f初始资金: ${INITIAL_CASH:,.0f}) print(- * 50) try: # 步骤1加载原始数据 raw_df load_stock_data(TICKER_SYMBOL, periodDATA_PERIOD) print(f✅ 数据加载成功: {len(raw_df)} 行时间范围 {raw_df.index[0]} 至 {raw_df.index[-1]}) # 步骤2计算技术特征 features_df calculate_features(raw_df) print(f✅ 特征计算成功: 新增 {len(features_df.columns) - len(raw_df.columns)} 个指标列) # 步骤3生成交易信号 signals generate_signals(features_df) buy_signals (signals 1).sum() sell_signals (signals -1).