价格行为分析工具包开发(第 32 部分):基于 Python 的 K 线识别引擎(二) 使用 TA-Lib 进行检测·进阶篇
📘

价格行为分析工具包开发(第 32 部分):基于 Python 的 K 线识别引擎(二) 使用 TA-Lib 进行检测·进阶篇

第 2/3 篇

TA-Lib 的形态识别与本地装配

TA-Lib 的 C 核心自 2001 年以 BSD 协议发布,内置 200 余个技术指标与 60 余种 K 线形态识别函数,算法经过长期验证。其 CDL 前缀系列专门处理蜡烛图形态,调用时传入 OHLC 数组,返回整数序列:非零值代表形态触发,正负号对应看涨或看跌倾向。 除十字星、吞没等常见形态外,弃婴(CDLABANDONEDBABY)是三根 K 线跳空孤立的反转信号,夹板(CDLSTICKSANDWICH)显示动能未改,续跳空(CDLTASUKIGAP)确认趋势延续。诱骗(CDLHIKKAKE)用假突破陷阱助推原趋势,腰带线(CDLBELTHOLD)则以单根 K 线顶住前势,在关键位可能释放强反转概率。 要把这套能力跑起来,Windows 下需先装匹配 Python 版本与系统架构的 .whl(如 cp39‑win_amd64 对应 64 位机 + Python 3.9),mac 与 Linux 则走底层 C 库再封 Python 包。验证只需 import 后打印版本号,无报错即就绪。 下面这段是跨平台安装与列出形态函数的实操命令,注意 CDL 前缀函数全量可通过 talib 命名空间直接检视。外汇与贵金属行情波动剧烈,形态信号仅作概率参考,实盘须自行风控。

MQL5 / C++
pip install path\to\your\downloaded\file.whl
brew install ta-lib
sudo apt-get update
sudo apt-get install libta-lib0-dev
pip install ta-lib
class="kw">import talib
print(talib.__version__)
CDL2CROWS
  CDL3BLACKCROWS
  CDL3INSIDE
  CDL3LINESTRIKE
  CDL3OUTSIDE
  CDL3STARSINSOUTH
  CDL3WHITESOLDIERS
  CDLABANDONEDBABY
  CDLADVANCEBLOCK
  CDLBELTHOLD
  CDLBREAKAWAY
  CDLCLOSINGMARUBOZU
  CDLCONCEALBABYSWALL
  CDLCOUNTERATTACK
  CDLDARKCLOUDCOVER
  CDLDOJI
  CDLDOJISTAR
  CDLDRAGONFLYDOJI
  CDLENGULFING
  CDLEVENINGDOJISTAR
  CDLEVENINGSTAR
  CDLGAPSIDESIDEWHITE
  CDLGRAVESTONEDOJI
  CDLHAMMER
  CDLHANGINGMAN
  CDLHARAMI
  CDLHARAMICROSS
  CDLHIGHWAVE
  CDLHIKKAKE
  CDLHIKKAKEMOD
  CDLHOMINGPIGEON
  CDLIDENTICAL3CROWS
  CDLINNECK
  CDLINVERTEDHAMMER
  CDLKICKING
  CDLKICKINGBYLENGTH
  CDLLADDERBOTTOM
  CDLLONGLEGGEDDOJI
  CDLLONGLINE
  CDLMARUBOZU
  CDLMATCHINGLOW
  CDLMATHOLD
  CDLMORNINGDOJISTAR
  CDLMORNINGSTAR
  CDLONNECK
  CDLPIERCING
  CDLRICKSHAWMAN
  CDLRISEFALL3METHODS
  CDLSEPARATINGLINES
  CDLSHOOTINGSTAR
  CDLSHORTLINE
  CDLSPINNINGTOP
  CDLSTALLEDPATTERN
  CDLSTICKSANDWICH
  CDLTAKURI
  CDLTASUKIGAP
  CDLTHRUSTING
  CDLTRISTAR
  CDLUNIQUE3RIVER
  CDLUPSIDEGAP2CROWS
  CDLXSIDEGAP3METHODS

◍ 请求响应闭环怎么跑起来

整套系统的骨架是「新 K 线收盘 → MQL5 抽数发 POST → Flask 算形态 → EA 回图表标注」的闭环。EA 每次只在 K 线收盘后向前取最近 60 根 OHLC 加 Unix 时间戳,封装成 JSON 推到本地 /patterns 接口,避免每跳报价都重算。 服务端拿到数据后用 TA-Lib 的六十余种 CDL 形态函数逐根识别,返回两个定长 60 的数组:patterns 存形态名,signals 标多空。EA 先清旧标签,再在有效形态 K 线最高价画字——青绿看涨、红看跌,并弹 MT5 警报。 EA 初次加载会打印一句日志,提醒你把 Flask 地址加进 EA 的 WebRequest 白名单,否则后续 HTTP 请求会静默失败。这个一次性配置不做,系统就不会有任何形态提示。 Flask 启动时会动态扫描 TA-Lib 中所有 CDL 开头函数装进内存字典,将来库一升级就能直接用新形态,不用改业务代码。同根 K 线若命中多个形态,按「吞没 > 孕线 > 十字星」优先级取主导,都没有就取 TA-Lib 返回值绝对值最大的那个,再映射 bullish / bearish。 下面这段 MQL5 片段是闭环的前端骨架:初始化报地址,OnTick 用时间戳去重,循环抽 60 根数据并组 JSON 发出。外汇与贵金属波动剧烈,这类信号仅作概率参考,实盘须自担高风险。

