ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

缠论可视化插件C++实现:分型、笔、中枢算法与Qt绘制

缠论可视化插件C++实现:分型、笔、中枢算法与Qt绘制 简介一套基于缠中说禅理论的炒股可视化分析插件使用C开发并集成C语言模块面向股市技术分析爱好者与量化插件开发者解决缠论结构识别与图形化呈现问题。包体共31个文件、约217KB包含10个头文件与8个C源码文件、7个文本说明、3个图片及editorconfig、gitignore、license等配置文档源码覆盖K线处理、笔、中枢、线段等核心逻辑文本文件提供缠论主图与选股策略说明图片可预览可视化效果配置类文件方便跨平台工程维护与规范协作。当前已有367人学习适合希望快速理解缠论程序化表达、参考C插件架构或扩展自定义分析模块的读者可借此获得一套结构完整的缠论可视化实现思路与可直接编译调试的工程框架。1. 缠论可视化插件的难点在算法链不在画K线学缠论的人有相当一部分不是卡在“看不懂”而是卡在“划分对不上”同一个顶分型A说成笔B说不成笔问题往往出在原始K线的包含关系没处理干净。基于缠中说禅理论的缠论可视化插件C设计源码本质上是一条计算链原始K线到包含合并再到分型、笔、线段、中枢最后由C把这条链上的每一层结果画到K线图上。可视化只是最后一步真正决定插件有没有价值的是前面那段递归结构是否稳定。这个方向适合自研行情分析工具、需要对照缠论算法源码改造的人也适合已经用行情软件画线、但忍受不了手动反复改笔的工程师。可视化插件要解决的是“人工画图不可复现”的问题。手动画一笔画错了只能撤销重来而且不同人画的结果很难对齐用C把算法固化之后同一个输入永远得到同一套分型、同一组笔和中枢参数一变重算一次即可。另一个反直觉的点是这类插件最花时间的地方不在绘图接口而在分型的边界条件——比如相邻三根K线中中间K线的高点和低点都高于两侧时才算严格顶分型这个条件在真实行情里经常被一根长下影线破坏。下面从数据结构开始推演把“缠论模型、C实现、Qt可视化、参数验证”这条路线走通。算法部分会给出可直接改造成自己项目的代码骨架绘图部分用自绘窗口说明核心交互最后落在一组排错和回归技巧上。2. 缠论模型与C数据结构设计先定义笔和中枢的代码形态2.1 从分型到中枢的递归链在插件里的映射缠论和均线、MACD这类指标的差异在于它没有一个“单一公式”而是由多级结构逐层推导。插件里如果把每一步都写在一个大循环里代码会越改越脆。通常做法是先把概念映射成数据结构再让计算过程按固定顺序跑包含合并是分型的前提分型是笔的前提笔是中枢的前提每一层都消费上一层的结果。设计时要把这条依赖关系直接写进模块划分里而不是让各函数随意互相调用。缠论概念代码形态关键字段说明K线KLinets,high,low,open,close原始行情算法只读不修改特征K线FeatureKLineorigIdx,high,low包含合并的结果origIdx指向原始K线分型Fractaltype,idx,priceidx对应原始K线序列绘制时对齐笔BistartIdx,endIdx,startPrice,endPrice一个顶分型到一个底分型的连接中枢ZhongShuzg,zd,startIdx,endIdx连续三笔重叠区间表格里最容易被忽略的是idx到底是原始K线下标还是特征K线下标。绘图、K线tooltip、时间对齐都直接依赖原始K线下标特征K线只是中间计算结果如果分型用了特征K线的数组下标画图时还要再做一次映射平白多一层出错可能。所以整个项目里凡是暴露给绘图层的索引统一使用原始K线序列下标。2.2 核心头文件的数据结构定义下面这段可以直接放进头文件是缠论可视化插件里最基础的一组类型// K线插件只依赖OHLCV时间戳用int64_t避免字符串比较 struct KLine { int64_t ts; double open, high, low, close; double vol; }; // 包含处理后的特征K线 struct FeatureKLine { int origIdx; // 对应原始K线的下标 double high, low; }; enum class FractalType { Top, Bottom }; struct Fractal { FractalType type; // 顶分型/底分型 int idx; // 原始K线序列下标不是特征K线下标 double price; // 顶分型记录高点底分型记录低点 }; // 笔两个相邻分型之间的连接 struct Bi { int startIdx, endIdx; double startPrice, endPrice; }; // 中枢连续三笔的重叠区间 struct ZhongShu { double zg, zd; // 上沿、下沿 int startIdx, endIdx; };注意Fractal.idx指向原始K线后Bi的构造和绘制都能直接复用ZhongShu的zg和zd对应“三段笔高点里的最低值”和“三段笔低点里的最高值”这两个值区间不为空时中枢才成立。下标一律用int而不是size_t因为分型比较时经常要做i - 1这类减法无符号整数在边界上容易变成很大的数。2.3 计算器模块划分与执行顺序把缠论算法封装成一个ChanCalculator类界面和命令行工具共用struct ChanParams { int minK 1; // 笔最小间隔K线数 bool strictFractal true; // 是否使用严格分型定义 }; class ChanCalculator { public: void setData(const std::vectorKLine klines); void setParams(const ChanParams params); void recalculate(); const std::vectorFractal fractals() const; const std::vectorBi bis() const; const std::vectorZhongShu zhongshus() const; private: std::vectorFeatureKLine mergeContainedK() const; std::vectorFractal findFractals(bool strict) const; std::vectorBi buildBis(int minK) const; std::vectorZhongShu findZhongShus() const; std::vectorKLine klines_; std::vectorFeatureKLine featureK_; std::vectorFractal fractals_; std::vectorBi bis_; std::vectorZhongShu zhongshus_; ChanParams params_; };管线执行顺序写在recalculate()里四步依次完成void ChanCalculator::recalculate() { featureK_ mergeContainedK(); // 1. 包含合并 fractals_ findFractals(params_.strictFractal); // 2. 分型识别 bis_ buildBis(params_.minK); // 3. 笔划分 zhongshus_ findZhongShus(); // 4. 中枢提取 }这样的分层有个好处改分型定义不会波及绘图代码加一个“背驰标记”只需要在算法层增加一个输出数组。klines_在recalculate()执行期间只读外部不能直接修改避免重算过程中界面线程读到半成品数据。3. 用C实现分型识别、笔划分与中枢提取3.1 K线包含关系合并的方向判断包含关系是缠论里最容易被略过的步骤。两根K线中一根的高点不低于另一根的高点、低点也不高于另一根的低点就认为存在包含。向上处理时取两根K线高点里的较大值、低点里的较大值向下处理时取高点里的较小值、低点里的较小值。方向则由前一根特征K线与更前一根的位置关系决定// 判断两根K线是否包含参数分别代表高点、低点 bool isContained(double h1, double l1, double h2, double l2) { return (h1 h2 l1 l2) || (h2 h1 l2 l1); } std::vectorFeatureKLine mergeContainedK(const std::vectorKLine kl) { std::vectorFeatureKLine out; for (int i 0; i (int)kl.size(); i) { FeatureKLine fk{i, kl[i].high, kl[i].low}; // origIdx i if (out.empty()) { out.push_back(fk); continue; } FeatureKLine last out.back(); if (!isContained(last.high, last.low, fk.high, fk.low)) { out.push_back(fk); // 无包含直接接入序列 continue; } // 方向判定比较前一根特征K线与当前特征K线的高点 bool up out.size() 2 ? true : out[out.size() - 2].high last.high; if (up) { last.high std::max(last.high, fk.high); last.low std::max(last.low, fk.low); } else { last.high std::min(last.high, fk.high); last.low std::min(last.low, fk.low); } // 合并结果写回last不新增特征K线 } return out; }逻辑说明last代表已经处理到的最新特征K线如果新K线与它包含就把新K线的信息合并进last等于这一组K线被压成一根。合并后origIdx保留的仍是这一组第一根K线的下标绘图时把分型画在这一组K线的最左端还是最右端取决于你希望它对齐到哪根K线两种实现都有人用建议在配置里注明。方向判定需要注意这里用特征K线的高点比较而不是收盘价比较。在暴跌行情里收盘方向滞后一拍特征K线高点方向反应更快但跳空缺口多的个股上两种模式会给出不同结果。源码里如果只有一个mergeDirection最好把它做成可配置项默认按高点方向close方向作为备选。3.2 分型识别严格定义与宽松分型的取舍分型识别的标准写法是特征K线序列中i的高点同时大于i-1和i1的高点且低点也同时大于两侧的低点才是严格顶分型底分型反之std::vectorFractal findFractals(const std::vectorFeatureKLine fk, bool strict) { std::vectorFractal res; int n (int)fk.size(); for (int i 1; i 1 n; i) { const auto a fk[i - 1]; const auto b fk[i]; const auto c fk[i 1]; bool top b.high a.high b.high c.high; bool bottom b.low a.low b.low c.low; if (top (!strict || (b.low a.low b.low c.low))) { res.push_back({FractalType::Top, fk[i].origIdx, b.high}); } else if (bottom (!strict || (b.high a.high b.high c.high))) { res.push_back({FractalType::Bottom, fk[i].origIdx, b.low}); } } return res; }参数说明stricttrue时顶分型要求高低点同时突破strictfalse时只要求高点突破对低点放行。宽松分型会多识别出边界型分型笔数增加震荡行情里会出现频繁翻转。日线级别建议开严格模式因为包含合并后K线数量已经减少再放宽会让笔太碎分钟线数据量大反而要用宽松模式减少漏识别。分型识别还有一个常见坑循环从1到n-2不会照顾到序列头尾如果数据长度小于3循环直接跳过调用方要自己处理空结果。3.3 笔划分的最小间隔参数minK分型出来后按“顶底交替”原则构造笔遇到同向分型只保留更极端的那个遇到反向分型且分型间隔大于minK就形成一笔std::vectorBi buildBis(const std::vectorFractal fs, int minK) { std::vectorBi res; if (fs.size() 2) return res; int cur 0; for (int i 1; i (int)fs.size(); i) { if (fs[i].type fs[cur].type) { // 同向分型保留价格更极端的分型作为候选端点 if ((fs[i].type FractalType::Top fs[i].price fs[cur].price) || (fs[i].type FractalType::Bottom fs[i].price fs[cur].price)) { cur i; } } else if (fs[i].idx - fs[cur].idx minK) { res.push_back({fs[cur].idx, fs[i].idx, fs[cur].price, fs[i].price}); cur i; } } return res; }逻辑说明cur记录当前笔的候选起点初始为第一个分型。后续分型若与起点同向且更极端直接替换起点若反向且距离满足条件输出一笔并把当前分型设为新起点。这里的“距离”用fs[i].idx - fs[cur].idx是原始K线下标差不是分型数组下标差所以minK的含义是“两分型端点之间的K线间隔数”。minK对应口径效果0无约束分型交替即成笔适合纯形态观察1新笔笔较密适合日线短线4接近老笔笔更稳适合30分钟以上级别实际项目中minK不建议调得太大笔太少会丢失买卖点从1开始试行情级别变大再往上加。3.4 由笔提取中枢区间的快速实现有了笔序列连续三笔的重叠区间就是笔中枢std::vectorZhongShu findZhongShus(const std::vectorBi bis) { std::vectorZhongShu res; if (bis.size() 3) return res; for (int i 0; i 2 (int)bis.size(); i) { double h1 std::max(bis[i].startPrice, bis[i].endPrice); double l1 std::min(bis[i].startPrice, bis[i].endPrice); double h2 std::max(bis[i 1].startPrice, bis[i 1].endPrice); double l2 std::min(bis[i 1].startPrice, bis[i 1].endPrice); double h3 std::max(bis[i 2].startPrice, bis[i 2].endPrice); double l3 std::min(bis[i 2].startPrice, bis[i 2].endPrice); double zg std::min({h1, h2, h3}); // 三段高点里的最低值 double zd std::max({l1, l2, l3}); // 三段低点里的最高值 if (zg zd) { res.push_back({zg, zd, bis[i].startIdx, bis[i 2].endIdx}); i 2; // 三段成枢后跳过避免后续笔并入同一中枢 } } return res; }说明这段代码需要#include algorithm并开启C11std::min({...})依赖初始化列表。zg zd表示三段笔存在重叠区间越宽表示中枢的震荡范围越大。这是笔中枢的快速提取方案完整缠论还要求中枢延伸段处理、中枢与更高层级的递归可视化第一版可以先用这个简化方案把中枢矩形画出来验证交互和绘图逻辑后再替换成递归版本。4. 可视化插件的Qt架构、图层绘制与数据接入4.1 插件形态选择自研Qt窗口比挂行情DLL更清晰缠论可视化插件有两条常见路线。一条是把算法编译成DLL挂到通达信、同花顺这类行情软件下面优点是可以复用主程序里已有的K线图组件缺点也很直接绘图接口受限想在图上额外标分型文字和中枢矩形经常要借用公式系统拐弯。另一条是用Qt自绘K线图在自研窗口内同时承载数据和算法自由度最高。我一般选后者因为缠论的“分型、笔、中枢”叠加图层不属于行情软件原生支持的对象自绘反而更可控。插件做成独立程序或嵌入宿主DLL都可行下面代码对应独立窗口方案。这种形态下算法层ChanCalculator不依赖任何Qt类型数据用std::vectorKLine传递。UI层只负责把计算结果画出来以后换一套GUI框架也不会动算法。4.2 QPainter分层绘制K线、分型、笔、中枢绘制顺序固定背景、K线、分型标记、笔、中枢矩形class ChanChartWidget : public QWidget { protected: void paintEvent(QPaintEvent*) override { QPainter painter(this); painter.fillRect(rect(), QColor(#1e1e1e)); drawKline(painter); // 先画原始K线 drawFractals(painter); // 分型标记 drawBis(painter); // 笔的折线 drawZhongShus(painter); // 中枢矩形 } private: void drawBis(QPainter p) { if (bis_.size() 2) return; p.setPen(QPen(QColor(#ffd54f), 2)); for (size_t i 1; i bis_.size(); i) { int x1 view_.xOf(bis_[i - 1].startIdx); int y1 view_.yOf(bis_[i - 1].startPrice); int x2 view_.xOf(bis_[i].startIdx); // 注意是bis_[i]的起点 int y2 view_.yOf(bis_[i].startPrice); p.drawLine(x1, y1, x2, y2); } } void drawZhongShus(QPainter p) { p.setBrush(QColor(80, 80, 200, 40)); p.setPen(QPen(QColor(#7986cb), 1)); for (const auto zs : zhongshus_) { int x1 view_.xOf(zs.startIdx); int x2 view_.xOf(zs.endIdx); int y1 view_.yOf(zs.zg); int y2 view_.yOf(zs.zd); p.drawRect(QRect(x1, y1, x2 - x1, y2 - y1)); } } };逻辑说明drawBis循环里连接的是bis_[i - 1]的端点到bis_[i]的起点因为相邻两笔首尾共用同一个分型这样画出来才是连续的折线。drawZhongShus的矩形高度由zg和zd的价差决定宽度由该组笔的起止下标决定。分型标记一般用短横线或小三角顶分型画在高点上方底分型画在低点下方。K线实体部分用fillRect填充影线用drawLine收盘高于开盘画红反之画绿。这里有个细节所有绘制函数只依赖view_坐标映射和算法输出不要在绘制函数里重新算缠论避免算法改动后绘图逻辑跟着错。4.3 视口映射、缩放与平移K线图坐标映射的核心只有两个函数xOf把K线下标转成像素列yOf把价格转成像素行class ViewPort { public: int width 1200, height 600; int leftMargin 60, rightMargin 20, topMargin 20, bottomMargin 30; int firstIdx 0, lastIdx 0; double minPrice 0, maxPrice 0; void setRange(int first, int last) { firstIdx first; lastIdx last; } void updatePriceRange(const std::vectorKLine kls) { minPrice kls[firstIdx].low; maxPrice kls[firstIdx].high; for (int i firstIdx 1; i lastIdx; i) { minPrice std::min(minPrice, kls[i].low); maxPrice std::max(maxPrice, kls[i].high); } } int xOf(int idx) const { if (lastIdx firstIdx) return 0; return leftMargin (idx - firstIdx) * (width - leftMargin - rightMargin) / (lastIdx - firstIdx); } int yOf(double price) const { if (maxPrice minPrice) return 0; return topMargin (maxPrice - price) * (height - topMargin - bottomMargin) / (maxPrice - minPrice); } };说明xOf里的整数除法在K线数量超过窗口宽度时会出现多根K线映射到同一列这是K线图的正常表现不需要用浮点数去“修正”否则缩放时反而会抖动。滚动和平移的实现就是在滚轮事件里调整firstIdx和lastIdx然后调用updatePriceRange和update()绘制函数本身不用改。价格轴标签放在左边距时间轴标签放在底边距轴绘制有条件抽成独立的drawAxis函数不要和K线画在一起。4.4 行情数据接入与线程安全插件的数据源常见是CSV文件或行情服务接口。CSV解析本身不复杂但要注意不要在UI线程里做文件IO窗口拖动会卡顿class ChanChartWidget : public QWidget { public: void loadDataAsync(const QString path) { std::futurestd::vectorKLine fut std::async(std::launch::async, [path]() { return loadDailyCsv(path.toStdString()); }); fut_ std::move(fut); } void loadFinished() { if (!fut_.valid()) return; auto data fut_.get(); // 只在主线程调用get() calculator_.setData(data); calculator_.recalculate(); view_.setRange(0, (int)data.size() - 1); view_.updatePriceRange(data); update(); } };说明loadFinished()由定时器或Qt事件触发保证get()在主线程执行。std::future::get()只能调用一次所以成员变量fut_用完后要判断valid()。算法层和UI层之间通过值传递std::vectorKLine避免共享指针在多线程里裸奔。如果项目里已经用了Qt的事件循环也可以用QMetaObject::invokeMethod把结果回调切回主线程逻辑一样。5. 缠论可视化插件的排错、回归验证与参数固化5.1 分型连续翻转的典型场景趋势线附近或长上下影线密集的震荡区分型经常出现“顶分型后马上又是顶分型”的情况。此时如果buildBis里没有做同向分型取极值笔会画出锯齿状。排查时先看strictFractal是否开着开着的条件下还乱就把特征K线图层显示出来直接检查包含合并方向是否在暴跌段里滞后后又恢复。5.2 快照回归用命令行对比输出代替肉眼查图算法改动后靠肉眼看图很难发现“某段少了一笔”。常见做法是把算法层编成不带界面的命令行工具跑同一份历史数据输出可比较的文本文档./chan_tool --input data/if_000300_1d.csv --minK 1 out/bi.tsv diff out/bi.tsv test/bi_expected.tsvchan_tool和可视化窗口共用同一个ChanCalculator只是额外多了参数解析和结果打印。输出格式可以做成“笔号 起点时间 终点时间 方向”手工确认过的结果保存为bi_expected.tsv并纳入git。每次改完算法跑一次diff能立刻定位到具体笔的变化比肉眼效率高。这里提到的if_000300_1d.csv只是一份普通指数日线数据也可以换成你本地的任何标的。5.3 参数调优顺序与特征K线调试图层参数不建议一个一个乱试。建议顺序先固定minK1看分型是否合理再开启特征K线图层核对包含合并确认分型无误后再去调笔的间隔。分型和笔的边界条件比中枢层级更容易影响观感中枢只在笔稳定之后才有意义。一个实用的做法是给ChanChartWidget加一个快捷键F切换showFeatureK_布尔值在K线图层下方用细线画出合并后的特征K线。特征K线的低点连线能直观反映包含合并方向是否正确这比反复试参数收敛快得多。最终参数宜写入chan_params.json启动时加载再按不同行情周期覆盖。注意minK1和minK4出来的笔数量可能差30%以上这不是bug而是缠论新旧笔口径差异。如果要做信号级应用建议在同一周期下固定一种口径中途换口径会让背驰判断失去连续性。本文还有配套的精品资源点击获取
返回列表