简介面向通达信公式开发与缠论量化研究者的DLL插件源码包聚焦相邻K线包含关系的自动判断与合并为笔、线段、中枢等结构构建提供基础数据。资源共24个文件以h头文件、cpp源文件为主辅以obj、pdb、ilk等编译中间文件及dsp/dsw工程配置并附dll、plg成品与使用说明压缩包约693KB。已有5767人学习下载说明其在通达信插件开发与缠论实现方向具有一定参考价值。源码围绕K线数据读取、包含条件判定、合并后序列更新、插件计算函数接口对接及结果回传图表等环节展开读者可借此理解缠论包含处理从规则到DLL落地的组织方式也能借鉴VC工程配置与调试产物的对应关系适合具备C基础、希望复现并扩展笔段中枢逻辑的开发者研读。对学习缠论规则如何转化为可编译插件、排查合并边界条件与验证结构输出均有实际帮助。1. 缠论K线包含处理为什么合并K线是分型成立的前提缠论K线包含处理经常被当成一个纯几何的小工具两根K线谁套住谁取个极值就算完事。可一旦把这段逻辑装进本地行情终端的插件里对着真实日线数据跑就会撞见一个反直觉的结果——同一段行情合并顺序换一下最后得到的标准K线序列能差出十几根顶底分型的位置跟着整体位移。缠论后续的笔、线段、中枢全部建立在这根被合并过的K线上合并错了后面全是假的。这就是为什么翻缠论源码最值得逐行读的不是分型函数而是前面那段看起来最笨的合并函数它决定了后面所有结论的真假也是整个插件里唯一没有“肉眼可验证”的环节。2. 包含关系判定从几何定义到一行比较函数2.1 包含的严格定义与四种边界情形把一根K线抽象成区间第 i 根写作 [L_i, H_i]L 是最低价、H 是最高价。包含关系的几何定义只有一句话如果一根K线的高低点完全落在另一根的高低点区间之内两者构成包含。写成不等式就是 H_a ≥ H_b 且 L_a ≤ L_b此时 a 包含 b。拆开看有四种情形其中两种容易被忽略。第一种是严格包含区间真包含没有争议。第二种是反向包含b 包住 a判定式反过来成立。第三种是完全相等两根K线的高低点一模一样这在涨跌停一字板或者极度缩量的横盘里并不罕见按上面的不等式它同时满足 a 包含 b 和 b 包含 a方向就没法唯一确定。第四种是“半包含”只有高点相等而低点更高或者只有低点相等而高点更低。按非严格不等式的定义这也算包含因为一方的区间是另一方的超集。这第四种情形是分歧的源头。有的实现只在严格包含时才合并半包含直接放过有的实现按 ≥ 和 ≤ 一律合并。两种约定在高位横盘、连续小星线的段落里会给出不同的K线数量进而让分型的确认位置前后差一两根。这里没有绝对的对错缠论原文也没把这件事钉死真正的红线是整个系统必须自洽——分型、笔、中枢用的一定是同一份合并结果不能分型用严格版、画图用宽松版。// 包含关系判定返回两根K线的关系 enum class Relation { NONE, // 无包含关系 A_CONTAINS_B, // 前一根包含后一根 B_CONTAINS_A, // 后一根包含前一根 EQUAL // 高低点完全相同互相包含 }; inline Relation judgeRelation(const Bar a, const Bar b) { const bool aCoversB (a.high b.high) (a.low b.low); const bool bCoversA (b.high a.high) (b.low a.low); if (aCoversB bCoversA) return Relation::EQUAL; // 全等需靠方向裁决 if (aCoversB) return Relation::A_CONTAINS_B; if (bCoversA) return Relation::B_CONTAINS_A; return Relation::NONE; }这段函数的全部价值在于把“全等”单独拎出来。如果不区分 EQUAL直接让 aCoversB 优先返回那么在连续一字板的行情里合并方向会被固定成某一种向上段落里本该取高高、却因为走了包含分支取了别的值K线序列的性质就被破坏了。把这个分支显式暴露出来逼迫调用方在 EQUAL 时去查方向是后面所有逻辑能立住的前提。2.2 全等K线、零振幅与除权后的浮点陷阱真实数据里最会咬人的不是标准K线而是那些退化的K线。一字涨停时 H L一根K线退化成一个点它会被任何覆盖这个价位的K线包含也会被同价位的前一根包含。如果连续五天一字板五根K线在几何上是同一个点合并逻辑必须能处理这种“多重全等”——每加一根都要和当前的合并结果比而不是两两配对。比一字板更隐蔽的是浮点误差。很多行情源内部用“分”为单位的整数存价格读出后转成 double 参与计算。11.11 这个十进制小数在二进制里没有精确表示转出来可能是 11.109999999999999。当一根K线的高点来自不同的计算路径比如复权再还原、或者跨周期重采样两个“应该相等”的价格在 double 层面会差一个 1e-14 量级的值。非严格不等式在遇到这种差异时会把本该判为包含的两根K线判成不包含合并结果和肉眼看到的图直接对不上。复权是另一个放大器。前复权等于原始价乘一个随时间变化的因子除权日附近的K线在复权后会整体平移原本不构成包含的两根可能变成包含原本包含的可能被推开。切换复权模式却不重算合并结果图上就会出现前半段和后半段“接不上”的裂痕。常见的处理思路有两条。一条是用相对容差做比较把所有价格比较都套一层近似函数另一条是干脆把价格整数化乘以一个固定倍数比如 10000转成 int64 再比。后一条在行情终端这种价格精度固定的场景里更稳因为它把“近似相等”变成了真正的相等不存在传递性被破坏的问题。相对容差的问题在于a≈b、b≈c 未必能推出 a≈c一旦用在排序或聚类上就会出怪事。2.3 比较函数怎么写相对容差与整数化两种路线如果走相对容差路线核心是把绝对误差按量级缩放。3 元的股票和 300 元的股票允许的误差不该是同一个数值否则低价股会被大量误判为相等高价股则几乎不会有相等判定。// 相对容差比较eps 按两者量级缩放 inline bool nearlyGE(double x, double y, double relEps 1e-9) { const double scale std::max({1.0, std::fabs(x), std::fabs(y)}); // x y 在容差意义下成立允许 x 比 y 小一点点 return x y - relEps * scale; } inline bool nearlyLE(double x, double y, double relEps 1e-9) { return nearlyGE(y, x, relEps); }relEps 取 1e-9 是个偏保守的值对应 300 元的股票上约 3e-7 的绝对误差足以覆盖双精度转换噪声又不至于把 0.0001 元的真实价差吞掉。scale 里带上 1.0 这个下界是为了避开价格接近 0 时容差退化为 0 的情况——虽然股票价格不会到 0但指数、期货连续合约的历史数据里偶尔会出现极端值。整数化路线的写法更直接读取时统一 round(price * 10000) 转 int64之后所有判定都用整数比较输出时再除回去。代价是精度被锁死在万分之一遇到需要更高精度的品种比如某些报价到小数点后四位的合约就得改倍数。好处是彻底消灭了浮点比较的所有玄学也让 EQUAL 判定变得可靠。两条路线在同一个插件里最好只用一条不要一半用容差、一半用整数否则合并和分型两个模块对“相等”的理解会出现分歧。真实项目里我倾向于整数化因为缠论的包含处理本质上是在做离散的区间比较把它拉到浮点域里没有任何收益只会引入不可复现的边界问题。3. 合并方向与滚动向上取高高、向下取低低方向到底从哪来3.1 方向由前两根已处理K线决定不是由当前两根合并规则本身只有两句话向上处理时合并K线的高点取两者较高、低点也取两者较高向下处理时高点取两者较低、低点也取两者较低。真正难的是方向从哪来。当前正在比较的这两根K线本身就是包含关系它们的相对位置没有方向信息方向必须往外找。标准做法是看已经处理完的序列的最后两根。设输出序列为 out长度为 n当新来一根原始K线与 out[n-1] 构成包含时方向由 out[n-1] 与 out[n-2] 的关系决定如果 out[n-1].high out[n-2].high同时 low 也更高说明前一段是向上的按向上合并反之按向下合并。这里有个初值问题。序列的最前面两根K线还没有“前两根”没有方向可查。有的实现默认向上有的实现把前两根直接原样放进输出序列、从第三根才开始判包含。后者更稳因为前两根如果不构成包含本来就不需要合并如果构成包含说明数据从第一根开始就是混沌的此时按任一方向合并都会引入偏差只能认了。还有个容易写错的地方方向判断要看的是已经处理完的序列不是原始序列。原始序列里第 i-1 根和第 i-2 根可能构成包含用它们判方向等于用一个尚未确定的量去确定另一个量。方向必须建立在“已经保证相邻不包含”的序列上这个不变量是整个算法的地基。// 用已处理序列的最后两根推断方向 // 返回 true 表示向上false 表示向下 bool inferDirection(const std::vectorBar out) { if (out.size() 2) return true; // 初值无依据时默认向上 const Bar prev2 out[out.size() - 2]; const Bar prev1 out[out.size() - 1]; // 已处理的相邻两根必不包含因此只需比较高点即可判定走向 return prev1.high prev2.high; }注意最后一行只比了 high。因为 out 里的相邻两根已经保证不构成包含那么它们的区间关系只有两种一根整体在另一根上方或者整体在下方。此时 high 的大小关系与 low 的大小关系必然一致比一个就够。如果这里出现了 high 高但 low 低的情况说明前面的合并流程已经出错了属于不变量被破坏应该在调试期用断言拦下来。3.2 滚动合并的实现合并结果必须回头再比顺序处理是唯一正确的遍历方式从左到右每来一根就和输出序列的末尾比。如果构成包含按方向合并进末尾如果不构成包含直接追加到输出序列。问题出在“合并进末尾”之后。合并会改变末尾K线的高点和低点改变后的区间有没有可能与 out[n-2] 又构成包含理论上不会——合并只会沿方向扩展区间向上合并时新低点变高、新高点变高它相对 out[n-2] 的位置只会更靠上不会退回去把 out[n-2] 包住。向下合并同理。所以单次合并之后不需要回头这个性质保证了算法是线性的。但工程实现里有两种写法会破坏它。一种是分治式先把原始序列两两配对合并一轮再对结果两两合并一轮。这种做法把顺序依赖彻底打乱了因为某一对合并时用的方向可能是错的错误会沿着层数指数放大。另一种是“先标记所有包含对再统一消除”听起来更高效实际上一根K线可能同时被前后两根包含标记完再消除会漏掉合并后的新包含关系。还有一种情况需要额外保护浮点容差介入之后理论上不成立的“回头包含”有可能真的出现。合并后的新区间因为容差被判定为与 out[n-2] 构成包含此时如果不处理输出序列就会带着一个违反不变量的元素继续往下跑后面所有的分型判断都建在流沙上。稳妥的做法是在合并后加一次回退检查如果真的发生了就再合并一次同时打日志——这类事件在生产环境里应该极其罕见一旦频繁出现说明容差参数或者数据源有问题。void mergeBars(const std::vectorBar in, std::vectorMergedBar out) { out.clear(); for (size_t i 0; i in.size(); i) { if (out.empty()) { out.push_back(MergedBar::from(in[i], i)); continue; } MergedBar last out.back(); Relation rel judgeRelation(last.bar, in[i]); if (rel Relation::NONE) { out.push_back(MergedBar::from(in[i], i)); // 无包含直接追加 continue; } // 构成包含方向由输出序列的最后两根决定 const bool up inferDirection(barsOf(out)); if (up) { last.bar.high std::max(last.bar.high, in[i].high); // 高高 last.bar.low std::max(last.bar.low, in[i].low); // 低高 } else { last.bar.high std::min(last.bar.high, in[i].high); // 高低 last.bar.low std::min(last.bar.low, in[i].low); // 低低 } last.endIdx i; // 索引区间右端点后移 last.bar.volume in[i].volume; // 成交量累加 last.bar.close in[i].close; // 收盘价跟最新 // 回溯保护容差可能导致合并后又与倒数第二根构成包含 while (out.size() 2 judgeRelation(out[out.size() - 2].bar, last.bar) ! Relation::NONE) { // 合并倒数第二根与最后一根方向仍由更前一根决定 mergeLastTwo(out); } } }代码里最值得说的是 last.bar.close in[i].close 这一行。合并K线的收盘价必须跟着最新那根走因为收盘价代表的是这根合并K线右端点时刻的市场状态。如果保持第一根的收盘价后续所有依赖收盘价的指标比如 MACD 的面积计算都会整体偏移。同理high 和 low 用的是极值open 通常保持第一根的开盘价不变因为开盘代表的是这段区间的起点。那个 while 循环是防御性的正常数据下一次都不会进。但它的存在让整个函数在面对脏数据时不至于输出一个违反不变量的序列代价是每次合并多一次比较可以接受。3.3 合并K线保留哪些字段时间、成交量与索引映射合并K线不是简单的高低点二元组它需要携带足够的信息让下游模块能追溯回原始K线。至少要有四个字段合并后的 high/low、代表时间、累计成交量、原始索引区间。代表时间取哪一根各家实现不同。取最后一根的时间即 endIdx 对应的时间更符合“这根K线代表到此刻为止”的语义画图时也不会出现K线跑到未来位置的情况。取第一根的时间则更接近“这根K线从何时开始”在计算持有时长时有意义。两种都能自洽关键是全系统统一。索引区间 [startIdx, endIdx] 是最容易被忽略、又最不能省的字段。分型确认之后要往前找对应的原始K线画标记如果没有这个映射就只能靠时间戳去原始序列里二分查找遇到停牌、除权导致的时间不连续时会找错。有了区间分型落在哪根合并K线上就直接对应原始序列的一段画图、统计、回溯全都是一次数组访问。成交量要不要累加也有讲究。缠论原文讨论的是价格形态不涉及量所以从纯理论角度合并K线的成交量没有定义。但实战中几乎所有人都会在合并K线上看量能累加是最符合直觉的选择——它保持了“合并前后总量守恒”这个可验证的性质调试时可以拿它当校验位用。字段取值规则用途high向上取 max向下取 min分型、笔的端点判定low向上取 max向下取 min同上open保持区间第一根少数依赖开盘的指标close跟随区间最后一根收盘价类指标volume区间内累加量能分析、守恒校验startIdx / endIdx原始序列下标区间回溯定位、画图这张表里每一行都对应过一次真实的翻车。尤其是 close 和 volume 那两行写反了不会崩溃、不会报错只会让指标数值慢慢偏离等到发现时已经找不到是哪一版改坏的。4. 插件接入DLL 如何在宿主进程里拿到K线并回写指标4.1 宿主与插件的接口契约函数表、结构体版本与导出名本地行情终端的插件体系通常是一个进程内 DLL。宿主启动时扫描插件目录LoadLibrary 加载GetProcAddress 拿到约定的导出函数之后每次需要计算指标就调用一次。插件自己不负责取数据数据由宿主通过回调函数表传进来——这是一条单向依赖插件不链接宿主的任何库只依赖一张纯 C 的函数指针表。这张表至少包含三组能力查询K线数量、按索引取第 i 根K线、回写计算结果。取数据的接口签名一般是 int GetBar(int index, Bar* out)返回实际填充的字节数或者错误码。回写有两种模式一种是按下标写数值适合均线这类逐点输出一种是注册自定义绘图对象适合画线段、中枢矩形这类图元。结构体版本是最容易被跳过、出事又最难查的一环。宿主编译时的 Bar 结构和插件编译时的可能不一致字段顺序、对齐、甚至字段数量都可能不同。常见的做法是在结构体头部放一个 size 字段宿主填自己认为的大小插件读出来校验不匹配就直接拒绝加载并返回错误码。这比让插件去读一个偏移错位的字段、拿到一堆垃圾值再崩溃要体面得多。// 宿主与插件之间的数据契约纯 C固定宽度类型 #pragma pack(push, 8) struct PluginBar { int32_t date; // YYYYMMDD或与宿主约定的日期编码 int32_t time; // HHMM 或分钟数日线填 0 double open; double high; double low; double close; double volume; double amount; }; #pragma pack(pop) // 宿主提供的回调表插件只读不写 struct HostApi { int32_t structSize; // sizeof(HostApi) int32_t apiVersion; // 宿主实现版本 int32_t (*getBarCount)(int32_t period); int32_t (*getBar)(int32_t period, int32_t index, PluginBar* out); int32_t (*setOutputValue)(int32_t index, double value); void* reserved[8]; // 预留保证向后兼容 };structSize 和 apiVersion 两个字段是有意冗余的。前者防的是编译期布局变化后者防的是语义变化——比如某天宿主把 volume 的单位从“手”改成“股”结构体大小不变但数值差 100 倍。插件读到 apiVersion 不匹配时至少知道该走兼容分支或者直接拒载。reserved 数组是给未来的自己留的后路。第一次写插件的人几乎都会觉得预留没用等到需要加一个“取复权因子”的接口时才发现所有已发布的插件都是按老的函数表编译的加字段会让老插件读到越界内存。预留八个指针的成本可以忽略收益是接口能平滑演进好几年。4.2 K线数据的内存布局对齐、时间字段与 32/64 位混编PluginBar 里全部用固定宽度类型是为了避开 int 在不同编译器下宽度不一致的问题。日期用 int32 存 YYYYMMDD比用字符串省空间也比较方便做范围查询但如果宿主内部用的是从某个基准日起的天数或者秒级时间戳就必须在接口层统一不能让插件去猜。对齐是个隐蔽的坑。#pragma pack(8) 保证了 double 落在 8 字节边界上访问效率最好如果宿主那边用了 pack(1)两边算出来的 sizeof 就会不同getBar 按错误的步长去读数组从第二根开始全部错位而第一根永远是对的——这种 bug 极难定位因为打印第一根数据看起来完全正常。用 structSize 校验能第一时间拦住它。32 位宿主加载 64 位 DLL 会直接失败反过来也一样这是操作系统层面的限制不是代码问题。麻烦的是插件目录里同时放了两个版本、宿主按文件名模糊匹配加载了错的那个。稳妥的做法是导出函数里带一个 GetPluginBitness()宿主拿到之后和自己的位宽比对不匹配就在日志里写清楚而不是让用户对着一个“加载成功但没输出”的空图发愣。数据所有权也得说清楚。getBar 返回的指针如果指向宿主内部缓冲区插件不能长期持有下一次行情推送就可能被覆盖如果要缓存必须自己拷贝。反过来setOutputValue 传的是值不是指针生命周期问题就消失了。接口设计上凡是能传值的都别传指针能传 const 引用的别传可写引用这能消掉一大半跨模块的内存事故。4.3 增量刷新最后一根K线的回滚与重算实时行情是持续推送的最后一根K线在收盘前会不断变化高点可能被刷新、低点可能被打破、成交量一直在涨。这意味着合并结果也在变——刚才还和倒数第二根构成包含下一秒可能就不构成了。如果每次推送都全量重算几千根K线的合并加上后面的分型、笔在低端机器上会明显卡顿如果只在末尾追加、不回头最后一根K线的合并在方向翻转时会算错。可行的做法是维护一个“确认边界”。收盘已确定的K线其合并结果不会再变把它们冻结起来边界之后的 N 根原始K线保留原始数据每次推送时全部丢弃重算。N 的取值要覆盖最坏情况下的连续包含链——理论上一条包含链可以很长连续几十根小星线互相包含并不罕见。工程上取 50 到 100 根是常见区间再大就退化成全量重算了。struct MergeCache { std::vectorMergedBar confirmed; // 已冻结的合并结果 size_t confirmedRawCount 0; // 冻结时已消费的原始K线根数 bool direction true; // 冻结时刻的方向状态重算的起点 // 每次行情推送调用丢弃 confirmedRawCount 之后的所有合并结果 void rollback(const std::vectorBar raw) { const size_t keep (confirmedRawCount kTailWindow) ? confirmedRawCount - kTailWindow : 0; confirmed.resize(/* 与 keep 对应的合并根数 */); confirmedRawCount keep; // 方向必须跟着回滚否则重算会用错方向 direction inferDirection(confirmed); } };rollback 里最容易被漏掉的是 direction 的重置。方向是随序列推进不断更新的状态量如果回滚时只截断了 confirmed 数组却没恢复方向重算的那几十根K线会全部按回滚前的方向合并。表现出来就是行情跳一下、图上的K线形态突变一次用户会觉得这软件“有鬼”。这类状态不一致属于典型的黑匣子问题只能靠把状态和它依赖的数据绑在一起回滚来根治。缓存失效的条件也要列全切换周期、切换复权模式、切换品种、切换数据源、盘中除权。少列一个就会出现“日线切换到 5 分钟再切回来日线的合并结果变成了 5 分钟的形状”这种离谱现象。稳妥的写法是给缓存加一个 key把上述所有维度都编码进去key 变了就整个丢弃重建不做局部复用。5. 排查清单包含处理最容易翻车的五个现场5.1 数据侧复权切换与停牌缺口现象前复权和不复权来回切换几次之后图上的分型标记开始对不上越靠左的历史K线偏移越大同一个顶分型在不同复权模式下落在不同的日期上。原因合并结果是缓存的切换复权模式时只重算了显示的K线数据没有让合并缓存失效。插件里存着旧复权模式下的索引映射新数据按老映射去定位偏移量随历史长度累积。解决把复权模式编进缓存 key切换时整块丢弃重建同时在插件入口处记录一次当前复权模式和缓存里的比对不一致就强制走全量重算。这类问题的血泪经验是任何影响价格的开关都必须传播到最底层的预处理缓存。现象某只股票停牌半年后复牌复牌前后的K线合并结果看起来正常但一遇到复牌当天的巨幅跳空方向判断就反了紧接着的几根K线被并进了错误的方向。原因停牌期间没有K线复牌首根与前一根之间的时间跨度是半年但合并算法只看价格不看时间把这两根当成连续交易日处理。跳空导致的高低点关系被误读为趋势反转。解决在原始数据层面就标注出交易日不连续的位置合并时遇到超过阈值的时间间隔强制重置方向、把当前K线作为新段落的第一根。阈值取多少个自然日要看品种A股一般 10 个自然日以上就该考虑。5.2 算法侧方向判断与滚动终止现象合并后的序列里出现了相邻两根仍然构成包含的情况分型函数在这种数据上会输出连续两个顶或者两个底笔的划分直接乱掉。原因增量重算时只回溯了固定根数而包含链的长度超过了这个窗口。回滚点落在一个仍然处于包含关系中的位置重算从这里开始方向初值是根据不完整的历史推出来的。解决不要用固定窗口拍脑袋而是在回滚点处加一次校验——检查回滚点前后的两根是否构成包含构成就再往前退一根直到找到一段确定的非包含边界。这个循环的上限设一个硬阈值比如 500 根超过就放弃增量、转全量重算。现象连续一字板的票合并后的K线数量比预期少很多成交量被吞掉一大截图上看起来像少了好几天的数据。原因全等K线H L 且与前一根完全相同在 judgeRelation 里返回 EQUAL合并逻辑按方向取极值取完还是同一个价格但 endIdx 往前推了成交量累加进去了而视觉上K线没有任何变化看起来像“消失了”。解决这是正确行为不是 bug但必须在UI上体现出来——把合并K线的数量或者索引区间显示出来或者在成交量柱上按区间宽度做视觉区分。真正需要修的是当 EQUAL 连续出现几十次时索引区间会变得极大画图时如果按区间宽度算K线宽度会画出一根超宽的柱子需要给宽度设上限。5.3 工程侧缓冲区越界与多周期串数据现象插件在日线下运行正常切到 5 分钟线就崩溃崩溃位置在合并函数的某个 vector::back() 调用上。原因5 分钟线的K线数量是日线的几十倍插件内部有个固定大小的静态数组比如 char buf[65536]按根数分配空间日线够用分钟线直接越界覆写了相邻内存。解决所有和K线数量相关的容器一律用动态分配禁止静态数组在入口处先调 getBarCount 拿到实际根数做一次上限检查比如超过 200 万根直接返回错误把“数据量超预期”变成一个明确的错误码而不是一次内存踩踏。现象同时打开两个窗口一个看日线一个看 5 分钟线两个窗口都加载同一个插件结果 5 分钟窗口的图上出现了日线的分型标记。原因插件用了全局静态变量存合并结果和索引映射两个窗口在同一个进程里共享同一份静态存储。后计算的覆盖先计算的。解决所有状态必须绑定到一次调用上下文不能用全局变量。如果宿主接口没提供上下文句柄就在插件内部按周期和品种做一个 mapkey 里带上周期、品种、复权模式取不到就新建。同时要注意线程安全——行情线程和绘图层可能并发访问map 外面得加锁或者用线程局部存储。6. 设计取舍全量重算、增量维护还是两级缓存把这套东西写完并且跑稳之后最值得回头想的一件事是合并这步到底该花多少工程成本。三种路线各有明确的适用面没有一条能通吃。路线实现成本单次耗时5000根状态一致性风险适用场景每次全量重算最低几十行毫秒级无每次都是纯函数盘后分析、回测、K线数量少增量 回滚窗口中等需要设计回滚点微秒级中状态多容易漏回滚实时行情、多窗口两级缓存含分型层高要把下游也算进去纳秒级命中高失效条件成倍增加高频刷新、复杂指标叠加我一般的判断标准是看“用户会不会感觉到”。盘后打开软件看图全量重算几千根K线对现代CPU来说就是一次眨眼为它设计增量缓存纯属自找麻烦而且增量逻辑一旦有 bug排查成本远高于省下的那点时间。只有实时推送的场景才值得上增量因为每三秒一次的全量重算在低配机器上会累积成可感知的卡顿。验证合并结果对不对比优化它更重要。我固定会跑四组性质检查任何一组挂了就直接回去看代码不看输出图。第一组是不变量合并后的序列里任意相邻两根都不能构成包含。这一条是算法的定义挂了说明方向或滚动逻辑有问题。第二组是守恒合并前后所有K线的最高价的最大值、最低价的最小值必须相等成交量总和必须相等。这两条能抓住绝大部分字段更新遗漏。第三组是单调合并K线的代表时间必须严格递增endIdx 必须严格递增且覆盖全部原始下标。第四组是幂等对合并结果再跑一次合并输出应该完全不变——因为相邻已经不含包含关系第二次跑应当原样返回。这四组检查加起来不到一百行但它拦下过我至少三次自以为没问题的改动。有一次是 close 字段跟错了K线前两组全过、第三组也过最后是靠第四组的幂等性发现的——第二次合并时 close 又跟了一次值就变了。从那以后我每次改合并或者分型相关的代码都强制走一遍这四组断言不管改动多小。增量缓存省下的时间是给用户的这几行断言省下的时间是给自己的。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?