MQL5 / C++
class="type">int OnInit()
{
   Log("EA started – allow WebRequest to: " + InpURL);
   class="kw">return INIT_SUCCEEDED;
}
class="type">void OnTick()
{
   class="type">class="kw">datetime bar = iTime(_Symbol, InpTF, class="num">0);
   if(bar == class="num">0 || bar == g_lastBar)
      class="kw">return;
   g_lastBar = bar;
   class=class="str">"cmt">// … (class="kw">continue processing)
}
class="type">class="kw">double o[BARS], h[BARS], l[BARS], c[BARS];
class="type">long   t[BARS];
for(class="type">int i = class="num">0; i < BARS; i++)
{
   class="type">int sh = i + class="num">1;
   o[i] = iOpen(_Symbol, InpTF, sh);
   h[i] = iHigh(_Symbol, InpTF, sh);
   l[i] = iLow(_Symbol, InpTF, sh);
   c[i] = iClose(_Symbol, InpTF, sh);
   t[i] = (class="type">long)iTime(_Symbol, InpTF, sh);
}
class="type">class="kw">string json = StringFormat(
   "{\"symbol\":\"%s\",\"timeframe\":%d,\"time\":[%s],\"open\":[%s],\"high\":[%s],\"low\":[%s],\"close\":[%s]}",
   _Symbol, InpTF, CSVInt(t), CSV(o), CSV(h), CSV(l), CSV(c)
);
Log("JSON-OUT: " + json);
class="type">char body[];
StringToCharArray(json, body, class="num">0, StringLen(json));
class="type">char reply[];
class="type">class="kw">string hdr = "Content-Type: application/json\r\n", respHdr;
class="type">int code = WebRequest("POST", InpURL, hdr, InpTimeout, body, reply, respHdr);
if(code == -class="num">1)
{
   Log("WebRequest failed: " + IntegerToString(GetLastError()));
   class="kw">return;
}
class="type">class="kw">string resp = CharArrayToString(reply, class="num">0, -class="num">1, CP_UTF8);

「把远端形态信号画到MT5图表上」

EA 拿到 HTTP 响应后先做结构校验:用 ExtractArray 分别取出 patterns 与 signals 字段,再经 ParseArray 展开成字符串数组。若两者长度与预设常量 BARS 不一致,直接记日志 'Malformed patterns or signals' 并 return,避免脏数据污染图表。 校验通过后进入逐根 K 线循环,shift = BARS - i 把数组下标映射成图表左侧偏移量,iTime / iHigh 取该根 K 线的开盘时间与最高价,作为 OBJ_TEXT 对象的锚点。信号字符串为 'bullish' 时文字染成 clrLime,否则 clrRed,字体大小走外部参数 InpFontSize,同时设 OBJPROP_SELECTABLE 为 false 防止误点。 每标注一个形态就触发 Alert,格式含品种、周期枚举、信号方向、形态名与时刻,例如 'XAUUSD PERIOD_H1 bullish pattern 'Hammer' at 2024.05.13 08:00'。实盘跑起来后,你能在 EURUSD 的 M15 上看到约 200 根内的锤子线被自动标绿或标红,外汇与贵金属波动剧烈,这类标注只提示概率倾向,不构成方向保证。 OnDeinit 里倒序扫 ObjectsTotal,凡名字以 PREFIX 开头一律 ObjectDelete,保证 EA 卸载后不留残影。后端 Flask 的 /patterns 路由用 talib 以 CDL 前缀动态收集全部蜡烛函数,收到请求先掐掉数据里的 \x00 截断符再 json.loads,否则 MT5 发来的结构体尾零容易让 Python 端抛异常。

