价格行为分析工具包开发(第 32 部分):基于 Python 的 K 线识别引擎(二) 使用 TA-Lib 进行检测·进阶篇
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 命名空间直接检视。外汇与贵金属行情波动剧烈,形态信号仅作概率参考,实盘须自行风控。
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 发出。外汇与贵金属波动剧烈,这类信号仅作概率参考,实盘须自担高风险。
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 端抛异常。
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;)
正文
ts = data.get(<span class="string">'time'</span>, []) open_ = np.array(data[<span class="string">'open'</span>][::-<span class="number">1</span>], dtype=<span class="built_in">float</span>) high = np.array(data[<span class="string">'high'</span>][::-<span class="number">1</span>], dtype=<span class="built_in">float</span>) low = np.array(data[<span class="string">'low'</span>][::-<span class="number">1</span>], dtype=<span class="built_in">float</span>) close = np.array(data[<span class="string">'close'</span>][::-<span class="number">1</span>], dtype=<span class="built_in">float</span>) idx = pd.to_datetime(np.array(ts[::-<span class="number">1</span>], dtype=<span class="string">'int64'</span>), unit=<span class="string">'s'</span>) 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: <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