
副图指标这个词在行情终端里出现的频率极高但真正能把一份指标源码读懂、改到适配自己使用习惯的人并不多。“狂牛启动”这个名字一听就知道属于哪一类它想捕捉的是横盘末端、量能异动、均线由纠缠转向发散的那一刻然后把这种启动信号用柱子或文字在副图上标出来。我做过不少类似的公式拆解也帮人改过一堆从各处收来的指标发现绝大多数“启动型”副图指标的骨架高度雷同——差别主要在阈值、平滑方式和标注方式上。这篇内容就把这套骨架拆开揉碎从命名套路、语法结构、打分制设计、参数标定一直讲到用脚本做信号密度校验和未来函数排查适合刚接触公式编写的新手也适合写了很多年公式但总被信号闪烁困扰的老手参考。文中所有代码都是我自己重写的演示版本不涉及任何第三方软件包的二次修改。1. 副图指标到底在画什么从“狂牛启动”的命名套路说起1.1 主图与副图的分工差异先把这个最基本的分工讲清楚后面所有设计都建立在这上面。主图承载的是价格本身——K线、均线、通道线纵轴刻度是价格或指数点位坐标系是被价格“锚定”的。副图不一样副图的纵轴是自定义的可以是0到100的分数、可以是成交量倍数、也可以是任意无量纲的数值它本质上是一个附加维度用来把主图上肉眼看不出来的信息单独开辟一块空间画出来。这个差异带来一个很实际的后果主图指标和副图指标不能互相抄公式。你从别人的主图指标里抄一段均线变色逻辑放进副图往往画不出来因为主图用价格做纵轴输出值是价格量级副图如果输出一个20日均价而你的副图范围设成0到100那根线会直接飞出画面顶部。我见过太多人卡在这一步以为公式写错了其实是输出量纲和副图坐标范围不匹配。所以设计副图指标的第一个决策不是“用什么函数”而是“我要输出什么量纲”。输出0到100的打分、输出0到1的布尔标记、输出成交量倍数、输出差值百分比这四种量纲的画法和后续标注方式完全不同。我个人偏好打分制0到100原因是它可以分级显示强弱而不是只有一个“有信号/没信号”的二值状态信息密度高得多。1.2 一个“启动型”指标通常由哪几块拼成把市面上流传的各类启动型副图拆开内核基本逃不出四个模块的组合。第一个是趋势确认模块负责判断当前是不是已经处于多头结构最常见的写法就是短中长三条均线的多头排列或者收盘价站上某条中期均线并且该均线本身在向上。第二个是量能异动模块负责判断有没有资金参与的迹象典型写法是当日成交量相对过去N日均量的倍数超过某个阈值。第三个是突破模块负责判断价格有没有脱离前期的横盘区间通常是收盘价创出过去N日新高或者突破某个箱体上沿。第四个是蓄势模块反过来判断启动之前是不是真的“安静”过——振幅收窄、波动率下降、成交量萎缩这一块最容易被漏掉但恰恰是区分“启动”和“追高”的关键。这四块拼在一起用逻辑与AND连接就得到了一个最朴素的启动信号。名字起得响一点就叫“狂牛启动”起得学术一点就叫“多因子共振”。内核是一样的。我把这个拆解过程放在最前面是想说明一件事你看到的那些花哨名字背后没有神秘算法只有这四块积木的不同组合方式和不同阈值。1.3 为什么很多人拿到源码还是用不明白这是我在实际交流中遇到最多的情况拿到一份源码复制粘贴进去能编译通过图上也有柱子冒出来但完全不知道那些柱子为什么在那个位置出现调一个参数信号全没了再调回来又满屏都是。问题出在三个地方。一是参数没有语义。很多流传的公式参数名就叫N1、N2、N3、M不告诉你N1代表什么周期。你把它从20改成30不知道自己在改的是“平台统计窗口”还是“均线周期”只能瞎试。二是阈值被硬编码在公式里比如直接写了VMA(V,5)*2你看着这个2不知道该不该动。三是条件用AND硬连接四个条件全满足才出信号任何一个条件稍微苛刻一点信号就归零稍微宽松一点信号就糊成一片。这种“全有或全无”的结构对参数极度敏感是很多人用不下去的根本原因。后面我会用打分制替代AND硬连接把“全有或全无”变成“加权求和再卡线”这是解决参数敏感问题最直接的办法。2. 公式语言的骨架五行结构拆解2.1 参数行、变量行、计算行、输出行、绘图行不管哪家的公式语言一份完整的副图指标代码都可以切成五层。理解了这五层你读任何陌生公式都能快速定位到关键逻辑。参数层是写在代码最前面的一组可调变量通常带默认值和取值范围。变量层用中间变量把复杂计算拆成一步步的中间结果用:这种“只赋值不输出”的符号。计算层是真正的逻辑运算各种比较、与或非、条件判断。输出层用:单冒号表示这个值要画到图上。绘图层是STICKLINE、DRAWTEXT、DRAWICON这类专门控制视觉呈现的函数。新手最容易犯的错是把该用:的地方写成:结果副图上冒出一堆乱七八糟的线。记住一条规则中间过程一律用:只有最终想看到的那个值才用:。一份写得干净的公式输出行通常只有一到三条。2.2 必须搞清楚的八个基础函数下面这张表是我认为写副图指标绕不开的八个函数按使用频率排序。表里只讲语义和坑具体语法各家略有差异以你所用终端的函数说明为准。函数语义典型用法最容易踩的坑MA(X,N)简单移动平均MA(C,20)N设太小噪声大设太大钝化严重EMA(X,N)指数移动平均EMA(C,12)与MA混用会导致两条线量纲感不一致REF(X,N)取N周期前的值REF(C,1)N0时返回当前值容易写出逻辑死循环HHV/LLV(X,N)N周期内最高/最低HHV(H,20)含当前K线做突破判断时要REF一层COUNT(COND,N)N周期内条件成立次数COUNT(CO,10)N为0表示从第一根K线起算性能差BARSLAST(COND)距上次条件成立的天数BARSLAST(信号)首次出现前返回无效值需处理CROSS(A,B)A上穿BCROSS(MA5,MA10)只在上穿那根为真连续走强不会再触发SUM(X,N)N周期累加SUM(V,5)与COUNT区别一个是求和一个是计数这里重点说HHV这个坑因为它在突破模块里几乎必用。HHV(H,20)统计的是包含当前K线在内的20根最高价如果你直接写CHHV(H,20)这个条件永远不成立——因为当前K线的收盘价不可能大于包含它自己在内的最高价。正确写法必须往前挪一根CREF(HHV(H,20),1)。这一个字的差别能让整个指标从“从不触发”变成“正常工作”。我在帮人看公式的时候这类错误占了相当大的比例。2.3 绘图函数与颜色控制副图的价值一半在数值一半在呈现。数值对了但画得糊用起来照样难受。常用的绘图手段就三种柱状STICKLINE、文字DRAWTEXT、图标DRAWICON。柱状用于表达强度比如打分越高柱子越高、颜色越深。这里有个实用技巧同一根柱子用两段STICKLINE叠画先画一根宽的浅色做底再画一根窄的深色做芯视觉上就有立体感比单色柱子清楚得多。文字标注适合标关键节点但要注意位置参数标注位置如果设在固定值上当副图纵轴范围变化时会飘出画面稳妥的做法是把标注位置写成“当前值 一个固定的偏移量”。颜色控制上我建议只保留两到三种语义色强势一种、弱势一种、中性一种。有些人喜欢把每个条件配一种颜色最后副图变成调色盘反而看不出重点。颜色是信息层级的表达工具不是装饰。3. 从零手写一个打分制启动副图指标3.1 第一步把模糊描述翻译成分项打分“启动”这个词是模糊的落到代码里必须变成可计算的东西。我的做法是先做一次翻译把“我觉得它要启动了”这个直觉拆成几个可以独立验证的分项再给每个分项分配权重。权重分配不是拍脑袋要按“这个条件对结论的贡献度”来。以启动判断为例量能异动是最难伪装的权重给最高趋势确认是基础门槛权重次之突破是触发条件权重再低一档蓄势是加分项权重最低。我给的一个参考配比是量能30分、趋势30分、突破25分、蓄势15分加起来100分方便读数。为什么用加权求和而不是AND因为AND的本质是权重无限大——任何一个条件不满足直接判0分。而现实中经常出现“量能很猛但趋势还没完全转好”这种半启动状态AND会把它全部丢掉打分制可以保留成60分、65分让你看到“正在酝酿”的中间态。这个差别在实际使用中非常明显。3.2 第二步分项公式逐条落地下面是我按这个思路写的一份演示代码参数全部命名成有语义的英文缩写方便你后续调整。这段代码用的是常见的类Pascal风格公式语法各家终端大同小异。{ 参数区 } {TREND_N : 趋势均线组合的长周期, 默认20} {PLAT_N : 平台统计窗口, 默认20} {VOL_N : 量能基准均线周期, 默认5} {VOL_K : 量能放大倍数阈值, 默认1.8} {AMP_MAX : 蓄势振幅上限(百分比), 默认12} { 趋势分 } MA5 : MA(C,5); MA10 : MA(C,10); MAL : MA(C,TREND_N); UP_TREND : MA5 MA10 AND MA10 MAL AND MAL REF(MAL,1); S_TREND : IF(UP_TREND, 30, IF(MA5MA10, 18, IF(CMAL, 10, 0))); { 量能分 } VMA : MA(V,VOL_N); VRAT : V / VMA; S_VOL : IF(VRAT VOL_K, 30, IF(VRAT VOL_K*0.75, 22, IF(VRAT 1.2, 12, IF(VRAT 1.0, 6, 0)))); { 突破分 } PLAT_H : REF(HHV(H,PLAT_N),1); BRK : C PLAT_H; S_BRK : IF(BRK, 25, IF(C PLAT_H*0.97, 12, 0)); { 蓄势分 } AMP : (HHV(H,PLAT_N)-LLV(L,PLAT_N))/LLV(L,PLAT_N)*100; S_AMP : IF(AMP AMP_MAX, 15, IF(AMP AMP_MAX*1.5, 8, 0)); { 总分与输出 } SCORE : S_TREND S_VOL S_BRK S_AMP; 启动分: SCORE, COLORWHITE; 警戒线: 70, COLORGRAY;这份代码有几个设计上的用意值得说一下。分项打分用的是嵌套IF做阶梯而不是简单的0/满分二值这样每一项都能贡献中间值。比如趋势项给了30、18、10、0四档量能项给了30、22、12、6、0五档。阶梯越细总分的分辨率越高用起来越能看出“接近了”和“还早得很”的区别。另外注意PLAT_H那一行我特意用了REF(HHV(...),1)就是前面提到的那个坑。突破判断必须是“今天的收盘价超过了昨天为止的N日最高”否则条件恒假。3.3 第三步输出层与信号标注数值算出来只是第一步怎么画决定了它好不好用。我的画法是三层底层画一个0到100的浅色柱体做背景刻度感中层画实际打分柱顶层在超过警戒线时叠加强调色并加文字。{基础柱: 用浅灰表示0-100的完整刻度} STICKLINE(1, 0, 100, 8, 0), COLOR404040; {打分柱: 不足70用中性色, 超过70用强调色} STICKLINE(SCORE0 AND SCORE70, 0, SCORE, 6, 0), COLOR808080; STICKLINE(SCORE70, 0, SCORE, 6, 0), COLORRED; STICKLINE(SCORE85, 0, SCORE, 3, 0), COLORYELLOW; {分项堆叠显示, 便于判断是哪一项在拉分} STICKLINE(1, 0, S_TREND, 2, 1), COLORBLUE; STICKLINE(1, S_TREND, S_TRENDS_VOL, 2, 1), COLORMAGENTA; {信号文字标注} DRAWTEXT(CROSS(SCORE,70), SCORE8, 启动);这里STICKLINE的第一个参数是条件后面依次是“起点值、终点值、宽度、是否空心”。最后那个分项堆叠是我个人觉得很实用的一个技巧——把趋势分和量能分画成堆叠的细条一眼就能看出这波分数是均线撑起来的还是量能撑起来的。趋势高量能低说明是慢牛型量能高趋势低说明是脉冲型两种情况下后续的应对方式完全不同。DRAWTEXT的CROSS(SCORE,70)也值得说一下。这里用CROSS而不是SCORE70是为了只在上穿那根K线标一次文字避免连续十几根K线叠出一串重复文字把图糊满。CROSS 的语义是“前一根不满足、这一根满足”正好符合“首次触发”的需求。3.4 第四步参数标定与信号密度检查代码写完不能直接就用了必须做参数标定。标定的核心指标是信号密度——在足够长的历史区间里信号出现的频率。我的经验基准是日线级别的启动型信号一年出现15到40次比较合理平均到每个月1到3次。低于10次说明条件太苛刻参数过拟合了历史样本外基本全是遗漏高于80次说明条件太松信号失去区分度跟随机撒点没区别。标定的方法是单参数扫描。固定其他参数把VOL_K从1.2往上每次加0.1扫到3.0记录每个取值下的信号次数画一张表。你会看到信号次数随阈值上升单调下降中间通常有一段“下降很平缓”的区间说明这个区间内信号对参数不敏感是最稳健的选择。如果找不到这段平缓区间说明你的条件之间高度冗余——两个条件在描述同一件事删掉一个。AMP_MAX这个参数要特别小心。蓄势振幅上限设得太严比如8%会漏掉大量温和波动的标的因为不同标的的波动率天然不同用一个绝对值卡所有标的是不合理的。更稳的做法是改成相对值用当前振幅除以过去一年的平均振幅得到振幅的收缩比收缩比小于0.6算蓄势。这样跨标的可比也不用天天调参数。4. 用 Python 把公式“验一遍”4.1 数据对齐与复权公式在终端里画出来你只能看到图和信号位置看不到信号数量和分布的统计。想知道这个指标到底出了多少次信号、信号之后走势分布如何得把公式搬到脚本里跑一遍。这不是为了做决策纯粹是公式校验。第一步是数据准备这里有个必须处理的问题复权。终端里的图表默认是复权显示的但你自己导出的原始数据往往是未复权的两者算出来的均线和信号位置会对不上。建议直接导出复权后的日线数据字段至少包含日期、开、高、低、收、量。如果只有未复权数据那就需要在除权日前后做前复权处理否则除权当天会出现一个巨大的假跳空把突破条件全部触发。第二个问题是数据对齐。成交量单位和价格单位不在一个量级计算量比时要做除法而不是比较绝对值。另外停牌日的处理要提前想好是剔除还是填充我建议剔除因为停牌日没有真实成交填出来的量比是假的。4.2 公式翻译对照把公式翻成脚本时逐个函数对着翻译别自己发挥。下面是对照关系用常见的表格处理库来写。公式写法脚本等价实现MA(C,N)df[close].rolling(N).mean()REF(X,N)df[X].shift(N)HHV(H,N)df[high].rolling(N).max()CROSS(A,B)(A B) (A.shift(1) B.shift(1))IF(COND,A,B)np.where(COND, A, B)COUNT(COND,N)COND.rolling(N).sum()翻译时有两点要特别注意。一是rolling的默认窗口是包含当前行的所以REF(HHV(H,20),1)要写成df[high].rolling(20).max().shift(1)先滚动再位移顺序不能反。二是np.where返回的是数组不是序列记得带上下标重建否则后续的布尔运算会出莫名其妙的错。4.3 信号密度与分布统计把打分算出来之后做三件事数信号次数、看信号的时间分布、看信号的后续统计特征。import numpy as np # 打分计算(省略中间分项, 与公式一一对应) df[score] df[s_trend] df[s_vol] df[s_brk] df[s_amp] # 上穿70作为信号 df[sig] (df[score] 70) (df[score].shift(1) 70) # 1. 信号密度: 按年统计 df[year] df.index.year density df.groupby(year)[sig].sum() print(density) # 2. 时间分布: 看看是不是集中堆在某几个月 df[month] df.index.to_period(M) print(df[df[sig]].groupby(month).size().describe()) # 3. 后续统计: 仅用于校验信号是否扎堆在特定形态 for n in [5, 10, 20]: df[ffwd_{n}] df[close].shift(-n) / df[close] - 1 print(df.loc[df[sig], [fwd_5,fwd_10,fwd_20]].describe())这段代码最有价值的输出不是第三段的统计描述而是第一段和第二段。如果某一年的信号次数是其他年份的三倍说明你的阈值是照着那一年的行情调的属于典型的过拟合。如果信号集中堆在某两个月说明你的条件里有一项在描述特定行情状态换个市场环境就失效。这两种情况比参数本身更值得关注。提示第三段的前瞻统计只用于检查信号是否在时间上扎堆不能当作收益预期。任何单一指标的历史分布都不具备外推性。5. 绕不开的坑未来函数、信号闪烁与数据错位5.1 未来函数的识别与规避未来函数是公式编写里最需要警惕的问题它的特征是用了当前K线还无法确定的信息。最典型的一类是对未来数据的引用另一类是基于全周期极值的回溯计算。识别方法很简单把最后一根K线删掉重新算一遍看历史信号有没有变化。如果删掉最后一根之后倒数第二根K线上的信号变了比如原本是信号现在变成不是那就说明公式里存在未来函数。正常的公式历史信号必须是稳定的不会因为新数据到来而改变。常见的未来函数包括各类ZIG、PEAK、TROUGH系列的转向类函数以及任何REF(X,-N)形式的负向引用。转向类函数在图表上画出来非常漂亮总能在最低点标出买点因为它本身就是用后面的高低点回推的。这类函数在事后复盘里无敌在实时信号里一文不值。我的态度很明确凡是用了转向类的公式一律不碰。5.2 信号闪烁的三种成因没有未来函数信号也可能闪烁。我遇到过三种成因。第一种是边界抖动。打分恰好卡在70附近量能从1.79变到1.81分数从69跳到71信号就闪出来了。解法是加迟滞区间上穿用70触发下穿用65才撤销形成一段缓冲区。第二种是盘中数据未完成。日线指标在盘中会随着价格波动反复变化收盘前看到的信号和收盘后可能完全不同。解法是只在收盘后确认或者在公式里显式要求“当前K线已完成”。第三种是多周期数据不对齐。用周线数据和日线数据混合运算时如果周线的最后一根是不完整的算出来的结果会随每天变化。5.3 常见问题速查表现象最可能的原因排查动作条件写了但从不触发HHV/LLV含当前K线需REF一层检查所有突破类条件的窗口起点信号满屏都是阈值过松或分项权重失效打印各分项得分分布看哪项恒为满分历史信号会变存在未来函数删最后一根K线重算对比历史信号副图曲线飞出画面输出量纲与坐标范围不匹配检查输出值是否被归一化与导出数据算的对不上未复权或复权方式不同统一为同一复权口径后重算回测结果异常好用了转向类函数或用了未来数据逐行检查负向REF和ZIG系列参数微调信号全变用了AND硬连接改打分制加阶梯阈值这张表里最后一条是我最想强调的。参数微调信号全变本质上不是参数问题是结构问题。AND连接的结构天然对参数敏感因为它把每个条件都当成必要条件。改用打分制之后单个参数变化只影响分数高低不会让信号从有到无鲁棒性完全不在一个层次上。6. 关于“和谐源码”这件事我的实际建议6.1 为什么第三方改包不能碰网上流传的所谓改版、去限制版本、二次打包的指标文件我一律不建议使用原因不是道德说教而是三条实打实的风险。第一是安全风险。这类文件来源不可追溯里面夹带什么完全靠运气你把它放进自己的工作环境里跑等于给一个陌生程序开了权限。第二是稳定性风险。二次打包会破坏原有的依赖关系和版本校验短期看着能用一旦主程序更新就直接崩而且崩溃的方式往往很隐蔽——不报错只是算出来的结果悄悄变了你在错误的数字上做了一堆无效分析。第三是责任风险。使用来源不明的修改版本一旦出现数据损失或者纠纷你没有任何追索渠道。替代方案很明确要么用官方正式渠道的版本和内置公式要么自己写。前者省事后者可控。公式语言本身是公开的语法网络上、官方文档里都有完整的函数说明写一个副图指标需要的全部信息都能合法获取。你缺的不是那份源码是读懂源码的能力——而这份能力读十份别人的代码不如自己完整写一份。6.2 自己写公式其实比找源码快这句话不是鼓励是我真实的效率感受。你花两个小时在各种渠道找一份能用的源码找到之后还要花三个小时搞清楚它的参数含义、改掉里面的过拟合阈值、验证它有没有未来函数——总耗时五个小时而且这五个小时的成果你并不完全理解。自己从零写一份把所有中间变量用:拆开每写一行就在图上输出一行看对不对全程大概两个到三个小时。关键是这个过程里每一个参数为什么取这个值、每一个条件为什么这么写你都是清楚的。后面行情变了要调整你知道该动哪里。写公式的顺序我建议固定下来先写数据层取值、平滑再写判断层各分项再写合成层打分或逻辑最后写呈现层柱、文字、颜色。每一层写完立刻输出到副图验证一层对上了再写下一层。这样出错时定位成本极低不会出现一整个公式写完不知道错在哪的情况。6.3 分享与复用的合规姿势如果你想把自己写的公式分享出去有几个实践上的建议。一是参数语义化别叫N1N2N3叫TREND_N、VOL_K别人一看就知道调的是什么这是对使用者最大的善意。二是在代码开头写清适用周期和适用场景日线用的公式搬到分钟线上参数全部要重标不写清楚会误导人。三是明确标注公式里用了哪些可能引起争议的函数如果有就写明白它的问题在哪。至于复用别人的公开公式我的原则是只用逻辑不用原文。看到一份不错的思路理解它想解决什么问题然后用自己的话重写一遍。这个过程里你会自然发现原公式的问题也会加入自己的改进。直接复制原文贴来贴去除了堆砌一份自己都不理解的代码库之外没有别的收获。7. 实操心得几个参数级的经验笔记写到这里把几个不写在文档里但实际很有用的细节补上。关于副图坐标范围固定。默认情况下副图纵轴会随数据自适应导致同一个指标在不同标的上柱子的高度视觉差异巨大。建议在输出层加一条固定为100的水平线作为参照就是前面代码里那句STICKLINE(1,0,100,...)的作用把纵轴撑到固定范围这样不同标的之间的分数高低可以直接横向比较。关于分项得分的事后追溯。用好这份指标的关键不是看总分是看分数构成。同样是75分趋势30加量能30加突破15加蓄势0和趋势18加量能12加突破25加蓄势15含义完全不同。前者是趋势和量能双强突破不足后者是蓄势充分但趋势还没跟上。所以我在输出层坚持做分项堆叠显示就是为了让这个构成信息随时可见。关于迟滞区间的具体设置。触发阈值和撤销阈值之间留多少合适我的经验是留总分的5%到8%。警戒线70撤销线设在64到66之间。留太窄起不到防抖作用留太宽信号会拖得很长。关于跨标的参数复用。绝对量类的参数振幅百分比、成交量绝对值跨标的基本不能直接复用一定要改成相对量振幅收缩比、量比倍数。相对量参数虽然也需要标定但至少在同一类标的之间是可迁移的。这一条我踩过很多次坑一开始图省事用固定百分比换一个波动率不同的标的信号要么全灭要么全亮。关于版本管理。公式改到第三版之后你一定会忘记第一版长什么样。建议每次大改之前把旧版另存一份文件名带上日期和改动摘要。这个习惯看起来笨但当你需要回溯“上个月那个参数为什么改掉”的时候能省掉大量时间。最后分享一个小技巧。当你调参数调到怀疑自己的时候把整个指标简化到只剩一两个分项先确认基础逻辑没问题再一层层加回去。绝大多数“参数怎么调都不对”的情况根源都在某个分项的条件写错了——比如前面说的HHV含当前K线、或者REF的位移方向反了。把复杂公式拆回最小可运行单元逐层验证是排查这类问题最快的方法比盯着几十行代码硬看有效得多。