MQL5 / C++
Log("HTTP " + IntegerToString(code) + " RESP: " + resp);
class="type">class="kw">string patTxt, sigTxt, patt[], sigs[];
if(!ExtractArray(resp, "patterns", patTxt) ||
   !ExtractArray(resp, "signals", sigTxt) ||
   !ParseArray(patTxt, patt) ||
   !ParseArray(sigTxt, sigs) ||
   ArraySize(patt) != BARS ||
   ArraySize(sigs) != BARS)
{
   Log("Malformed patterns or signals");
   class="kw">return;
}
for(class="type">int i = class="num">0; i < BARS; i++)
{
   class="type">class="kw">string pat = patt[i], sig = sigs[i];
   if(pat == "" || pat == "None") class="kw">continue;
   class="type">int shift = BARS - i;
   class="type">class="kw">datetime tm = iTime(_Symbol, InpTF, shift);
   class="type">class="kw">double y = iHigh(_Symbol, InpTF, shift);
   class="type">class="kw">string obj = PREFIX + IntegerToString(shift);
   ObjectCreate(class="num">0, obj, OBJ_TEXT, class="num">0, tm, y);
   ObjectSetString(class="num">0, obj, OBJPROP_TEXT, pat);
   class="type">color col = (sig == "bullish" ? clrLime : clrRed);
   ObjectSetInteger(class="num">0, obj, OBJPROP_COLOR, col);
   ObjectSetInteger(class="num">0, obj, OBJPROP_FONTSIZE, InpFontSize);
   ObjectSetInteger(class="num">0, obj, OBJPROP_SELECTABLE, class="kw">false);
   Alert(StringFormat("%s %s %s pattern &class="macro">#x27;%s&class="macro">#x27; at %s",
                       _Symbol, EnumToString(InpTF), sig, pat,
                       TimeToString(tm, TIME_DATE|TIME_MINUTES)));
}
class="type">void OnDeinit(const class="type">int reason)
{
   for(class="type">int i = ObjectsTotal(class="num">0,class="num">0,-class="num">1)-class="num">1; i >= class="num">0; i--)
   {
      class="type">class="kw">string n = ObjectName(class="num">0, i, class="num">0, -class="num">1);
      if(StringFind(n, PREFIX) == class="num">0)
         ObjectDelete(class="num">0, n);
   }
   Log("EA removed");
}
app = Flask(__name__)
logging.basicConfig(level=logging.INFO,
                    format=&class="macro">#x27;%(asctime)s %(levelname)s %(message)s&class="macro">#x27;)
app.logger.setLevel(logging.INFO)
# load all TA-Lib candlestick functions
CDL_FUNCS = {
    name: getattr(talib, name)
    for name in talib.get_functions() if name.startswith("CDL")
}
@app.route(&class="macro">#x27;/patterns&class="macro">#x27;, methods=[&class="macro">#x27;POST&class="macro">#x27;])
def patterns():
    app.logger.info("Received /patterns request")
    try:
        raw = request.data
        if b&class="macro">#x27;\x00&class="macro">#x27; in raw:
            raw = raw.split(b&class="macro">#x27;\x00&class="macro">#x27;, class="num">1)[class="num">0]
        data = json.loads(raw.decode(&class="macro">#x27;utf-class="num">8&class="macro">#x27;))
    except Exception as e:
        class="kw">return jsonify(error="Invalid JSON", details=str(e)), class="num">400
    try:
        symbol = data.get(&class="macro">#x27;symbol&class="macro">#x27;, &class="macro">#x27;Instrument&class="macro">#x27;)

正文

&nbsp;&nbsp;&nbsp;&nbsp;ts&nbsp;&nbsp;&nbsp;&nbsp; = data.get(<span class="string">'time'</span>, []) &nbsp;&nbsp;&nbsp;&nbsp;open_&nbsp;&nbsp;= np.array(data[<span class="string">'open'</span>][::-<span class="number">1</span>],&nbsp;&nbsp;dtype=<span class="built_in">float</span>) &nbsp;&nbsp;&nbsp;&nbsp;high&nbsp;&nbsp; = np.array(data[<span class="string">'high'</span>][::-<span class="number">1</span>],&nbsp;&nbsp;dtype=<span class="built_in">float</span>) &nbsp;&nbsp;&nbsp;low&nbsp;&nbsp;&nbsp;&nbsp;= np.array(data[<span class="string">'low'</span>][::-<span class="number">1</span>],&nbsp;&nbsp; dtype=<span class="built_in">float</span>) &nbsp;&nbsp;&nbsp;&nbsp;close&nbsp;&nbsp;= np.array(data[<span class="string">'close'</span>][::-<span class="number">1</span>], dtype=<span class="built_in">float</span>) &nbsp;&nbsp;&nbsp;&nbsp;idx&nbsp;&nbsp;&nbsp;&nbsp;= pd.to_datetime(np.array(ts[::-<span class="number">1</span>], dtype=<span class="string">'int64'</span>), unit=<span class="string">'s'</span>) &nbsp;&nbsp;&nbsp;&nbsp;app.logger.info(<span class="string">f"Loaded <span class="subst">{len(open_)}</span> bars for <span class="subst">{symbol}</span>"</span>) <span class="keyword">except</span> KeyError <span class="keyword">as</span> ke: &nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> jsonify(error=<span class="string">f"Missing field <span class="subst">{ke}</span>"</span>), <span class="number">400</span> <span class="keyword">except</span> Exception <sp

常见问题

在 Python 端对 candle 实体与影线比例加阈值约束,再用 ATR 过滤波幅过小周期,误报会明显下降。
用本地 HTTP 服务暴露 JSON 信号,图表端轮询或长连接拉取后在对应价位画箭头与文本标签即可。
小布已内置 AIGC 形态诊断,打开对应品种页就能看到远端算好的 K 线信号,不用本地装 TA-Lib。
给每次响应带时间戳与 bar 序号,客户端按序号去重并只画未处理的新 bar,避免重复或错位。
正常,贵金属高杠杆高风险,形态在趋势段有效、震荡段失效,建议叠加趋势过滤再统计概率。