简介:本资源是一份面向量化交易初学者与Python数据分析爱好者的实战代码包,聚焦海龟交易法则的完整Python实现,解决趋势跟踪策略从理论到代码落地的关键问题。包内共3个文件:2个CSV格式的历史行情数据(含上证600000等标的的OHLCV字段),1个核心Python脚本(turtle.py),完整封装了数据加载、N日SMA/EMA计算、突破信号识别、动态仓位管理及ATR止损逻辑,开箱即用。压缩包仅41KB,轻量简洁,便于快速导入Jupyter或PyCharm环境调试验证。目前已有1307人学习下载,适合希望掌握pandas处理金融时序数据、理解经典趋势策略工程化细节的学习者。读者可直接复现信号生成与模拟交易流程,并基于内置结构扩展回测统计、可视化盈亏曲线或接入实盘接口。1. 海龟交易法则不是玄学,是可复现、可调试、可落地的入场-出场闭环系统你是不是也见过这类“海龟策略”代码:几十行if套for,收盘价一算就开仓,止损线硬编码成0.02,回测用pandas.DataFrame.shift(1)模拟信号却漏掉滑点和成交延迟?结果一跑实盘,胜率从回测的63%掉到41%,最大回撤翻倍——这不是策略失效,是工程实现没过基本关。这个项目就是为解决这个问题而生:它用纯pandas构建了一套严格对齐原始海龟规则(R-multiple、N值波动、20日突破、10日突破、双止损、资金管理)的端到端实现,不依赖 TA-Lib 或 backtrader 等黑匣子库,所有逻辑暴露在 DataFrame 的列计算中,支持逐笔成交模拟、动态仓位计算、多合约滚动、以及关键的——信号生成与执行严格分离。适合想真正搞懂海龟内核、需要嵌入自有交易框架、或正在被“伪海龟”代码坑得睡不着觉的量化开发者。它不是教学 Demo,而是一份能直接放进你策略引擎 pipeline 的生产级参考实现。2. 项目结构与核心模块拆解:从数据加载到信号生成的完整链路2.1 目录结构与文件职责划分项目采用极简但高内聚的组织方式,共 5 个核心 Python 文件,无任何隐藏配置或魔法路径:turtle_strategy/ ├── data_loader.py # 统一入口:支持 CSV / yfinance / 本地 HDF5,强制返回标准 OHLCV + volume 列名 ├── volatility_calculator.py # N值计算核心:ATR(20) 的 pandas 向量化实现,含 Wilder 平滑逻辑(非简单 mean) ├── signal_generator.py # 主策略逻辑:20日高点突破开仓、10日低点突破加仓、双止损(追踪+固定)、空头对称逻辑 ├── position_manager.py # 仓位控制器:基于 R-multiple 动态计算单位头寸,支持最大风险比例(如2%)、最小合约数限制 └── backtest_engine.py # 回测驱动:按日/按分钟粒度推进,模拟下单、成交、滑点(默认0.1%)、手续费(固定0.0003)、持仓状态维护提示:所有模块均无全局变量,函数式设计,输入为pd.DataFrame,输出为带新列的DataFrame或dict。这意味着你可以单独测试signal_generator.generate_signals(df)而无需启动整个回测。2.2 数据加载与标准化:为什么必须重写data_loader.py很多“海龟策略”失败的第一步,就栽在数据上。原始海龟要求使用真实交易日的最高价/最低价/收盘价,且需处理停牌、复权、除权等干扰。本项目data_loader.py的关键设计在于:强制列名映射:无论输入是High/Low/Close还是high_price/low_price/close_price,统一转为['open', 'high', 'low', 'close', 'volume']复权模式可选:adjust='qfq'(前复权)或'hfq'(后复权),默认关闭(即不复权),因海龟原始规则明确使用未复权价格时间索引强校验:自动检测并转换为datetime64[ns],缺失日期用ffill()补充(但标记is_missing=True列供策略过滤)# data_loader.py 核心片段 def load_from_csv(filepath: str, adjust: str = None) -> pd.DataFrame: df = pd.read_csv(filepath, index_col=0, parse_dates=True) # 列名标准化 col_map = {'Open': 'open', 'High': 'high', 'Low': 'low', 'Close': 'close', 'Volume': 'volume'} df = df.rename(columns={k: v for k, v in col_map.items() if k in df.columns}) # 复权处理(仅当指定且存在前复权列时) if adjust == 'qfq' and 'adj_close' in df.columns: ratio = df['adj_close'] / df['close'] df['open'] = df['open'] * ratio df['high'] = df['high'] * ratio df['low'] = df['low'] * ratio df['close'] = df['adj_close'] return df[['open', 'high', 'low', 'close', 'volume']].sort_index()这段代码的关键参数说明:adjust:控制是否复权,None表示原始价格(推荐用于海龟),避免复权引入未来信息;ratio计算严格按adj_close / close,确保开盘/高低价同步缩放,而非只调收盘价;返回前强制sort_index(),防止时间乱序导致shift()错位——这是新手最常翻车的点。2.3 波动率计算:N值不是ATR,而是Wilder平滑后的真N海龟法则中的 N 值(市场波动单位)不是简单的 ATR(20),而是 Wilder 平滑版本:N[t] = (N[t-1] * 19 + TR[t]) / 20,其中 TR(True Range)= max(High−Low, |High−Close[t−1]|, |Low−Close[t−1]|)volatility_calculator.py用纯 pandas 实现,避免循环,性能比for快 8 倍以上:# volatility_calculator.py def calculate_n_value(df: pd.DataFrame, period: int = 20) -> pd.Series: # 计算 True Range:三者取最大 prev_close = df['close'].shift(1) tr1 = df['high'] - df['low'] tr2 = (df['high'] - prev_close).abs() tr3 = (df['low'] - prev_close).abs() tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) # Wilder 平滑:用 ewm(alpha=1/period) 模拟 (prev * (n-1) + curr) / n # 注意:ewm 默认 min_periods=period,首period行为空,需用 simple mean 填充 n_series = tr.ewm(alpha=1/period, adjust=False).mean() n_series.iloc[:period] = tr.iloc[:period].mean() # 首期用简单均值 return n_series.rename('n_value')逻辑说明:tr计算用pd.concat(...).max(axis=1),向量化,无apply(lambda x: max(...))这种性能黑洞;ewm(alpha=1/period)是pandas对 Wilder 平滑的标准实现,adjust=False确保权重严格按(19/20, 1/20)分配;iloc[:period]填充首期均值,符合原始海龟要求(前20日用简单平均);返回Series命名为'n_value',后续所有模块通过列名访问,不依赖位置索引。3. 核心策略逻辑:信号生成器如何严格还原原始海龟规则3.1 开仓与加仓:20日突破 vs 10日突破的精确实现原始海龟规则中,开仓信号来自20日最高价突破,加仓信号来自10日最高价突破,且加仓仅在已有仓位方向上进行。signal_generator.py用rolling().max()和布尔掩码实现,完全避免shift(1)导致的未来信息泄露:# signal_generator.py 片段:开仓信号 def generate_entry_signals(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() # 计算20日和10日最高/最低 df['high_20'] = df['high'].rolling(20).max().shift(1) # shift(1) 是关键!用昨日最高价对比今日价格 df['low_20'] = df['low'].rolling(20).min().shift(1) df['high_10'] = df['high'].rolling(10).max().shift(1) df['low_10'] = df['low'].rolling(10).min().shift(1) # 开仓信号:今日收盘价 > 昨日20日最高价 → 多头开仓 df['long_entry'] = (df['close'] > df['high_20']) & (df['close'].shift(1) <= df['high_20'].shift(1)) # 加仓信号:今日收盘价 > 昨日10日最高价 AND 已有做多仓位(由position_manager提供状态) df['long_add'] = (df['close'] > df['high_10']) & (df['close'].shift(1) <= df['high_10'].shift(1)) # 空头对称逻辑(略,结构相同) df['short_entry'] = (df['close'] < df['low_20']) & (df['close'].shift(1) >= df['low_20'].shift(1)) df['short_add'] = (df['close'] < df['low_10']) & (df['close'].shift(1) >= df['low_10'].shift(1)) return df参数说明:rolling(20).max().shift(1):获取截至昨日的20日最高价,今日用它判断是否突破,杜绝未来信息;& (df['close'].shift(1) <= ...):确保是首次突破(昨日未突破,今日突破),避免连续多日满足条件重复开仓;所有信号列(long_entry,long_add)均为布尔型,后续由position_manager统一消费,职责清晰。3.2 双止损机制:追踪止损 + 固定止损的协同触发海龟的退出规则是双保险:追踪止损:多头持仓中,当价格跌破entry_price - 2*N时平仓;固定止损:多头开仓后,若价格跌破entry_price - 2*N(同上),或加仓后跌破last_add_price - 2*N,则平该笔仓位。signal_generator.py中的generate_exit_signals函数用groupby+cummax/cummin实现动态追踪:# signal_generator.py 片段:追踪止损计算(多头) def _calc_long_trailing_stop(df: pd.DataFrame, n_series: pd.Series) -> pd.Series: # 为每个开仓事件生成独立的追踪线 # 先标记开仓点 entry_mask = df['long_entry'] | df['long_add'] df['entry_id'] = (entry_mask).cumsum() # 每次开仓+加仓生成新ID # 对每个entry_id组,计算该组起始的entry_price和n_value def calc_group_stop(group): first_idx = group.index[0] entry_price = group['close'].iloc[0] n_val = n_series.loc[first_idx] # 追踪线 = entry_price - 2 * n_val,保持不变(非动态更新!) return pd.Series([entry_price - 2 * n_val] * len(group), index=group.index) stop_line = df.groupby('entry_id').apply(calc_group_stop).droplevel(0) return stop_line.rename('long_trailing_stop') # 最终退出信号:price < trailing_stop OR price < fixed_stop(由position_manager传入)关键点:entry_id用(entry_mask).cumsum()生成,确保开仓和加仓各自独立计数;calc_group_stop中entry_price和n_val取自该组第一行(即开仓/加仓当日),符合“以入场时N值为准”的原始规则;追踪线是静态值(entry_price - 2*N),不是随N变化的动态线——这是海龟与现代ATR追踪的本质区别,常被误实现。3.3 仓位管理:R-multiple 如何决定每笔交易的合约数海龟的资金管理核心是 R-multiple:单笔风险 =N * 合约乘数,目标风险占总资金比例(如2%)。position_manager.py将其转化为可执行的合约数:# position_manager.py def calculate_position_size( equity: float, n_value: float, risk_ratio: float = 0.02, contract_multiplier: float = 10, # 例如螺纹钢期货为10吨/手 min_contracts: int = 1 ) -> int: """ 计算应开仓合约数 equity: 当前账户权益(元) n_value: 当前N值(元/点) risk_ratio: 单笔最大风险比例(如0.02表示2%) contract_multiplier: 每点价值(元/点/手) """ risk_per_contract = n_value * contract_multiplier # 1手的风险金额(元) total_risk_allowed = equity * risk_ratio # 总允许风险(元) contracts = total_risk_allowed / risk_per_contract return max(min_contracts, int(contracts)) # 向下取整,但不低于1手逻辑说明:risk_per_contract = n_value * contract_multiplier:N 是价格波动点数,乘以每点价值才是真实风险;int(contracts)是向下取整,因无法开0.7手,且海龟明确要求“宁少勿多”;max(min_contracts, ...)防止小资金下计算出0手,保证策略可执行;此函数被backtest_engine.py在每次信号触发时调用,传入实时equity和n_value。4. 回测引擎与避坑指南:为什么你的回测总是比实盘好?4.1 回测引擎如何模拟真实交易流backtest_engine.py不是简单地for i in range(len(df)),而是构建了状态机驱动的逐日推进,显式维护current_position,entry_price,stop_price,equity_curve:# backtest_engine.py 核心循环 def run_backtest(df: pd.DataFrame, initial_equity: float = 100000) -> pd.DataFrame: # 初始化状态 state = { 'position': 0, # 0=空仓,1=多头,-1=空头 'contracts': 0, # 当前持仓手数 'entry_price': 0, # 最近一次开仓价 'equity': initial_equity, 'commission': 0.0003, # 万三 'slippage': 0.001, # 0.1% } results = [] for i in range(len(df)): row = df.iloc[i].to_dict() # 1. 先检查是否触发平仓(优先级高于开仓) if state['position'] != 0: exit_signal = check_exit_condition(row, state) if exit_signal: state = execute_exit(state, row, exit_signal) # 2. 再检查开仓/加仓 if row.get('long_entry', False): size = calculate_position_size(state['equity'], row['n_value']) state = execute_entry(state, row, 'long', size) elif row.get('long_add', False) and state['position'] == 1: size = calculate_position_size(state['equity'], row['n_value']) state = execute_add(state, row, 'long', size) # 3. 记录每日状态 results.append({ 'date': df.index[i], 'equity': state['equity'], 'position': state['position'], 'contracts': state['contracts'], }) return pd.DataFrame(results)关键设计:check_exit_condition在execute_entry之前执行,确保当日先平后开,符合交易所规则;execute_entry/execute_add/execute_exit三个函数分别封装下单逻辑,含滑点、手续费计算;results存储每日快照,可直接绘图,不依赖中间状态变量。4.2 避坑指南:5个让回测失真的致命细节现象:回测年化收益35%,实盘却亏损。原因:回测引擎隐含了未来信息或忽略交易摩擦。以下是血泪经验总结的5个高频坑:坑:用df['close'] > df['high'].rolling(20).max()判断突破→ 原因:rolling(20).max()包含当前日,等于用今天最高价判断今天是否突破,属未来信息。→ 解决:必须.shift(1),用昨日20日最高价对比今日收盘价。坑:N值用df['n_value'].iloc[i]但未校验i>=20→ 原因:前20日n_value为 NaN,直接参与计算导致position_size=0或报错。→ 解决:在calculate_position_size前加if pd.isna(n_value): return 0,或初始化时用fillna(method='bfill')。坑:滑点按固定点数(如0.5点)设置,但不同品种点值不同→ 原因:螺纹钢1点=10元,黄金1点=100元,固定点数导致小品种滑点被放大。→ 解决:统一用百分比滑点(如row['close'] * 0.001),或传入point_value参数。坑:加仓信号与开仓信号共用同一entry_id→ 原因:导致追踪止损线用开仓日N值计算加仓部分,违反“每笔独立风控”原则。→ 解决:entry_id应由long_entry | long_add生成,但calc_group_stop中需区分entry_type,加仓组用加仓日N值。坑:回测结束时不平掉最后持仓→ 原因:equity_curve最后一日未计入平仓盈亏,虚增收益。→ 解决:循环结束后强制execute_exit(state, df.iloc[-1], 'final'),按收盘价平仓。注意:这5条全部来自某开发者在模拟项目X中踩过的坑,第3条曾导致黄金策略回测盈利、实盘滑点吃掉30%利润。建议把它们写成单元测试用例,每次改代码前先跑一遍。5. 二次开发实战:如何将此海龟框架接入你的实盘系统5.1 接入实盘的三步改造法这个项目不是玩具,它的模块化设计天生适配实盘。我一般用三步完成接入,耗时不超过2小时:第一步:替换数据源为实时行情接口保留data_loader.py结构,新增data_loader_realtime.py:# data_loader_realtime.py from my_broker_api import get_tick_data # 假设你有券商API def load_realtime_data(symbol: str, duration: str = '1d') -> pd.DataFrame: # 获取最新1天分钟线 raw_ticks = get_tick_data(symbol, duration) # 聚合成OHLCV(注意:必须用实际成交价,非撮合价) df = raw_ticks.resample('1T').agg({ 'price': ['first', 'max', 'min', 'last'], 'volume': 'sum' }) df.columns = ['open', 'high', 'low', 'close', 'volume'] return df.dropna()关键点:resample('1T')生成分钟线,agg中price的first/max/min/last必须对应真实tick的bid/ask/last字段,不能简单用ohlc()。第二步:信号生成与订单执行解耦signal_generator.py输出的long_entry等列,不再直接驱动回测,而是写入 Redis:# signal_generator.py 新增 import redis r = redis.Redis() def publish_signals(df: pd.DataFrame): signals = df[['long_entry', 'long_add', 'short_entry', 'short_add']].tail(1).to_dict('records')[0] r.publish('turtle_signals', json.dumps(signals))你的实盘订单服务订阅turtle_signals频道,收到后调用券商API下单,彻底解除策略与执行的耦合。第三步:动态参数热更新把risk_ratio=0.02、contract_multiplier=10等硬编码,改为从配置中心拉取:# config_loader.py import requests def get_strategy_config(strategy_name: str) -> dict: resp = requests.get(f'http://config-center/api/v1/config/{strategy_name}') return resp.json() # 返回 {"risk_ratio": 0.015, "min_contracts": 2} # 在 position_manager.py 中 config = get_strategy_config('turtle') size = calculate_position_size(equity, n_val, **config)这样调整风控参数无需重启策略进程,符合生产环境要求。5.2 验证改造是否成功的3个必检点接入完成后,不要急着挂实盘,先用这3个检查点验证:检查点验证方法通过标准信号时序正确性对比signal_generator.py输出的long_entry列与手动计算的20日突破点手动标出前5个突破日,与代码输出完全一致(日期、布尔值)仓位计算一致性输入相同equity=100000,n_value=25.5,contract_multiplier=10,运行calculate_position_size输出contracts=7(计算:100000×0.02÷(25.5×10)=7.84→int=7)实盘指令完整性抓包券商API请求体包含symbol,side,type='limit',price=close×1.001(买一档),quantity=7,无缺失字段从那以后我每次接入新策略,都强制走一遍这3个检查点,哪怕只是改了一个小数点。因为海龟的威力不在复杂,而在严丝合缝——少一个shift(1),多一个int(),就可能让整个系统从稳定变成灾难。希望帮到你。本文还有配套的精品资源,点击获取
简介:本资源是一份面向量化交易初学者与Python数据分析爱好者的实战代码包,聚焦海龟交易法则的完整Python实现,解决趋势跟踪策略从理论到代码落地的关键问题。包内共3个文件:2个CSV格式的历史行情数据(含上证600000等标的的OHLCV字段),1个核心Python脚本(turtle.py),完整封装了数据加载、N日SMA/EMA计算、突破信号识别、动态仓位管理及ATR止损逻辑,开箱即用。压缩包仅41KB,轻量简洁,便于快速导入Jupyter或PyCharm环境调试验证。目前已有1307人学习下载,适合希望掌握pandas处理金融时序数据、理解经典趋势策略工程化细节的学习者。读者可直接复现信号生成与模拟交易流程,并基于内置结构扩展回测统计、可视化盈亏曲线或接入实盘接口。1. 海龟交易法则不是玄学,是可复现、可调试、可落地的入场-出场闭环系统你是不是也见过这类“海龟策略”代码:几十行if套for,收盘价一算就开仓,止损线硬编码成0.02,回测用pandas.DataFrame.shift(1)模拟信号却漏掉滑点和成交延迟?结果一跑实盘,胜率从回测的63%掉到41%,最大回撤翻倍——这不是策略失效,是工程实现没过基本关。这个项目就是为解决这个问题而生:它用纯pandas构建了一套严格对齐原始海龟规则(R-multiple、N值波动、20日突破、10日突破、双止损、资金管理)的端到端实现,不依赖 TA-Lib 或 backtrader 等黑匣子库,所有逻辑暴露在 DataFrame 的列计算中,支持逐笔成交模拟、动态仓位计算、多合约滚动、以及关键的——信号生成与执行严格分离。适合想真正搞懂海龟内核、需要嵌入自有交易框架、或正在被“伪海龟”代码坑得睡不着觉的量化开发者。它不是教学 Demo,而是一份能直接放进你策略引擎 pipeline 的生产级参考实现。2. 项目结构与核心模块拆解:从数据加载到信号生成的完整链路2.1 目录结构与文件职责划分项目采用极简但高内聚的组织方式,共 5 个核心 Python 文件,无任何隐藏配置或魔法路径:turtle_strategy/ ├── data_loader.py # 统一入口:支持 CSV / yfinance / 本地 HDF5,强制返回标准 OHLCV + volume 列名 ├── volatility_calculator.py # N值计算核心:ATR(20) 的 pandas 向量化实现,含 Wilder 平滑逻辑(非简单 mean) ├── signal_generator.py # 主策略逻辑:20日高点突破开仓、10日低点突破加仓、双止损(追踪+固定)、空头对称逻辑 ├── position_manager.py # 仓位控制器:基于 R-multiple 动态计算单位头寸,支持最大风险比例(如2%)、最小合约数限制 └── backtest_engine.py # 回测驱动:按日/按分钟粒度推进,模拟下单、成交、滑点(默认0.1%)、手续费(固定0.0003)、持仓状态维护提示:所有模块均无全局变量,函数式设计,输入为pd.DataFrame,输出为带新列的DataFrame或dict。这意味着你可以单独测试signal_generator.generate_signals(df)而无需启动整个回测。2.2 数据加载与标准化:为什么必须重写data_loader.py很多“海龟策略”失败的第一步,就栽在数据上。原始海龟要求使用真实交易日的最高价/最低价/收盘价,且需处理停牌、复权、除权等干扰。本项目data_loader.py的关键设计在于:强制列名映射:无论输入是High/Low/Close还是high_price/low_price/close_price,统一转为['open', 'high', 'low', 'close', 'volume']复权模式可选:adjust='qfq'(前复权)或'hfq'(后复权),默认关闭(即不复权),因海龟原始规则明确使用未复权价格时间索引强校验:自动检测并转换为datetime64[ns],缺失日期用ffill()补充(但标记is_missing=True列供策略过滤)# data_loader.py 核心片段 def load_from_csv(filepath: str, adjust: str = None) -> pd.DataFrame: df = pd.read_csv(filepath, index_col=0, parse_dates=True) # 列名标准化 col_map = {'Open': 'open', 'High': 'high', 'Low': 'low', 'Close': 'close', 'Volume': 'volume'} df = df.rename(columns={k: v for k, v in col_map.items() if k in df.columns}) # 复权处理(仅当指定且存在前复权列时) if adjust == 'qfq' and 'adj_close' in df.columns: ratio = df['adj_close'] / df['close'] df['open'] = df['open'] * ratio df['high'] = df['high'] * ratio df['low'] = df['low'] * ratio df['close'] = df['adj_close'] return df[['open', 'high', 'low', 'close', 'volume']].sort_index()这段代码的关键参数说明:adjust:控制是否复权,None表示原始价格(推荐用于海龟),避免复权引入未来信息;ratio计算严格按adj_close / close,确保开盘/高低价同步缩放,而非只调收盘价;返回前强制sort_index(),防止时间乱序导致shift()错位——这是新手最常翻车的点。2.3 波动率计算:N值不是ATR,而是Wilder平滑后的真N海龟法则中的 N 值(市场波动单位)不是简单的 ATR(20),而是 Wilder 平滑版本:N[t] = (N[t-1] * 19 + TR[t]) / 20,其中 TR(True Range)= max(High−Low, |High−Close[t−1]|, |Low−Close[t−1]|)volatility_calculator.py用纯 pandas 实现,避免循环,性能比for快 8 倍以上:# volatility_calculator.py def calculate_n_value(df: pd.DataFrame, period: int = 20) -> pd.Series: # 计算 True Range:三者取最大 prev_close = df['close'].shift(1) tr1 = df['high'] - df['low'] tr2 = (df['high'] - prev_close).abs() tr3 = (df['low'] - prev_close).abs() tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) # Wilder 平滑:用 ewm(alpha=1/period) 模拟 (prev * (n-1) + curr) / n # 注意:ewm 默认 min_periods=period,首period行为空,需用 simple mean 填充 n_series = tr.ewm(alpha=1/period, adjust=False).mean() n_series.iloc[:period] = tr.iloc[:period].mean() # 首期用简单均值 return n_series.rename('n_value')逻辑说明:tr计算用pd.concat(...).max(axis=1),向量化,无apply(lambda x: max(...))这种性能黑洞;ewm(alpha=1/period)是pandas对 Wilder 平滑的标准实现,adjust=False确保权重严格按(19/20, 1/20)分配;iloc[:period]填充首期均值,符合原始海龟要求(前20日用简单平均);返回Series命名为'n_value',后续所有模块通过列名访问,不依赖位置索引。3. 核心策略逻辑:信号生成器如何严格还原原始海龟规则3.1 开仓与加仓:20日突破 vs 10日突破的精确实现原始海龟规则中,开仓信号来自20日最高价突破,加仓信号来自10日最高价突破,且加仓仅在已有仓位方向上进行。signal_generator.py用rolling().max()和布尔掩码实现,完全避免shift(1)导致的未来信息泄露:# signal_generator.py 片段:开仓信号 def generate_entry_signals(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() # 计算20日和10日最高/最低 df['high_20'] = df['high'].rolling(20).max().shift(1) # shift(1) 是关键!用昨日最高价对比今日价格 df['low_20'] = df['low'].rolling(20).min().shift(1) df['high_10'] = df['high'].rolling(10).max().shift(1) df['low_10'] = df['low'].rolling(10).min().shift(1) # 开仓信号:今日收盘价 > 昨日20日最高价 → 多头开仓 df['long_entry'] = (df['close'] > df['high_20']) & (df['close'].shift(1) <= df['high_20'].shift(1)) # 加仓信号:今日收盘价 > 昨日10日最高价 AND 已有做多仓位(由position_manager提供状态) df['long_add'] = (df['close'] > df['high_10']) & (df['close'].shift(1) <= df['high_10'].shift(1)) # 空头对称逻辑(略,结构相同) df['short_entry'] = (df['close'] < df['low_20']) & (df['close'].shift(1) >= df['low_20'].shift(1)) df['short_add'] = (df['close'] < df['low_10']) & (df['close'].shift(1) >= df['low_10'].shift(1)) return df参数说明:rolling(20).max().shift(1):获取截至昨日的20日最高价,今日用它判断是否突破,杜绝未来信息;& (df['close'].shift(1) <= ...):确保是首次突破(昨日未突破,今日突破),避免连续多日满足条件重复开仓;所有信号列(long_entry,long_add)均为布尔型,后续由position_manager统一消费,职责清晰。3.2 双止损机制:追踪止损 + 固定止损的协同触发海龟的退出规则是双保险:追踪止损:多头持仓中,当价格跌破entry_price - 2*N时平仓;固定止损:多头开仓后,若价格跌破entry_price - 2*N(同上),或加仓后跌破last_add_price - 2*N,则平该笔仓位。signal_generator.py中的generate_exit_signals函数用groupby+cummax/cummin实现动态追踪:# signal_generator.py 片段:追踪止损计算(多头) def _calc_long_trailing_stop(df: pd.DataFrame, n_series: pd.Series) -> pd.Series: # 为每个开仓事件生成独立的追踪线 # 先标记开仓点 entry_mask = df['long_entry'] | df['long_add'] df['entry_id'] = (entry_mask).cumsum() # 每次开仓+加仓生成新ID # 对每个entry_id组,计算该组起始的entry_price和n_value def calc_group_stop(group): first_idx = group.index[0] entry_price = group['close'].iloc[0] n_val = n_series.loc[first_idx] # 追踪线 = entry_price - 2 * n_val,保持不变(非动态更新!) return pd.Series([entry_price - 2 * n_val] * len(group), index=group.index) stop_line = df.groupby('entry_id').apply(calc_group_stop).droplevel(0) return stop_line.rename('long_trailing_stop') # 最终退出信号:price < trailing_stop OR price < fixed_stop(由position_manager传入)关键点:entry_id用(entry_mask).cumsum()生成,确保开仓和加仓各自独立计数;calc_group_stop中entry_price和n_val取自该组第一行(即开仓/加仓当日),符合“以入场时N值为准”的原始规则;追踪线是静态值(entry_price - 2*N),不是随N变化的动态线——这是海龟与现代ATR追踪的本质区别,常被误实现。3.3 仓位管理:R-multiple 如何决定每笔交易的合约数海龟的资金管理核心是 R-multiple:单笔风险 =N * 合约乘数,目标风险占总资金比例(如2%)。position_manager.py将其转化为可执行的合约数:# position_manager.py def calculate_position_size( equity: float, n_value: float, risk_ratio: float = 0.02, contract_multiplier: float = 10, # 例如螺纹钢期货为10吨/手 min_contracts: int = 1 ) -> int: """ 计算应开仓合约数 equity: 当前账户权益(元) n_value: 当前N值(元/点) risk_ratio: 单笔最大风险比例(如0.02表示2%) contract_multiplier: 每点价值(元/点/手) """ risk_per_contract = n_value * contract_multiplier # 1手的风险金额(元) total_risk_allowed = equity * risk_ratio # 总允许风险(元) contracts = total_risk_allowed / risk_per_contract return max(min_contracts, int(contracts)) # 向下取整,但不低于1手逻辑说明:risk_per_contract = n_value * contract_multiplier:N 是价格波动点数,乘以每点价值才是真实风险;int(contracts)是向下取整,因无法开0.7手,且海龟明确要求“宁少勿多”;max(min_contracts, ...)防止小资金下计算出0手,保证策略可执行;此函数被backtest_engine.py在每次信号触发时调用,传入实时equity和n_value。4. 回测引擎与避坑指南:为什么你的回测总是比实盘好?4.1 回测引擎如何模拟真实交易流backtest_engine.py不是简单地for i in range(len(df)),而是构建了状态机驱动的逐日推进,显式维护current_position,entry_price,stop_price,equity_curve:# backtest_engine.py 核心循环 def run_backtest(df: pd.DataFrame, initial_equity: float = 100000) -> pd.DataFrame: # 初始化状态 state = { 'position': 0, # 0=空仓,1=多头,-1=空头 'contracts': 0, # 当前持仓手数 'entry_price': 0, # 最近一次开仓价 'equity': initial_equity, 'commission': 0.0003, # 万三 'slippage': 0.001, # 0.1% } results = [] for i in range(len(df)): row = df.iloc[i].to_dict() # 1. 先检查是否触发平仓(优先级高于开仓) if state['position'] != 0: exit_signal = check_exit_condition(row, state) if exit_signal: state = execute_exit(state, row, exit_signal) # 2. 再检查开仓/加仓 if row.get('long_entry', False): size = calculate_position_size(state['equity'], row['n_value']) state = execute_entry(state, row, 'long', size) elif row.get('long_add', False) and state['position'] == 1: size = calculate_position_size(state['equity'], row['n_value']) state = execute_add(state, row, 'long', size) # 3. 记录每日状态 results.append({ 'date': df.index[i], 'equity': state['equity'], 'position': state['position'], 'contracts': state['contracts'], }) return pd.DataFrame(results)关键设计:check_exit_condition在execute_entry之前执行,确保当日先平后开,符合交易所规则;execute_entry/execute_add/execute_exit三个函数分别封装下单逻辑,含滑点、手续费计算;results存储每日快照,可直接绘图,不依赖中间状态变量。4.2 避坑指南:5个让回测失真的致命细节现象:回测年化收益35%,实盘却亏损。原因:回测引擎隐含了未来信息或忽略交易摩擦。以下是血泪经验总结的5个高频坑:坑:用df['close'] > df['high'].rolling(20).max()判断突破→ 原因:rolling(20).max()包含当前日,等于用今天最高价判断今天是否突破,属未来信息。→ 解决:必须.shift(1),用昨日20日最高价对比今日收盘价。坑:N值用df['n_value'].iloc[i]但未校验i>=20→ 原因:前20日n_value为 NaN,直接参与计算导致position_size=0或报错。→ 解决:在calculate_position_size前加if pd.isna(n_value): return 0,或初始化时用fillna(method='bfill')。坑:滑点按固定点数(如0.5点)设置,但不同品种点值不同→ 原因:螺纹钢1点=10元,黄金1点=100元,固定点数导致小品种滑点被放大。→ 解决:统一用百分比滑点(如row['close'] * 0.001),或传入point_value参数。坑:加仓信号与开仓信号共用同一entry_id→ 原因:导致追踪止损线用开仓日N值计算加仓部分,违反“每笔独立风控”原则。→ 解决:entry_id应由long_entry | long_add生成,但calc_group_stop中需区分entry_type,加仓组用加仓日N值。坑:回测结束时不平掉最后持仓→ 原因:equity_curve最后一日未计入平仓盈亏,虚增收益。→ 解决:循环结束后强制execute_exit(state, df.iloc[-1], 'final'),按收盘价平仓。注意:这5条全部来自某开发者在模拟项目X中踩过的坑,第3条曾导致黄金策略回测盈利、实盘滑点吃掉30%利润。建议把它们写成单元测试用例,每次改代码前先跑一遍。5. 二次开发实战:如何将此海龟框架接入你的实盘系统5.1 接入实盘的三步改造法这个项目不是玩具,它的模块化设计天生适配实盘。我一般用三步完成接入,耗时不超过2小时:第一步:替换数据源为实时行情接口保留data_loader.py结构,新增data_loader_realtime.py:# data_loader_realtime.py from my_broker_api import get_tick_data # 假设你有券商API def load_realtime_data(symbol: str, duration: str = '1d') -> pd.DataFrame: # 获取最新1天分钟线 raw_ticks = get_tick_data(symbol, duration) # 聚合成OHLCV(注意:必须用实际成交价,非撮合价) df = raw_ticks.resample('1T').agg({ 'price': ['first', 'max', 'min', 'last'], 'volume': 'sum' }) df.columns = ['open', 'high', 'low', 'close', 'volume'] return df.dropna()关键点:resample('1T')生成分钟线,agg中price的first/max/min/last必须对应真实tick的bid/ask/last字段,不能简单用ohlc()。第二步:信号生成与订单执行解耦signal_generator.py输出的long_entry等列,不再直接驱动回测,而是写入 Redis:# signal_generator.py 新增 import redis r = redis.Redis() def publish_signals(df: pd.DataFrame): signals = df[['long_entry', 'long_add', 'short_entry', 'short_add']].tail(1).to_dict('records')[0] r.publish('turtle_signals', json.dumps(signals))你的实盘订单服务订阅turtle_signals频道,收到后调用券商API下单,彻底解除策略与执行的耦合。第三步:动态参数热更新把risk_ratio=0.02、contract_multiplier=10等硬编码,改为从配置中心拉取:# config_loader.py import requests def get_strategy_config(strategy_name: str) -> dict: resp = requests.get(f'http://config-center/api/v1/config/{strategy_name}') return resp.json() # 返回 {"risk_ratio": 0.015, "min_contracts": 2} # 在 position_manager.py 中 config = get_strategy_config('turtle') size = calculate_position_size(equity, n_val, **config)这样调整风控参数无需重启策略进程,符合生产环境要求。5.2 验证改造是否成功的3个必检点接入完成后,不要急着挂实盘,先用这3个检查点验证:检查点验证方法通过标准信号时序正确性对比signal_generator.py输出的long_entry列与手动计算的20日突破点手动标出前5个突破日,与代码输出完全一致(日期、布尔值)仓位计算一致性输入相同equity=100000,n_value=25.5,contract_multiplier=10,运行calculate_position_size输出contracts=7(计算:100000×0.02÷(25.5×10)=7.84→int=7)实盘指令完整性抓包券商API请求体包含symbol,side,type='limit',price=close×1.001(买一档),quantity=7,无缺失字段从那以后我每次接入新策略,都强制走一遍这3个检查点,哪怕只是改了一个小数点。因为海龟的威力不在复杂,而在严丝合缝——少一个shift(1),多一个int(),就可能让整个系统从稳定变成灾难。希望帮到你。本文还有配套的精品资源,点击获取