去年写完《QMT 量化实战通达信指标完美复现指南上》之后我发现后台和私信里问得最多的反而不是“MACD 怎么平移”这种基础函数搬运而是同一个痛点的不同说法“通达信里明明画得出来同样的逻辑搬到 QMT 里就是不对”“K线图形能对上一到买卖信号就对不上”。这些提问者的共同特征很明显他们卡住的都不是纯计算类指标而是带状态、带过滤、甚至跨周期引用的那类“进阶指标”。所以这篇下篇我打算把这几类彻底讲透先用一张难度分级表帮你对号入座然后专门讲 BARSLAST、VALUEWHEN、FILTER 这类高级函数的 Python 翻译再用一个“金叉—回踩—再启动”类型的教学样例完整走一遍复现流程最后把指标信号接到自动下单的桥接层。内容偏进阶默认你已经有 QMT 的数据获取和基础 DataFrame 操作能力如果还没看过上篇建议先把 MA、MACD、KDJ 的向量化写法过一遍再回来。1. 复现前的难度分级你到底在复现“什么类型”的指标1.1 四个难度等级的判断方法先把通达信指标按复现难度分个级。我发现很多人在 QMT 里改一晚上改不出来其实不是 Python 水平问题而是根本没意识到指标内部用了哪类机制。通达信公式语言的执行逻辑是逐K线扫描但大多数函数是向量化的所以第一眼看起来都很简单可一旦碰到“条件成立后还要持续若干根K线”的写法用纯向量思维去解就特别容易绕晕。我把常见指标分成四类分别说明其特征和复现策略难度等级典型特征典型函数复现策略L1 纯计算类只依赖当前K线和固定周期窗口MA、EMA、REF、HHV、LLV、STDpandas rolling 直接平移L2 信号交叉类判断两序列在本K线发生关系变化CROSS、ISDOWN、ISUP前后两根K线值比较vectorized 写法L3 状态持久类依赖历史上最近一次条件成立的距今K线数BARSLAST、BARSLASTCOUNT、FILTER、VALUEWHEN、BACKSET辅助函数封装常见用循环或聚合思路L4 周期混合与未来数据类引用其他周期数据或使用事后才能确定的拐点ZIG、PEAK、TROUGH、DRAWLINE 和各类 #WEEK/#MONTH 调用重采样对齐并必须改造未来函数判断一个指标属于哪一级最直接的办法是在通达信公式编辑器里按CtrlF把用到的函数列一遍只要看到 BARSLAST、BARSLASTCOUNT、BACKSET、FILTER、VALUEWHEN 这类出现就说明指标内部存在跨K线的状态记忆单纯平移公式一定出问题。只要看到#WEEK、#MONTH这类跨周期引用就必须做数据重采样。而看到 ZIG、PEAK、TROUGH 或者画在未来位置上的 DRAWLINE就要立刻警惕这很可能是一个含未来数据的指标。1.2 为什么“未来函数类指标”不能直接复现先说一条铁律凡是用了未来函数的指标你在任何平台上都不可能精确复现这不是编程能力问题是时间方向本身就反了。举个例子通达信里很常见的 ZIG 指标用来画“之字形”拐点它判断当前K线是不是阶段高低点标准是“与后面几根K线的极值做比较”。也就是说当下这根K线的信号要等后面几根K线走完才能确认。在历史图上跑因为数据是完整的你看到的结果似乎很合理但在实盘中每一根新K线都可能推翻之前的拐点判断信号会来回修正。很多人在 QMT 里复现这种指标后发现回测收益极好实盘一跑就崩根因就在这里。这也是“量化泄露未来信息”这个词在交易圈里被反复讨论的核心场景。我的处理原则是遇到未来函数指标先分清楚它要表达的交易思想是什么再把这个思想改造成“实时可用”的逻辑。比如 ZIG 的本质是“识别趋势中的转折”那就可以用均线斜率、价格突破前高前低等实时可确认的条件去替代而不是硬套 ZIG 数学公式。改出来的指标和原版图形肯定不完全一样但交易逻辑是连续、可执行的这才是量化环境里真正需要的东西。记住复现指标的目的是拿到稳定可用的信号不是为了在图上画出一模一样的线。1.3 从副图指标入手控制复现复杂度实际拆解时我建议从副图指标而不是主图指标练手。副图指标通常是 MACD、KDJ、RSI、量能类计算逻辑相对规整信号是“单值输出”比较好验证。主图指标常常叠加了画线、填充、未来拐点判断视觉效果好但拆起来非常伤神。还要注意一点通达信里很多网上分享的指标源码看起来是副图但内部悄悄嵌套了未来函数或跨周期调用复制到 QMT 里跑出的结果会非常诡异。我每接手一个指标第一步就是先做上面那个“函数审阅”确定等级后再说翻译的事。这一步省下来的时间远比想象中多千万别跳。2. 通达信高级状态函数在 Python 里的标准翻译2.1 CROSS 函数最常见的“差一根K线”翻车点虽然 CROSS 只是 L2 等级但翻车率很高先把它彻底讲干净。通达信里的CROSS(A, B)定义为当 A 从小于等于 B 的状态在当根K线变成大于 B也就是“上穿”。很多人会把它直接翻译成(a b)这在向量化回测里会漏掉一个前提“前一K线不满足条件”导致信号多出一大堆。标准翻译如下import pandas as pd def cross_up(a: pd.Series, b: pd.Series) - pd.Series: 通达信 CROSS 的等价实现上一根 ab 且当前 ab return (a.shift(1) b.shift(1)) (a b)注意这里我用的是不是因为通达信 CROSS 对等于的情况也视为“之前未上穿”。如果你用成当 A 和 B 连续两根都在同一水平时信号方向判断就会出错。还有一点shift(1)之后第一个位置是 NaN和任何值比较都会返回 False所以不需要额外填充除非你后续要做布尔值的算术运算那才需要.fillna(False)。关于“差一根K线”的细节再展开一下实盘中当根K线收盘才能计算的信号真正能下单的窗口是下一根K线。通达信里用BARSSINCE或REF可以绕但 QMT 回测环境里就要自己做好“信号延迟一拍”的处理。保守做法是把信号列整体shift(1)之后再作为下单位条件这个原则在第三节会专门演示。2.2 BARSLAST距上次条件成立经过了多少根K线BARSLAST(COND)是我见过最多人挠头的通达信函数它的含义是“从当前K线往回数上一次条件 COND 成立时距离当前K线经过了多少根K线”。注意如果当前K线条件本身为真BARSLAST 返回 0如果历史中从未出现过条件返回的是缺失值或一个很大的数。这种逻辑在 Python 里一个直观写法就是循环import numpy as np def barslast(cond: pd.Series) - pd.Series: cond cond.reset_index(dropTrue).astype(bool) out np.zeros(len(cond), dtypeint) last_true -1 for i in range(len(cond)): if cond.iloc[i]: last_true i out[i] i - last_true if last_true 0 else 0 return pd.Series(out, cond.index)不过当数据量上万条时逐行 Python 循环会拖慢速度。更快的向量化写法是利用“最近一次条件为真的位置”的积累操作def barslast_vec(cond: pd.Series) - pd.Series: cond cond.reset_index(dropTrue).astype(bool) idx np.arange(len(cond)) last_pos np.where(cond, idx, -1) last_pos np.maximum.accumulate(last_pos) out np.where(last_pos 0, idx - last_pos, 0) return pd.Series(out, cond.index)核心思路是把条件为真的位置记录下来然后用maximum.accumulate把“最近一次为真”的位置广播到每一个后续K线最后用当前K线位置减去它。我在实际项目里常用这个向量化版性能比循环版提升非常明显而且结果完全一致。唯一要提醒的是如果你的 pandas 索引不是从 0 开始的连续整数务必先reset_index(dropTrue)否则位置减法会错位。还有一个容易踩的坑BARSLAST 的结果是“经过的K线数”不是“距今多少分钟”。在 5 分钟周期里BARSLAST3 表示距离上次条件成立经过了 3 根5分钟K线也就是 15 分钟前。如果你要把它换算成实际时间再去和行情时间比对得单独乘上周期长度别直接把数字当时间用。2.3 BARSLASTCOUNT连续条件成立的计数BARSLASTCOUNT(COND)返回的是当前K线为止条件连续成立了多少根。比如“连续上涨天数”就可以用它表达。这个函数很多人误以为等价于rolling(N).sum()但 N 是未知的你必须根据条件逐根累计。向量化方案如下def barslastcount(cond: pd.Series) - pd.Series: cond cond.reset_index(dropTrue).astype(bool) group (~cond).cumsum() return group.map(...) if False else None其实最清晰的写法是分组计数。因为条件为假的那一根会把分组号加一所以同一个连续为真的区间都落在同一组里对组内做cumsum就是连续成立数量def barslastcount(cond: pd.Series) - pd.Series: cond cond.reset_index(dropTrue).astype(bool) group (~cond).cumsum() count cond.groupby(group).cumsum() return pd.Series(count.values, cond.index)你可以验证一下条件序列为True, True, False, True, True, True时输出应该是1, 2, 0, 1, 2, 3。这个函数在写“连板高度”“连续放量N天”这类指标时非常常用。2.4 VALUEWHEN 与 FILTER取值和信号屏蔽VALUEWHEN(COND, X)的含义是当 COND 条件成立时取 X 的数值如果当前K线条件不成立则取最近一次条件成立时的 X 值。实现起来比 BARSLAST 简单用 pandas 的 where 加 ffill 即可def valuewhen(cond: pd.Series, x: pd.Series) - pd.Series: return x.where(cond).ffill()FILTER(X, N)则负责信号屏蔽当 X 出现一个有效信号后从下一根K线开始连续 N 根K线内即使 X 再次为真也不再输出直到 N 根之后才允许再次输出避免同类信号在短时间内频繁出现。典型应用场景就是买点信号过滤防止金叉之后紧接着再出一次金叉。def filter_signal(cond: pd.Series, n: int) - pd.Series: cond cond.reset_index(dropTrue).astype(bool) out np.zeros(len(cond), dtypebool) suppress_until -1 for i in range(len(cond)): if cond.iloc[i] and i suppress_until: out[i] True suppress_until i n return pd.Series(out, cond.index)这个函数用循环更直观因为信号屏蔽天然存在先后依赖。如果数据量很大可以通过“取条件为真的位置然后按间隔筛选”的方式优化原理一样但代码可读性会差一些。我一般只在超过几十万行的数据集上才考虑优化这段普通行情数据用循环没有压力。2.5 一组可直接复制的高级函数工具包把上面几个函数收拢起来就是一套日常复现通达信指标的基础工具包# tdx_utils.py import pandas as pd import numpy as np def cross_up(a, b): return (a.shift(1) b.shift(1)) (a b) def barslast(cond): cond cond.reset_index(dropTrue).astype(bool) idx np.arange(len(cond)) last_pos np.maximum.accumulate(np.where(cond, idx, -1)) return pd.Series(np.where(last_pos 0, idx - last_pos, 0), cond.index) def barslastcount(cond): cond cond.reset_index(dropTrue).astype(bool) group (~cond).cumsum() return pd.Series(cond.groupby(group).cumsum().values, cond.index) def valuewhen(cond, x): return x.where(cond).ffill() def filter_signal(cond, n): cond cond.reset_index(dropTrue).astype(bool) out np.zeros(len(cond), dtypebool) suppress_until -1 for i in range(len(cond)): if cond.iloc[i] and i suppress_until: out[i] True suppress_until i n return pd.Series(out, cond.index)这个工具包我维护了很久遇到新的通达信指标第一件事就是对照函数清单把能用现成辅助函数替换的先替换掉剩下的才是需要单独写逻辑的状态机部分。3. 完整复现流程拆解一个“金叉—回踩—再启动”类教学指标3.1 先在通达信里看清信号结构我拿一个网上常见的“三步点金”类副图信号做教学样例。先说明下面这段是为了讲透方法写的示意版本结构模仿这类信号常见的“金叉—回踩—再启动”三步判定不代表任何特定原版源码你直接用它测试和拆解逻辑即可。通达信公式示意如下MA5:MA(CLOSE,5); MA10:MA(CLOSE,10); MA20:MA(CLOSE,20); TREND:(MA5MA10) AND (CLOSEMA20); STEP1:CROSS(MA5,MA10); STEP2:(LOWMA10) AND (CLOSEMA10) AND REF(TREND,1); STEP3:(CLOSEREF(HHV(CLOSE,5),1)) AND (VOLREF(VOL,1)*1.2); SIGNAL:STEP1 OR (STEP2 AND STEP3); FINAL:FILTER(SIGNAL,5);这个结构很典型第一步是均线金叉给出趋势启动第二步是回踩确认要求盘中低点跌到 10 日线附近但收盘拉回且前一个K线趋势方向已经成立第三步是再启动要求收盘价突破前 5 根K线的最高收盘价同时成交量比上一根放大 1.2 倍。最后再用 FILTER 屏蔽 5 根K线内的重复信号。3.2 Python 复现与字段说明在 QMT 拿到 DataFrame 后按这个顺序实现import pandas as pd import numpy as np # 伪代码qmt 返回的 df 必须包含 open/high/low/close/volume 五个字段 # df xtdata.get_market_data_ex([], stock_code, period1d, count-1)[stock_code] df[ma5] df[close].rolling(5).mean() df[ma10] df[close].rolling(10).mean() df[ma20] df[close].rolling(20).mean() trend (df[ma5] df[ma10]) (df[close] df[ma20]) step1 cross_up(df[ma5], df[ma10]) step2 (df[low] df[ma10]) (df[close] df[ma10]) trend.shift(1).fillna(False) step3 (df[close] df[close].rolling(5).max().shift(1)) ( df[volume] df[volume].shift(1) * 1.2 ) signal_raw step1 | (step2 step3) final_signal filter_signal(signal_raw, 5) df[signal] final_signal这段代码里需要特别解释两个细节第一trend.shift(1)对应通达信里的REF(TREND,1)意思是回踩确认必须发生在趋势已经成立之后如果当前K线才刚金叉就不算有效回踩。第二df[close].rolling(5).max().shift(1)对应REF(HHV(CLOSE,5),1)取的是前一根K线往前数 5 根的最高收盘价千万别漏掉 shift漏了就是未来数据。3.3 信号验证怎么确认复现是对的复现之后不能只看有没有信号更要验证信号出现的“位置”和通达信里是否一致。我的验证方法很笨但很有效取一段有代表性的历史区间把通达信副图里每个信号出现的日期列出来然后在 QMT 的 DataFrame 里筛出final_signal为 True 的日期逐一比对。如果位置有偏移先检查是不是 REF 和 shift 错位如果频率不对先检查 FILTER 的屏蔽窗口是否实现正确。我还习惯把信号叠加到 K 线图上肉眼观察一遍重点看这几类金叉后的第一个买点是否出现回踩信号是否都出现在上涨趋势的回调段而不是下跌趋势中的随机位置FILTER 是否真的把 5 根K线内的重复信号屏蔽掉了。用肉眼先筛一遍再让代码去计算胜率、盈亏比这样能在早期发现逻辑错误省下后面回测定位问题的时间。3.4 实盘信号的延迟处理这节特别重要。上面算出final_signal是当根K线收盘之后才确定的值因为close、volume都是收盘或全量数据。如果你在 QMT 的自动交易里用df[signal].iloc[-1]在盘中实时判断这根K线还没收盘close和volume都是动态变化的信号可能反复跳动。所以实盘策略里我通常会把信号列再整体右移一根df[trade_signal] df[signal].shift(1).fillna(False)trade_signal为 True 意味着“上一根K线收盘后确认了买点当前这根K线执行下单”。这样虽然会晚一根K线但信号是稳定、可执行的。尤其是本例中 STEP2 用了 LOW 和 CLOSE盘中“收盘拉回”这个动作只有收盘才能确认延迟处理是必须的。4. 跨周期指标复现让小时级信号驱动分钟级K线4.1 为什么跨周期引用是难点通达信里写跨周期指标很简单经常是一句KDJ:KDJ(9,3,3); K:#MIN60.K;就完了。但 QMT 的 DataFrame 是单周期的你必须自己同时下载两套周期数据然后做时间对齐。对齐本身不难难的是处理“未完成K线”。举个例子你想用 60 分钟级别的 KDJ 多头趋势来过滤 5 分钟级别上出现的买入信号。如果当前这根 5 分钟K线运行到 14:35对应的那根 60 分钟K线14:00到15:00还没结束它的 close、low、high 都在变化KDJ 也一直在变。如果你直接把这个“正在变化的60分钟KDJ”用于过滤5 分钟信号会随着 60 分钟指标的变化来回开关实盘根本没法执行。解决原则只有一条只用“已经收盘确认的”大周期K线数据。具体操作是把大周期指标整体 shift 一根或是在代码里判断当前时间是否处于新的大周期第一根K线只用上一根大周期K线的指标值。4.2 在 QMT 里获取并重采样多周期数据先获取分钟数据然后通过 pandas 重采样生成 60 分钟数据import pandas as pd # 伪代码得到 1 分钟的 DataFrame # df_min xtdata.get_market_data_ex([], stock_code, period1m, count-1)[stock_code] df_min.index pd.to_datetime(df_min.index) df_60 df_min.resample(60min, labelright, closedright).agg({ open: first, high: max, low: min, close: last, volume: sum }).dropna()这里有两个参数值得解释closedright表示一个 60 分钟区间按右边界闭合方式切分labelright表示把区间结束时间作为这根 K 线的标签。A 股分钟数据通常以 09:35、09:36… 这样的时间戳表示当根K线结束时间所以用labelright能保持时间语义一致。重采样后df_60的每根K线都是完整一小时的聚合结果。4.3 大周期指标计算与向下对齐在 60 分钟 DataFrame 上算好 KDJ 或 MACD 后再对齐回分钟级别k60, d60, j60 kdj(df_60) # 复用你自己封装好的 kdj 函数 df_min[k60] k60.reindex(df_min.index, methodffill) df_min[d60] d60.reindex(df_min.index, methodffill) df_min[j60] j60.reindex(df_min.index, methodffill) # 关键盘中过滤时只能使用上一根已收盘的60分钟值 df_min[k60_ok] df_min[k60].shift(1) df_min[d60].shift(1)reindex(..., methodffill)会把每个分钟K线对应到当前所属的 60 分钟区间的最新值但那个值可能是未完成K线的动态值。所以我又追加了一步shift(1)确保用的是“上一根已经走完的60分钟K线”的指标。如果你策略允许在 60 分钟K线收盘后立即决策可以把shift(1)改成按时间判断当前K线是不是 60 分钟周期的第一根但后者实现起来要小心节假日和午间休市不如shift(1)稳定。4.4 高频周期的“信号确认”技巧跨周期复现最容易出现的问题是日线或小时级别的信号明明正常但到了分钟级就变得极其稀疏或极其频繁。原因往往是你直接把大周期信号广播到了每一个分钟K线上导致同一个大周期买点在分钟级被重复计算或者 FILTER 在大周期算好后没考虑分钟级重复。我的习惯是大周期只输出“状态”不输出“交易信号”。比如 60 分钟 KDJ 只输出“多头状态”这一个布尔值分钟级则负责输出具体的买点触发条件。状态可以持续多根分钟K线买点只出现一次。这样逻辑清爽后续做自动化下单也更容易控制频率。5. 从指标信号到自动化下单的最后一公里5.1 先把信号整理成可消费的表格指标复现完成后下一步就是让策略进程能消费它。我建议把信号统一输出成一张干净的订单候选表字段包含# order_candidates DataFrame 应包含的字段 # time : 信号确认的时间 # stock : 股票代码 # signal : 1买入-1卖出0无操作 # strength : 信号强度可以用 KDJ 的 J 值或量比填充 # reason : 触发的信号名称方便复盘字段尽量规范尤其是reason。很多人在 QMT 里跑了一段时间后想复盘发现信号是有的但不知道当时为什么触发因为所有信号都记成一个 buy。加一个 reason 字段成本很低复盘价值极高。5.2 策略主循环与调度的基本架构自动交易的主循环不需要复杂。以定时任务为例每分钟拉一次最新行情重算指标再查一下信号表有没有新增信号def strategy_loop(): df get_latest_kline(stock, period1m, count200) df calc_all_indicators(df) new_signal detect_signal(df) if new_signal ! 0: place_order(stock, new_signal, volume100)这里要特别注意幂等性同一根K线确认出来的信号不能因为定时任务重复触发而重复下单。常见做法是记录上一次已处理的K线时间戳本次只处理比它新的K线数据processed_bar_time None def strategy_loop(): global processed_bar_time df get_latest_kline(...) latest_time df.index[-1] if latest_time processed_bar_time: return processed_bar_time latest_time # ... 剩下的信号判断与下单逻辑5.3 用 HTTP 桥接层把策略和交易客户端连接起来在实际部署中我发现很多人遇到一个尴尬情况策略代码跑在轻量级 Python 环境里数据计算和数据获取都没问题但下单函数只能通过完整版交易客户端暴露。于是这两年社区里开始流行一种做法用 HTTP 接口在策略进程和交易客户端之间搭一个桥接层策略只管算信号把下单请求通过本地 HTTP 调用发给交易端交易端负责执行、回报和风控。这个思路我来回用了很久确实能解决“策略环境”和“交易环境”分离的问题。一个最小示意如下from http.server import HTTPServer, BaseHTTPRequestHandler import json class TradeBridge(BaseHTTPRequestHandler): def do_POST(self): content_length int(self.headers.get(Content-Length, 0)) payload json.loads(self.rfile.read(content_length)) action payload.get(action) stock payload.get(stock) volume payload.get(volume) # 这里只做参数校验实际下单调用封装好的 QMT 交易接口 if action not in (buy, sell): self.send_response(400) self.end_headers() return # 调用交易客户端封装的 order 函数示意 # result trading_client.order(stock, action, volume) result {status: filled, stock: stock, volume: volume} self.send_response(200) self.send_header(Content-Type, application/json) self.end_headers() self.wfile.write(json.dumps(result).encode(utf-8)) if __name__ __main__: server HTTPServer((127.0.0.1, 8808), TradeBridge) server.serve_forever()这段代码只是一个结构性示范具体的下单函数要根据你所用 QMT 交易接口来封装。但有几个原则是通用的第一桥接服务只绑定127.0.0.1不要暴露到公网第二所有下单请求都要做参数白名单校验至少校验股票代码、买卖方向、手数范围第三下单前必须有成交回报确认不能只发出请求就认为成交了。5.4 风控检查清单自动化上线前我建议把下面这张风控清单过一遍。很多问题不是指标逻辑问题而是工程层面的“手滑”问题单标的单日最大买入次数和累计买入数量全天最大亏损回撤触发后暂停交易并在群里通知信号重复触发的幂等控制确保同一根K线只处理一次下单失败的重试次数避免网络抖动时反复提交连接断开后的自动恢复策略优先恢复数据链路再恢复交易沙盒环境的参数与实盘环境隔离不要混用配置QMT 沙盒环境适合先跑全流程熟悉接口但我测试下来的体会是沙盒和实盘在委托字段、成交回报细节上有差异所以最稳的验证方法还是先做“只记录信号不真实下单”的观察期跑两到三周确认信号和预期的买卖点一致后再放开自动下单。6. 性能优化与常见错误排查6.1 为什么纯 Python 循环在 QMT 里跑得特别慢很多人写完指标复现后第一版总是“跑得出结果但是慢到没法用”。最常见的原因是在 DataFrame 上做逐行循环而且循环体内还在大量使用 pandas 的索引切片。pandas 的索引操作有开销几万行数据循环下来时间会放大非常多。我的优化顺序是先把指标翻译成向量化计算减少循环如果遇到必须循环的状态类逻辑比如 FILTER 这种天然有先后依赖的先把 Series 转成 numpy 数组循环算完再转回 pandas。下面这段代码是没有必要做 DataFrame 循环的示范注意对比# 慢逐行访问 DataFrame 的列 for i in range(1, len(df)): df.loc[i, ma5] df[close].iloc[i-4:i1].mean() # 快直接用 rolling df[ma5] df[close].rolling(5).mean()数据量到几十万行时两者的性能差距是数量级的。QMT 的分钟级历史数据动不动就是几十万根K线一开始就养成分层优化的习惯能省很多等待时间。6.2 复权方式不一致回测对不上历史图的常见原因通达信默认显示前复权价格而 QMT 在下载历史数据时复权方式需要单独设置。如果你拿通达信复盘里看到的信号位置和 QMT 回测结果对不上第一嫌疑就是复权方式不一致。前复权的特点是价格会随历史分红送股调整越早的K线价格越低后复权则相反不复权则是原始成交价。对指标计算而言均线、动量、突破这类型的价格触发性指标在复权方式不同时历史信号位置会明显不同甚至可能差出好几根K线。所以我的习惯是先确定 QMT 数据下载时的复权字段设为前复权再和通达信对比同一时间段的价格确认两者一致后才开始复现指标。6.3 常见错误速查表症状可能原因排查方向信号整体比通达信晚一根K线REF 和 shift 方向理解反了检查所有 REF 是否对应 .shift(1)确认信号确认时间和下单位置信号明显多出很多CROSS 实现漏了上一K线比较用(a.shift(1) b.shift(1)) (a b)替换简单的(a b)信号出现时间不对像是提前知道了未来用了 rolling(5).max() 没有再次 shift或在当根K线使用了未收盘的 close价格突破类条件统一用“上一根K线收盘价已确认”的写法FILTER 没起作用先调 filter_signal再又对结果做延迟处理顺序搞混先 FILTER再 shift 生成交易信号两步分开跨周期信号频繁闪烁直接 reindex 广播了未完成大周期K线值对齐后整体 shift(1)只用已收盘大周期值回测结果和通达信图形完全对不上复权方式不一致确认 QMT 下载数据的前复权设置6.4 实盘运行后的增量更新策略历史数据一次性计算完成后实盘里更常见的场景是“每天新增一根或几根K线”。这时候不需要每次全量重算整个 DataFrame只需要增量计算最近若干根K线。我习惯的做法是保留一份历史计算结果每天拉取最后 50 根K线重新计算指标再拼接回去。50 根K线对于均线周期不超过 20 的指标来说足够覆盖需要前瞻的窗口计算量很小也不会引入未来数据。增量更新最容易出问题的地方是 rolling 窗口的边界。如果只取最后 50 根K线就必须保证这 50 根数据是连续的、没有缺口的否则 rolling(5).mean() 会把缺口当成正常数据计算结果带着暗伤。每次更新时检查一下日期连续性成本很低。6.5 复盘记录的落地最后分享一个我坚持了很久的小习惯每次信号触发后把触发时的分钟级行情快照、指标值、最终是否成交一并存进本地数据库字段至少包括时间、代码、信号类型、触发条件详情、当时的指标数值。这样做的好处短期内看不出来但积累三到六个月后你可以直接拿这些记录做信号迭代分析找出哪些条件下信号成功率明显偏高哪些条件下老是假突破。很多量化优化思路其实是从这些看似无用的历史记录里冒出来的。我在实际复现了几十个通达信指标之后最深的一个体感是先判断指标属于哪个难度等级再决定用工具函数还是单独写状态逻辑永远比拿到公式就直接翻译靠谱。通达信公式写得再花哨底层无非是“数值计算、状态记忆、周期对齐”这三件事。把这三大类问题拆清楚复现速度会快得很明显。剩下那些含未来函数的指标也别一棍子打死能改成实时逻辑就改改不了就果断放弃至少你不会在实盘里被反复变化的信号坑到。