
1. 为什么要把通达信公式搬到Python里做过A股量化的人多半都经历过这个阶段在通达信里用公式管理器写了一套自认为还不错的技术指标或者选股条件回测看着挺美但一旦想接入实盘、想批量跑全市场、想和机器学习模型结合就发现通达信那套公式语言TDX Formula的边界太窄了。它没有真正的循环控制没有复杂数据结构跨品种批量计算效率低更别提和外部数据源、风控模块、下单接口打通了。把通达信公式改写成Python代码本质上解决的是策略从看图工具走向可编程系统的问题。通达信公式适合快速验证想法Python适合工程化落地。这两者之间需要一个翻译层而这个翻译层不是简单的语法替换而是对数据对齐方式、计算语义、边界条件的重新理解。这篇文章适合三类人看一是有通达信公式基础、想转Python量化的朋友二是已经在用Python做量化、但需要把别人给的TDX公式快速移植过来的开发者三是想理解技术指标底层计算逻辑、不想被黑盒函数糊弄的进阶玩家。我会从公式语言的核心语义讲起逐条拆解改写方法给出可直接复用的代码模板最后把我踩过的坑一次性倒出来。核心关键词通达信公式改写、Python量化、技术指标计算、向量化、数据对齐。这些词会贯穿全文你如果正在做这件事建议边看边对着自己的公式试。2. 通达信公式语言的核心语义拆解2.1 通达信公式到底在算什么很多人写通达信公式写了好几年但其实没想清楚它的执行模型。通达信公式的本质是基于时间序列的向量运算。每一根K线对应一个数据点公式里的变量默认都是长度等于K线数量的数组。你写CLOSE拿到的是整个收盘价序列你写MA(CLOSE, 5)拿到的是每个位置上前5根K线收盘价的均值序列。这一点和Python里用pandas的Series做向量化运算是一一对应的。理解了这个对应关系改写就有了主线通达信的每个变量名对应Python里的一个Series或ndarray。但通达信有个特殊之处它区分序列变量和即时变量。序列变量用:赋值会保留历史值即时变量用:赋值只在当前K线有效。这个区别在改写时经常被忽略导致结果对不上。2.2 常见函数分类与Python对应关系我把通达信常用函数分成几类改写时按类处理效率最高通达信函数功能Python对应实现MA, EMA, SMA移动平均pandas的rolling().mean() / ewm()REF(X, N)引用N周期前的值Series.shift(N)HHV, LLVN周期最高/最低rolling().max() / rolling().min()CROSS(A, B)A上穿B(AB) (A.shift(1)B.shift(1))COUNT(X, N)N周期内满足条件次数rolling(N).sum()SUM(X, N)N周期求和rolling(N).sum()IF(X, A, B)条件选择numpy.where(X, A, B)BARSLAST(X)上次条件成立到现在的周期数需要手写循环或技巧实现这张表是改写的核心字典。你会发现大部分函数都能用pandas的rolling和shift组合出来但BARSLAST、BARSCOUNT这类涉及距离上次事件的函数pandas没有直接对应需要特殊处理。2.3 改写前必须搞清楚的三个语义差异第一个差异是数据对齐。通达信默认按交易日连续排列停牌日会被跳过或填充。而Python里如果你用pandas读数据索引可能是自然日也可能是交易日rolling窗口的计算基准完全不同。我见过太多人改写的MA结果和通达信差一两个点就是因为索引没对齐。第二个差异是空值处理。通达信在数据不足N根K线时MA返回的是无效值后续计算会跳过。pandas的rolling默认前N-1个是NaN但如果你用了min_periods参数不当会得到错误结果。改写时建议保持min_periodsN和通达信行为一致。第三个差异是未来函数。通达信里有些写法比如用REF引用未来数据在回测时会偷看未来Python里如果不注意shift方向同样会犯这个错。改写时凡是涉及REF(X, -N)这种负向引用的都要警惕。提示改写前先把原公式在通达信里跑一遍导出几组关键数值作为基准Python改完后逐一对齐。这是最笨但最可靠的方法。3. 逐类拆解从TDX语法到Python实现3.1 移动平均类函数的精确改写先看最简单的MA。通达信里MA(CLOSE, 5)Python写法import pandas as pd import numpy as np df[ma5] df[close].rolling(window5, min_periods5).mean()看起来简单但有个细节通达信的MA是简单移动平均不是指数移动平均。很多人把EMA和MA搞混。EMA的改写要用ewmdf[ema12] df[close].ewm(span12, adjustFalse).mean()这里adjustFalse是关键。pandas的ewm默认adjustTrue计算方式和通达信的EMA不同。通达信EMA的递推公式是EMA_today EMA_yesterday * (N-1)/(N1) CLOSE * 2/(N1)。用adjustFalse才能得到一致结果。至于SMA通达信的SMA(X, N, M)是加权移动平均公式为SMA_today (M*X (N-M)*SMA_yesterday) / N。这个pandas没有直接对应需要手写def tdx_sma(series, n, m): result np.zeros(len(series)) result[:] np.nan # 找到第一个有效值作为起点 for i in range(len(series)): if i 0: result[i] series[i] else: result[i] (m * series[i] (n - m) * result[i-1]) / n return pd.Series(result, indexseries.index)注意这个函数用了循环效率不高。如果数据量大可以用numba加速或者用递推公式的解析解做向量化。我实测过100万根K线用纯Python循环大概要几秒用numba能压到毫秒级。3.2 引用与位移REF和SHIFT的坑REF(CLOSE, 1)就是昨天的收盘价Python里是df[close].shift(1)。但这里有个大坑shift的方向。pandas的shift(1)是把数据往下移一行也就是当前行拿到的是上一行的值这正好对应REF的正向引用。而shift(-1)是往上移拿到的是未来数据对应REF的负向引用。我见过有人写REF(CLOSE, 1)改成shift(-1)结果回测收益高得离谱一查是偷看了未来。记住REF正数用shift正数REF负数用shift负数但负数是未来函数实盘不能用。还有一个细节通达信的REF在数据不足时返回无效值pandas的shift默认填充NaN行为一致。但如果你用了fillna要小心别把无效值填成0那会污染后续计算。3.3 条件判断与逻辑运算的向量化通达信里IF(CLOSE OPEN, 1, 0)Python里最直接的是df[signal] np.where(df[close] df[open], 1, 0)但更pandas的写法是df[signal] (df[close] df[open]).astype(int)两种都行前者更接近通达信语义后者更快。我一般用后者因为布尔转整数是零拷贝的。CROSS函数是改写的高频难点。通达信CROSS(A, B)表示A上穿B即前一周期AB且当前周期AB。Python实现def cross_up(a, b): return (a b) (a.shift(1) b.shift(1))注意这里用了而不是and因为是对Series做逐元素运算。and只能用于标量用错了会报错。这是新手最常见的错误之一。3.4 周期统计类HHV、LLV、COUNT、SUM这四个函数都是滚动窗口统计pandas的rolling能直接搞定df[hhv20] df[high].rolling(20).max() df[llv20] df[low].rolling(20).min() df[count_up] (df[close] df[open]).rolling(10).sum() df[sum_vol] df[volume].rolling(5).sum()但要注意通达信的HHV(X, N)在N为0时表示从第一根K线到当前的最高值是扩展窗口而不是固定窗口。Python里要用expanding()df[hhv_all] df[high].expanding().max()这个细节如果不注意改写出来的结果在早期数据上会差很多。3.5 最难啃的骨头BARSLAST和BARSCOUNTBARSLAST(X)返回上一次X为真到现在的周期数。这个函数pandas没有直接对应我试过三种实现方式方式一纯循环最直观但最慢def barslast(condition): result np.full(len(condition), np.nan) last_true -1 for i, val in enumerate(condition): if val: last_true i if last_true 0: result[i] i - last_true return pd.Series(result, indexcondition.index)方式二用groupby技巧速度快但可读性差def barslast_fast(condition): cond condition.astype(int) groups cond.cumsum() # 在每个组内做累积计数 return cond.groupby(groups).cumcount()方式三用numba加速循环兼顾速度和可读性。我一般推荐方式三因为量化策略里BARSLAST用得很多性能不能忽视。BARSCOUNT类似返回的是从第一根有效K线到当前的周期数可以用np.arange配合条件筛选实现。4. 完整实战把一个真实选股公式改写成Python4.1 原始公式解析假设有这样一个通达信选股公式我做了脱敏处理这是常见形态N:20; M:60; 均线多头:MA(CLOSE,5)MA(CLOSE,10) AND MA(CLOSE,10)MA(CLOSE,N); 放量:VOLMA(VOL,5)*1.5; 突破:CROSS(CLOSE,HHV(HIGH,N)); 回调:CLOSE/REF(CLOSE,1)1.03; 选股:均线多头 AND 放量 AND 突破 AND 回调;这个公式的逻辑是均线多头排列 放量 突破20日高点 当日涨幅不超过3%。改写前先逐行拆解语义。4.2 逐行改写与参数计算第一步准备数据。假设你已经有了一个DataFrame包含open、high、low、close、volume索引是交易日import pandas as pd import numpy as np # 假设df已经按日期升序排列 df df.sort_index() N 20 M 60第二步计算均线df[ma5] df[close].rolling(5, min_periods5).mean() df[ma10] df[close].rolling(10, min_periods10).mean() df[ma20] df[close].rolling(N, min_periodsN).mean() df[均线多头] (df[ma5] df[ma10]) (df[ma10] df[ma20])第三步放量判断df[vol_ma5] df[volume].rolling(5, min_periods5).mean() df[放量] df[volume] df[vol_ma5] * 1.5第四步突破判断。这里HHV(HIGH, N)是20日最高价CROSS是上穿df[hhv20] df[high].rolling(N, min_periodsN).max() df[突破] (df[close] df[hhv20]) (df[close].shift(1) df[hhv20].shift(1))注意这里有个细节通达信的CROSS(CLOSE, HHV(HIGH,N))HHV包含了当日最高价所以当日收盘价要超过当日最高价是不可能的。实际上这个写法在通达信里也有点问题更合理的应该是用REF(HHV(HIGH,N),1)。但既然原公式这么写改写时就保持一致只是要意识到这个逻辑可能选不出票。第五步回调判断df[回调] df[close] / df[close].shift(1) 1.03第六步组合信号df[选股] df[均线多头] df[放量] df[突破] df[回调]4.3 结果对齐与验证方法改完后怎么验证我的做法是在通达信里把公式加载到副图导出最近100个交易日的选股信号然后在Python里输出同样区间的信号逐日对比。对比时重点看三类差异一是边界日期数据不足N根K线时二是停牌日前后三是除权除息日。这三类是最容易出问题的地方。如果发现信号数量对不上先检查rolling的min_periods设置再检查shift的方向最后检查逻辑运算符用的是还是and。90%的问题出在这三处。提示验证时不要只看最终信号要把中间变量均线多头、放量、突破、回调逐个对比。这样能快速定位是哪一步出了问题。5. 性能优化让改写后的代码跑得比通达信还快5.1 向量化优先原则Python量化最大的性能陷阱就是在循环里做逐行计算。我见过有人把通达信公式改写成for循环遍历每一根K线结果跑全市场5000只股票要几个小时。而用pandas向量化同样的任务几分钟就能跑完。原则很简单能用rolling、shift、ewm解决的绝不用循环。只有BARSLAST这类无法向量化的函数才用循环而且要用numba加速。5.2 用numba加速无法向量化的部分对于BARSLAST、SMA这类需要递推的函数numba是最佳选择from numba import njit njit def barslast_numba(condition): n len(condition) result np.full(n, np.nan) last_true -1 for i in range(n): if condition[i]: last_true i if last_true 0: result[i] i - last_true return result第一次调用会有编译开销之后每次调用都是机器码速度。我实测过100万根K线的BARSLAST纯Python循环要2秒左右numba只要5毫秒。5.3 批量处理多只股票的技巧全市场选股时不要一只一只股票循环调用公式函数。更好的做法是把所有股票数据拼成一个大DataFrame用groupby做分组计算# df包含code列 df df.sort_values([code, date]) df[ma5] df.groupby(code)[close].transform( lambda x: x.rolling(5, min_periods5).mean() )这样pandas会在底层做优化比外层循环快很多。如果数据量特别大可以考虑用polars替代pandas速度能再提升几倍。5.4 内存与计算效率的平衡rolling窗口越大内存占用越高。如果只是做选股不需要保留所有中间变量可以用del及时释放df[ma5] df[close].rolling(5).mean() df[ma10] df[close].rolling(10).mean() # 用完就删 del df[ma5], df[ma10]另外数据类型也很关键。默认的float64占8字节如果精度要求不高可以转成float32内存直接减半。成交量用int32或int64不要用float。6. 常见问题与排查技巧实录6.1 改写后结果对不上的排查清单现象可能原因排查方法均线值差一点min_periods设置不对检查rolling的min_periods是否等于窗口信号数量多很多逻辑运算符用错确认用而非and用早期数据全是NaN窗口数据不足通达信会跳过pandas默认NaN行为一致突破信号提前出现shift方向反了检查REF正负和shift正负的对应EMA结果不一致adjust参数没设ewm必须加adjustFalse停牌日前后异常索引未对齐确认索引是交易日且无缺失这张表是我自己踩坑总结的基本上覆盖了90%的改写问题。遇到对不上的情况按表逐项排查比盲目调试快得多。6.2 除权除息导致的数据断裂通达信默认使用前复权数据而很多Python数据源默认是不复权或后复权。如果不做处理改写后的均线、突破信号会完全对不上。解决办法在数据准备阶段统一做前复权处理。前复权的逻辑是以最新价格为基准把历史价格按除权因子调整。具体公式是# 假设有除权因子adjust_factor df[close_adj] df[close] * df[adjust_factor] df[open_adj] df[open] * df[adjust_factor] df[high_adj] df[high] * df[adjust_factor] df[low_adj] df[low] * df[adjust_factor]成交量不需要复权但要注意除权日的成交量可能异常放大做放量判断时要考虑这个因素。6.3 未来函数的识别与规避通达信公式里有些写法是隐式的未来函数改写时如果不注意回测会虚高。常见的未来函数模式REF(X, -N)直接引用未来数据ZIG、PEAK、TROUGH这些函数在通达信里是事后确认的实盘不可用BACKSET把条件成立的位置向前回溯也是未来函数改写时遇到这些函数要么直接放弃要么用近似方法替代。比如ZIG可以用固定百分比的回撤来判断转折点虽然不如ZIG精确但至少实盘可用。6.4 实盘与回测的一致性保障回测跑通了不代表实盘能用。我总结了几条保障一致性的经验第一数据源要统一。回测用什么数据实盘就用什么数据不要回测用前复权、实盘用不复权。第二计算时点要明确。通达信公式默认用当日收盘价计算实盘如果盘中计算要用最新价替代收盘价但要注意盘中数据不完整的问题。第三信号确认要延迟。回测时信号在收盘后确认实盘也要等收盘后再下单不要盘中看到信号就冲进去。第四异常值要处理。涨跌停、停牌、新股上市首日这些特殊情况回测和实盘的处理方式要一致。7. 我个人的几条实操心得改写通达信公式这件事我做了不下几百个有几个体会是文档里不会写的。第一不要追求100%一致。通达信和Python的计算引擎不同浮点精度、数据源、复权方式都有细微差异追求完全一致是徒劳的。只要信号方向一致、数量差异在可接受范围内比如5%以内就可以用。第二先跑通再优化。很多人一上来就想写向量化、用numba结果逻辑还没验证就陷入性能优化的泥潭。我的做法是先用最笨的循环写一版确认结果对了再逐步优化。第三保留原始公式作为注释。改写后的Python代码里把对应的通达信公式用注释保留在函数上方。这样以后回头看能快速理解代码意图也方便别人接手。第四建立自己的函数库。MA、EMA、CROSS、BARSLAST这些函数改写一次就封装好下次直接调用。我现在的函数库积累了上百个函数新公式改写基本就是拼积木。第五测试用例要覆盖边界。数据不足、停牌、涨跌停、除权除息这些边界情况一定要写测试用例。我见过太多策略在正常行情下跑得好好的一遇到除权就崩了。最后分享一个小技巧如果你不确定某个通达信函数的精确语义可以在通达信里写一个最简单的公式比如X: BARSLAST(CLOSEOPEN);然后导出数据用几根K线手动验证。这比查文档快得多也更可靠。