缠论笔结构简洁版
分型连成笔线,标顶底箭头,只给最近几个转折画短支撑阻力段
MT5 真机运行截图 · 服务器把编译好的 EX5 挂到黄金 H1 图表上截的,你装上去看到的就是这个样子
#property copyright "XAUOF 指标工坊"
#property version "2.0"
#property strict
#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots 3
//--- 笔线:DRAW_SECTION 只占 1 个缓冲区,会把相邻的非空值直接连成折线
#property indicator_type1 DRAW_SECTION
#property indicator_color1 clrCyan
#property indicator_width1 2
#property indicator_label1 "笔"
//--- 顶分型箭头
#property indicator_type2 DRAW_ARROW
#property indicator_color2 clrOrangeRed
#property indicator_width2 2
#property indicator_label2 "顶分型"
//--- 底分型箭头
#property indicator_type3 DRAW_ARROW
#property indicator_color3 clrDeepSkyBlue
#property indicator_width3 2
#property indicator_label3 "底分型"
//============================ 可调参数 ============================
input int FractalBars = 5; // 分型识别宽度:左右各比较几根K线。越大转折越少越干净,日线推荐5,M15推荐3
input int MinMovePoints = 300; // 最小折返点数:小于这个幅度的反向转折直接忽略。黄金推荐300-800,外汇推荐300
input int CalcBars = 600; // 只计算最近多少根K线,0=全部。设小一点图更清爽、速度更快
input bool ShowArrows = true; // 是否显示顶底分型箭头
input int ArrowGapPoints = 0; // 箭头与K线的间距(点)。0=自动按近期波幅计算
input int KeyLevelCount = 3; // 只给最近几个转折点画水平线。0=完全不画(这是上一版满屏横线的开关)
input int SegmentBars = 24; // 水平线长度(横跨几根K线)。不再用贯穿全图的HLINE
input bool ShowPriceLabel = true; // 是否在水平线右端标价格数字
//============================ 绘图缓冲区 ============================
double PenBuffer[]; // 笔线(只在转折点有值,其余为 EMPTY_VALUE)
double TopBuffer[]; // 顶分型箭头
double BtmBuffer[]; // 底分型箭头
//============================ 转折点记录 ============================
// 三个平行数组存已确认的转折点:第几根K线、价格、是顶还是底
int pvBar[];
double pvPrice[];
bool pvTop[];
int pvCount = 0;
const string OBJ_PREFIX = "CLN_"; // 本指标创建的图表对象统一前缀,方便一次性清理
//+------------------------------------------------------------------+
//| 初始化 |
//+------------------------------------------------------------------+
int OnInit()
{
// 参数兜底,防止用户填 0 或负数导致循环越界
if(FractalBars < 1)
{
Print("FractalBars 必须 >= 1");
return INIT_PARAMETERS_INCORRECT;
}
SetIndexBuffer(0, PenBuffer, INDICATOR_DATA);
SetIndexBuffer(1, TopBuffer, INDICATOR_DATA);
SetIndexBuffer(2, BtmBuffer, INDICATOR_DATA);
// 空值一律用 EMPTY_VALUE,绝不能用 0(0 是合法价格,会把线拉到图表底部)
PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE);
PlotIndexSetInteger(1, PLOT_ARROW, 234); // 顶:向下箭头
PlotIndexSetInteger(2, PLOT_ARROW, 233); // 底:向上箭头
IndicatorSetString(INDICATOR_SHORTNAME, "缠论笔结构");
IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
ArrayResize(pvBar, 0);
ArrayResize(pvPrice, 0);
ArrayResize(pvTop, 0);
pvCount = 0;
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| 卸载时清掉自己画的对象,不给用户留垃圾 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, OBJ_PREFIX);
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| 主计算 |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
// 至少要够一个分型的宽度
if(rates_total < FractalBars * 2 + 2) return 0;
// 笔结构是"一根新K线可能改写整条笔"的东西,所以每出现一根新K线就在窗口内整体重算一次。
// 盘中每个tick都重算没有意义,只会拖慢终端。
static datetime lastBarTime = 0;
static int lastRates = 0;
bool needRecalc = (prev_calculated == 0)
|| (rates_total != lastRates)
|| (time[rates_total - 1] != lastBarTime);
if(!needRecalc) return rates_total;
lastBarTime = time[rates_total - 1];
lastRates = rates_total;
// 整体重算前先把缓冲区全部清成空值,避免残留旧箭头
ArrayInitialize(PenBuffer, EMPTY_VALUE);
ArrayInitialize(TopBuffer, EMPTY_VALUE);
ArrayInitialize(BtmBuffer, EMPTY_VALUE);
// 只处理最近 CalcBars 根,索引 0 是最老的K线,rates_total-1 是当前K线
int startBar = 0;
if(CalcBars > 0 && rates_total > CalcBars)
startBar = rates_total - CalcBars;
DetectPivots(rates_total, startBar, high, low);
// 把转折点写进绘图缓冲区:DRAW_SECTION 会自动把它们连成笔线
double gap = AutoGap(rates_total, high, low);
for(int p = 0; p < pvCount; p++)
{
int b = pvBar[p];
if(b < 0 || b >= rates_total) continue; // 索引兜底
PenBuffer[b] = pvPrice[p];
if(ShowArrows)
{
if(pvTop[p]) TopBuffer[b] = pvPrice[p] + gap; // 顶箭头画在K线上方
else BtmBuffer[b] = pvPrice[p] - gap; // 底箭头画在K线下方
}
}
DrawKeyLevels(rates_total, time);
return rates_total;
}
//+------------------------------------------------------------------+
//| 扫描分型并按"顶底交替"整理成笔 |
//+------------------------------------------------------------------+
void DetectPivots(const int rates_total, const int startBar,
const double &high[], const double &low[])
{
pvCount = 0;
ArrayResize(pvBar, 0);
ArrayResize(pvPrice, 0);
ArrayResize(pvTop, 0);
// 分型需要左右各 FractalBars 根K线才能确认,所以两端各留出这么多
int first = MathMax(startBar, FractalBars);
int last = rates_total - 1 - FractalBars;
for(int i = first; i <= last && !IsStopped(); i++)
{
bool isTop = true; // 顶分型:这根的最高价比左右邻居都高
bool isBtm = true; // 底分型:这根的最低价比左右邻居都低
for(int j = 1; j <= FractalBars; j++)
{
// 左边允许相等、右边必须严格超过,这样遇到并列高点取靠前那根,不会重复标
if(high[i] < high[i - j] || high[i] <= high[i + j]) isTop = false;
if(low[i] > low[i - j] || low[i] >= low[i + j]) isBtm = false;
if(!isTop && !isBtm) break;
}
if(isTop && isBtm) continue; // 理论上不会同时成立,保险起见跳过
if(isTop) AddPivot(i, high[i], true);
else if(isBtm) AddPivot(i, low[i], false);
}
}
//+------------------------------------------------------------------+
//| 加入一个转折点:同向取更极端的,反向要够幅度才认 |
//+------------------------------------------------------------------+
void AddPivot(const int bar, const double price, const bool isTop)
{
if(pvCount == 0)
{
PushPivot(bar, price, isTop);
return;
}
int lastIdx = pvCount - 1;
// 情况一:和上一个转折同方向(连续几个顶或连续几个底)
// 只保留最极端的那一个,顶取最高、底取最低
if(pvTop[lastIdx] == isTop)
{
if((isTop && price > pvPrice[lastIdx]) || (!isTop && price < pvPrice[lastIdx]))
{
pvBar[lastIdx] = bar;
pvPrice[lastIdx] = price;
}
return;
}
// 情况二:反方向转折。幅度不到 MinMovePoints 就当成噪音丢掉,
// 这是横盘时不再来回画小锯齿的关键
double minMove = (double)MinMovePoints * _Point;
if(MathAbs(price - pvPrice[lastIdx]) < minMove) return;
PushPivot(bar, price, isTop);
}
//+------------------------------------------------------------------+
//| 往转折点数组尾部追加一条记录 |
//+------------------------------------------------------------------+
void PushPivot(const int bar, const double price, const bool isTop)
{
int n = pvCount + 1;
if(ArrayResize(pvBar, n) < n) return;
if(ArrayResize(pvPrice, n) < n) return;
if(ArrayResize(pvTop, n) < n) return;
pvBar[pvCount] = bar;
pvPrice[pvCount] = price;
pvTop[pvCount] = isTop;
pvCount = n;
}
//+------------------------------------------------------------------+
//| 箭头离K线的距离:手填就用手填的,填0就按近20根平均振幅算 |
//+------------------------------------------------------------------+
double AutoGap(const int rates_total, const double &high[], const double &low[])
{
if(ArrowGapPoints > 0) return (double)ArrowGapPoints * _Point;
int n = MathMin(20, rates_total);
if(n <= 0) return 10 * _Point; // 除数为0的保护
double sum = 0.0;
for(int i = rates_total - n; i < rates_total; i++)
{
if(i < 0) continue;
sum += (high[i] - low[i]);
}
double avg = sum / n; // n 已确认 > 0
if(avg <= 0.0) return 10 * _Point;
return avg * 0.6;
}
//+------------------------------------------------------------------+
//| 只给最近 KeyLevelCount 个转折点画短水平线段(不是贯穿全图的横线) |
//+------------------------------------------------------------------+
void DrawKeyLevels(const int rates_total, const datetime &time[])
{
// 每次重画前先全清,对象数量很少(最多 KeyLevelCount*2 个),开销可以忽略
ObjectsDeleteAll(0, OBJ_PREFIX);
if(KeyLevelCount <= 0 || pvCount <= 0)
{
ChartRedraw(0);
return;
}
int n = MathMin(KeyLevelCount, pvCount);
int secs = PeriodSeconds(PERIOD_CURRENT);
if(secs <= 0) secs = 60; // 极端情况兜底
for(int k = 0; k < n; k++)
{
int p = pvCount - 1 - k; // 从最新的转折点往前取
if(p < 0) break;
int b1 = pvBar[p];
if(b1 < 0 || b1 >= rates_total) continue;
double price = pvPrice[p];
datetime t1 = time[b1];
// 线段右端:转折点往右数 SegmentBars 根。超出已有K线就按周期往未来推时间
int b2 = b1 + MathMax(1, SegmentBars);
datetime t2;
if(b2 < rates_total) t2 = time[b2];
else t2 = time[rates_total - 1] + (datetime)((b2 - (rates_total - 1)) * secs);
color clr = pvTop[p] ? clrOrangeRed : clrDeepSkyBlue;
string nm = OBJ_PREFIX + "LV_" + IntegerToString(p);
if(ObjectCreate(0, nm, OBJ_TREND, 0, t1, price, t2, price))
{
ObjectSetInteger(0, nm, OBJPROP_COLOR, clr);
ObjectSetInteger(0, nm, OBJPROP_STYLE, STYLE_DOT);
ObjectSetInteger(0, nm, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, nm, OBJPROP_RAY_LEFT, false);
ObjectSetInteger(0, nm, OBJPROP_RAY_RIGHT, false); // 关键:不向右延伸
ObjectSetInteger(0, nm, OBJPROP_BACK, true);
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
}
if(ShowPriceLabel)
{
string tn = OBJ_PREFIX + "TX_" + IntegerToString(p);
if(ObjectCreate(0, tn, OBJ_TEXT, 0, t2, price))
{
ObjectSetString(0, tn, OBJPROP_TEXT, DoubleToString(price, _Digits));
ObjectSetInteger(0, tn, OBJPROP_COLOR, clr);
ObjectSetInteger(0, tn, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, tn, OBJPROP_ANCHOR, ANCHOR_LEFT);
ObjectSetInteger(0, tn, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, tn, OBJPROP_HIDDEN, true);
}
}
}
ChartRedraw(0);
}作品说明
The user's right that it's cluttered — the horizontal lines are from OBJ_HLINE being drawn at every fractal point, and since OBJ_HLINE spans the full width of the chart infinitely, dozens of fractals means dozens of full-width lines crossing everything. I need to rework the code to fix this clutter issue.
I should switch to OBJ_TREND with short segments matching the reference chart, only draw for the most recent N pivots, limit labels too, and stop redrawing objects inefficiently every calculation. I also spot a real bug: the extremePoints array keeps growing on every recalculation instead of resetting, and there's a buffer mismatch since DRAW_ZIGZAG actually requires 2 indicator buffers but my SetIndexBuffer assignments didn't account for that offset correctly.
A cleaner design: use DRAW_SECTION for the zigzag line since it only needs a single buffer and connects non-empty values naturally, then separate plots for top and bottom fractal arrows — three plots total instead of four. I'll draw short OBJ_TREND segments and price labels only at the last N pivots, and just recompute the whole zigzag from scratch each tick rather than trying to do incremental updates, since that's simpler and safer.
Now I'm limiting the recalculation to a configurable window instead of the full chart history to keep it fast, sweeping through that window to find fractals and building a pivot list that enforces alternating tops and bottoms, applying an amplitude filter, and keeping only the most extreme pivot when duplicates occur.
I plan to draw the zigzag buffer and arrows only at pivot bars, limit horizontal segment drawing to the last few pivots with price labels, and clean up old chart objects before redrawing to avoid clutter, only triggering recalculation on a new bar to reduce churn. For the fractal test itself I'm weighing strict inequality versus allowing ties on one side to avoid missing valid pivots.
I need to compute the future bar's time when the segment extends past the available bars, extrapolating using the period length if the target index falls beyond rates_total. I also need to make sure the fractal detection loop only considers bars that have enough neighbors on both sides, so it can't start too close to the array edges.
Now I'm setting up the indicator's properties — buffers for the pen (bi) lines, top fractal arrows, and bottom fractal arrows with their respective colors and labels.
Then I'm defining the input parameters: fractal comparison bars, minimum reversal points for gold, calculation window, whether to show arrows, and settings for drawing key level horizontal lines.
I'm setting up global arrays to track pivot points and writing functions to detect pivots, draw key levels, and clean up chart objects. I'm handling the empty-value plotting so segments skip gaps properly, using distinct arrow codes for top and bottom fractals, and deciding when the indicator needs to recalculate versus reuse cached results.
I'm also working out the arrow offset distance from price, considering a point-based gap input that scales reasonably for gold but might need adjustment for other instruments like stocks.
For the auto-offset behavior, I'm planning to default to computing an average high-low range over the last 20 bars when the input is set to zero. On a full recalculation I'll reset the entire buffer to EMPTY_VALUE to avoid stale drawings, and I'll clear all chart objects using a common name prefix so cleanup stays simple.
Now I'm structuring the response: a short explanation of why the chart got cluttered with horizontal lines and labels (each fractal point was drawing a full-width line plus a text label), followed by the corrected code, then usage notes.
I also realized the earlier version had a buffer misalignment bug—DRAW_ZIGZAG needs two buffers but I'd only allocated one, so I'm switching to DRAW_SECTION which only needs a single buffer. Writing the pivot detection function...
Writing pivot-merging logic...
I'll skip that extra edge-case guard for simplicity, since it risks deleting legitimate pivots — better to keep the logic simple and documented. Now I'm writing the loop that fills the indicator buffers from the pivot arrays.
Filling arrow buffers...
For key levels, I'm writing a function that clears old "CLN_" objects and draws horizontal lines for the most recent pivots up to KeyLevelCount.
Still writing trend line creation...
Adding price label text objects...
Now I'm writing a function to compute an automatic arrow gap based on recent price range, falling back to a fixed point value if a manual gap setting is provided.
Then I'm handling initialization validation, cleanup of chart objects on deinit, and figuring out the starting bar index for calculations when limiting to a fixed number of bars.
For the recalculation approach, doing a full recalc each new bar is justified for zigzag structures despite the extra cost, since it keeps the logic simple and correct without needing prev_calculated tricks. Now I need to draft the title (缠论笔结构自动画线, staying within character limits) and a concise Chinese description under 200 words explaining what the horizontal lines represent.
那些横线是我上一版给每一个顶/底分型都画了一条 OBJ_HLINE。MT5 的 HLINE 是贯穿整张图的无限长水平线,几十个分型就是几十条铺满屏幕的横线,再加上每个分型都挂中文标签,必然糊成一片。
顺带修掉一个真实缺陷:上一版 plot1 用 DRAW_ZIGZAG,它要占 2 个缓冲区,而我按 1 个分配,导致缓冲区和 plot 序号整体错位。这版改用 DRAW_SECTION(只占 1 个缓冲区)。
这一版的做法:只留青色笔线 + 顶底箭头,水平线改成短线段、且只给最近 3 个转折点画,其余全部不画。
使用说明
安装:存为 ChanLun_Pen_Clean.mq5 放进 MQL5/Indicators,F7 编译,拖到图上。上一版留下的横线用图表右键「对象列表」全选删除,或直接把旧指标从图上移除。
嫌乱就调这三个参数
KeyLevelCount:满屏横线的总开关。设0就一条水平线都不画,只剩笔线和箭头,最清爽;默认3只给最近三个转折画短段。MinMovePoints:笔线锯齿多就往大调。黄金日线 500-800、M15 200-300,改完立即生效。FractalBars:转折点密度。调到 7-8 只留大级别转折,调到 3 抓小波段。
CalcBars 控制往左算多少根,默认 600。往前拉历史看不到线就把它调大或设 0。
怎么看
青色折线是笔,连接已确认的顶底;橙红下箭头是顶分型,天蓝上箭头是底分型;短虚线段是最近几个转折的支撑阻力位,右端标着价格。
注意
分型天生滞后 FractalBars 根K线才能确认,所以最右侧那段笔是未完成的,箭头不会出现在当前K线上,这不是 bug。指标只在每根新K线收盘后重算一次,盘中不会跳动。这版只画结构,不判断买卖点,也不下单。
AI 解读根据源码逐行分析写成,供参考,以实际运行为准
这是什么
这是一个 MT5 图表指标,把缠论里的「笔」画出来。它自动找顶分型和底分型,按顶底交替连成青色折线,转折处标箭头,并只给最近几个转折画一小段支撑阻力线。解决的问题是:手动数分型、画笔太累,而很多同类指标满屏横线看不清——这一版把横线改成了短线段,图面干净。
逻辑原理
计算分三步。第一步找分型:某根K线的最高价同时高于左右各 FractalBars 根K线,就是顶分型;最低价同理是底分型。第二步整理成笔:连续同向的分型只留最极端那个(顶取最高、底取最低);反向转折必须达到 MinMovePoints 点数才被承认,幅度不够就当噪音丢掉——这是横盘不出小锯齿的关键。第三步画图:转折点写进缓冲区,DRAW_SECTION 自动把相邻转折连成折线;顶画橙红向下箭头,底画天蓝向上箭头;最近 KeyLevelCount 个转折处画一条虚线短线段(不向右延伸),右端可标价格数字。
注意重算机制:每根新K线出现时,整个计算窗口从头重算一遍。所以最近一笔的转折位置可能随新K线变化,这是笔线算法的固有特性,不是 bug。
参数说明
- FractalBars(5):分型识别宽度,左右各比较几根K线。越大转折越少越干净。注释建议日线 5、M15 用 3。
- MinMovePoints(300):最小折返点数,小于这个幅度的反向转折直接忽略。注释建议黄金 300-800、外汇 300。
- CalcBars(600):只计算最近多少根K线,0 为全部。设小一点图更清爽、速度更快。
- ShowArrows(true):是否显示顶底分型箭头。
- ArrowGapPoints(0):箭头与K线的间距(点)。0 表示自动按近 20 根平均振幅的 0.6 倍计算。
- KeyLevelCount(3):只给最近几个转折点画水平线段。0 为完全不画。
- SegmentBars(24):水平线段长度,横跨几根K线。
- ShowPriceLabel(true):是否在线段右端标价格数字。
怎么用
把 .ex5 文件放进 MT5 的 MQL5/Indicators 目录,刷新导航器,拖到图表上即可。建议挂黄金或主流外汇品种,先在当前周期看默认参数效果。看图方式:青色折线就是笔,箭头是分型确认点,虚线短线段是最近转折附近的参考支撑阻力位。卸载指标时会自动清掉自己画的对象,不留垃圾。
适用场景
适合看结构、找转折、配合支撑阻力做计划的交易者,黄金和外汇都行,周期上注释里给了日线和 M15 的参数建议。震荡行情里 MinMovePoints 能滤掉小锯齿,但参数太小仍会出很多碎笔。不适合想要固定不重绘信号、或直接拿它当进出场指令的人——它只是结构标注工具,不产生交易信号。
风险提示
一,会重绘:新K线可能改写最近一笔,箭头和线段位置会变,别把刚出现的箭头当确认信号,分型要等右侧 FractalBars 根K线走完才成立。二,滞后:分型确认本身就需要右侧若干根K线,转折确认时价格已经走了一段。三,参数敏感:MinMovePoints 换品种、换周期必须重调,否则要么满屏碎笔要么漏掉大转折。四,CalcBars 设太小可能截断笔结构,窗口边缘的笔不完整。
常见问题
- 这个指标会重绘吗
- 会。代码里每根新K线出现时会在计算窗口内整体重算笔结构,所以最近一笔的转折点可能随新K线变化,属于这类笔线算法的固有特性。
- 可以用在 XAUUSD 以外的品种吗
- 可以,代码没有限制品种。但 MinMovePoints 是按点数过滤噪音的,换品种后需要自行调整,注释建议黄金 300-800、外汇 300。
- MT4 能用吗
- 不能,这是 MQL5 指标,用了 MT5 特有的绘图缓冲区机制,需要 MT5 平台。
- 参数默认值适合什么周期
- 代码注释建议 FractalBars 日线用 5、M15 用 3,MinMovePoints 黄金 300-800。其他周期需自行测试。
- 为什么图上没有水平线
- 检查 KeyLevelCount 是否为 0,为 0 时完全不画水平线段;另外它只给最近 KeyLevelCount 个转折画短线段,不是全图横线。
Chan Pen Structure Indicator for MT5
A lightweight MT5 indicator that detects top and bottom fractals, connects them into Chan-theory pen (stroke) lines, and marks pivots with arrows. It draws short support/resistance segments only at the most recent pivots to keep the chart clean. All parameters are adjustable.
Source code is shown above. Compiled EX5 files are available to VIP members.
评论
还没有人评论,来抢个沙发。