数据科学和机器学习(第 37 部分):利用烛条形态和人工智能战胜市场·进阶篇
(2/3)·单根烛条 10 种形态如何编码、收集并喂给模型,多数教程卡在检测这一步
不少交易者把阳线阴线当预测信号,其实单根烛条本身不指向未来。本篇接上篇概念,继续把烛条形态转成可被机器学习消费的工程数据,并推进到交易机器人。
◍ 把烛条形态画到主图上才算真懂
做价格行为分析时,烛台形态常被讲得玄乎,但真要拿去喂机器学习模型,数据品质比肉眼感觉重要得多。一个直接的办法是用代码把形态在主图可视化,让人和机器对得上账。 这个指标挂在主图窗口,开了 5 个缓冲区和 1 个绘图序列:阳线默认橙色、阴线蓝色,一旦识别出形态就把该 K 线标红,并打一个 90 度旋转的文字标签写明形态名。 ta-lib.mqh 是静态类,不用初始化,直接在 OnCalculate 里调 CDL 开头的形态函数就行。下面这段节选能看出来,它先按收盘大于开盘填颜色缓冲,再用 20% 波幅做 padding 把 Doji 标签放到高点上方。 肉眼回看图表,红色 K 线基本就是人能在盘面上圈出来的那些形态,说明逻辑没跑偏。下一步是把这些识别结果用脚本落盘成 CSV,再拿去训模型——外汇和贵金属波动大、杠杆高,形态信号只是概率倾向,实盘前务必在 MT5 历史数据里自己跑一遍验证。
class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_buffers class="num">5 class="macro">#class="kw">property indicator_plots class="num">1 class="macro">#class="kw">property indicator_type1 DRAW_COLOR_CANDLES class="macro">#class="kw">property indicator_color1 clrDodgerBlue, clrOrange, clrRed class="macro">#class="kw">property indicator_style1 STYLE_SOLID class="macro">#class="kw">property indicator_width1 class="num">1 class="type">class="kw">double OpenBuff[]; class="type">class="kw">double HighBuff[]; class="type">class="kw">double LowBuff[]; class="type">class="kw">double CloseBuff[]; class="type">class="kw">double ColorBuff[]; class="macro">#include <ta-lib.mqh> class=class="str">"cmt">//!important for candlestick patterns class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">//--- if (rates_total<class="num">1) class="kw">return rates_total; for(class="type">int i = prev_calculated; i < rates_total; i++) { OpenBuff[i] = open[i]; HighBuff[i] = high[i]; LowBuff[i] = low[i]; CloseBuff[i] = close[i]; class=class="str">"cmt">//--- if (close[i]>open[i]) ColorBuff[i] = class="num">1.0; else ColorBuff[i] = class="num">0.0; class=class="str">"cmt">//--- class="type">class="kw">double padding = MathAbs(high[i] - low[i]) * class="num">0.2; class=class="str">"cmt">// class="num">20% padding if (CTALib::CDLDOJI(open[i], close[i])) { TextCreate(class="type">class="kw">string(i)+(class="type">class="kw">string)time[i], time[i]-PeriodSeconds(), high[i]+padding, "Doji", clrBlack, class="num">90.0); ColorBuff[i] = class="num">2.0; } if (CTALib::CDLDRAGONFLYDOJI(open[i], high[i], low[i], close[i])) {
「把反转形态标记到K线上的写法」
这段逻辑跑在指标的主循环里,对每根柱线 i 依次调用 CTALib 里的形态识别函数,命中就在 high[i]+padding 的位置用 TextCreate 写形态名,并把 ColorBuff[i] 置为 2.0 做统一着色。 具体覆盖了蜻蜓十字、墓碑十字、锤子线、倒锤子、纺锤线,以及多空Marubozu。其中纺锤线传了 0.3 和 2.0 两个阈值,Marubozu 传了整数 2 作为实体占比容差,这些参数直接决定形态判定的松紧。 所有标注都用 clrBlack 黑色、90.0 角度绘制,name 用序号拼 time[i] 保证唯一不重叠。外汇和贵金属波动大,这类标注只是形态提示,后续反转概率仍需结合上下文验证,开 MT5 把这段代码贴进自定义指标即可看到实时标记。
TextCreate(class="type">class="kw">string(i)+(class="type">class="kw">string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"DragonFly Doji", clrBlack, class="num">90.0); ColorBuff[i] = class="num">2.0; } if (CTALib::CDLGRAVESTONEDOJI(open[i], high[i], low[i], close[i])) { TextCreate(class="type">class="kw">string(i)+(class="type">class="kw">string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"GraveStone Doji", clrBlack, class="num">90.0); ColorBuff[i] = class="num">2.0; } if (CTALib::CDLHAMMER(open[i], high[i], low[i], close[i])) { TextCreate(class="type">class="kw">string(i)+(class="type">class="kw">string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Hammer", clrBlack, class="num">90.0); ColorBuff[i] = class="num">2.0; } if (CTALib::CDLINVERTEDHAMMER(open[i], high[i], low[i], close[i])) { TextCreate(class="type">class="kw">string(i)+(class="type">class="kw">string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Inverted Hammer", clrBlack, class="num">90.0); ColorBuff[i] = class="num">2.0; } if (CTALib::CDLSPINNINGTOP(open[i], high[i], low[i], close[i], class="num">0.3, class="num">2.0)) { TextCreate(class="type">class="kw">string(i)+(class="type">class="kw">string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Spinning Top", clrBlack, class="num">90.0); ColorBuff[i] = class="num">2.0; } if (CTALib::CDLBULLISHMARUBOZU(open[i], high[i], low[i], close[i], class="num">2)) { TextCreate(class="type">class="kw">string(i)+(class="type">class="kw">string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bullish Marubozu", clrBlack, class="num">90.0); ColorBuff[i] = class="num">2.0; } if (CTALib::CDLBEARISHMARUBOZU(open[i], high[i], low[i], close[i], class="num">2)) { TextCreate(class="type">class="kw">string(i)+(class="type">class="kw">string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bearish Marubozu", clrBlack, class="num">90.0); ColorBuff[i] = class="num">2.0; } } class=class="str">"cmt">//--- class="kw">return value of prev_calculated for next call class="kw">return(rates_total); }
用 18 年日线喂饱形态样本
较高时间帧的烛条形态本身低频,拿周线或日线硬凑几年数据,机器学习多半会饿死。这里直接拉了 2005.01.01 到 2023.01.01 的 XAUUSD 日线,18 年大约 4600+ 根柱线,对日线级形态识别算够厚的一层底料。 除形态标签外,OHLC 四价也一并落盘。不是每套模型都用得到,但后面若想做「形态出现后 N 根收益分布」或特征交叉,缺了原始价格就只能重跑,不如一次写全。 下面这段 MT5 脚本把 TA-Lib 的烛条识别封装和 Pandas 风格 DataFrame 都引了进来,输入段锁死品种、周期与起止日,OnStart 里先 CopyRates 拉四个价格向量,再逐个调 CTALib 的形态函数塞进 DataFrame,最后 to_csv 导出。 #include <ta-lib.mqh> // 引入 CTALib 类,封装了各类烛条形态识别函数 #include <MALE5\Pandas\pandas.mqh> // 引入 DataFrame 风格容器,便于按列组织形态与价格 input datetime start_date = D'2005.01.01'; // 回测样本起点:2005年1月1日 input datetime end_date = D'2023.01.01'; // 回测样本终点:2023年1月1日 input string symbol = "XAUUSD"; // 目标品种:黄金现货美元报价 input ENUM_TIMEFRAMES timeframe = PERIOD_D1; // 时间帧:日线 void OnStart() { //--- 脚本主入口 vector open, high, low, close; // 声明四个价格向量 open.CopyRates(symbol, timeframe, COPY_RATES_OPEN, start_date, end_date); // 拉取开盘价序列 high.CopyRates(symbol, timeframe, COPY_RATES_HIGH, start_date, end_date); // 拉取最高价序列 low.CopyRates(symbol, timeframe, COPY_RATES_LOW, start_date, end_date); // 拉取最低价序列 close.CopyRates(symbol, timeframe, COPY_RATES_CLOSE, start_date, end_date); // 拉取收盘价序列 CDataFrame df; // 建一个 DataFrame 装所有列 vector cdl_patterns = {}; // 临时向量存形态返回值 cdl_patterns = CTALib::CDLWHITECANDLE(open, close); // 白烛形态识别 df.insert("White Candle", cdl_patterns); // 写入列 cdl_patterns = CTALib::CDLBLACKCANDLE(open, close); // 黑烛形态识别 df.insert("Black Candle", cdl_patterns); cdl_patterns = CTALib::CDLDOJI(open, close); // 十字星识别 df.insert("Doji Candle", cdl_patterns); cdl_patterns = CTALib::CDLDRAGONFLYDOJI(open, high, low, close); // 蜻蜓十字识别 df.insert("Dragonflydoji Candle", cdl_patterns); cdl_patterns = CTALib::CDLGRAVESTONEDOJI(open, high, low, close); // 墓碑十字识别 df.insert("Gravestonedoji Candle", cdl_patterns); cdl_patterns = CTALib::CDLHAMMER(open, high, low, close); // 锤子线识别 df.insert("Hammer Candle", cdl_patterns); cdl_patterns = CTALib::CDLINVERTEDHAMMER(open, high, low, close); // 倒锤子识别 df.insert("Invertedhammer Candle", cdl_patterns); cdl_patterns = CTALib::CDLSPINNINGTOP(open, high, low, close); // 纺锤线识别 df.insert("Spinningtop Candle", cdl_patterns); cdl_patterns = CTALib::CDLBULLISHMARUBOZU(open, high, low, close); // 看涨光头光脚识别 df.insert("BullishMarubozu Candle", cdl_patterns); cdl_patterns = CTALib::CDLBEARISHMARUBOZU(open, high, low, close); // 看跌光头光脚识别 df.insert("BearishMarubozu Candle", cdl_patterns); df.insert("Open", open); // 存开盘价 df.insert("High", high); // 存最高价 df.insert("Low", low); // 存最低价 df.insert("Close", close); // 存收盘价 df.to_csv(StringFormat("CandlestickPatterns.%s.%s.csv",symbol,EnumToString(timeframe)), true); // 导出为 CSV,含表头 } 实盘前先把脚本丢进 MT5 的脚本目录编译,跑完会在终端目录生成 CandlestickPatterns.XAUUSD.PERIOD_D1.csv。外汇与贵金属杠杆高、跳空频繁,样本外的形态失效概率不低,这份 CSV 只解决「有没有数据」的问题,不暗示任何形态胜率。
class="macro">#include <ta-lib.mqh> class=class="str">"cmt">//Contains CTALib class for candlestick patterns detection class="macro">#include <MALE5\Pandas\pandas.mqh> class=class="str">"cmt">//[MQL5官方文档] input class="type">class="kw">datetime start_date = D&class="macro">#x27;class="num">2005.01.class="num">01&class="macro">#x27;; input class="type">class="kw">datetime end_date = D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;; input class="type">class="kw">string symbol = "XAUUSD"; input ENUM_TIMEFRAMES timeframe = PERIOD_D1; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- vector open, high, low, close; open.CopyRates(symbol, timeframe, COPY_RATES_OPEN, start_date, end_date); high.CopyRates(symbol, timeframe, COPY_RATES_HIGH, start_date, end_date); low.CopyRates(symbol, timeframe, COPY_RATES_LOW, start_date, end_date); close.CopyRates(symbol, timeframe, COPY_RATES_CLOSE, start_date, end_date); CDataFrame df; vector cdl_patterns = {}; cdl_patterns = CTALib::CDLWHITECANDLE(open, close); df.insert("White Candle", cdl_patterns); cdl_patterns = CTALib::CDLBLACKCANDLE(open, close); df.insert("Black Candle", cdl_patterns); cdl_patterns = CTALib::CDLDOJI(open, close); df.insert("Doji Candle", cdl_patterns); cdl_patterns = CTALib::CDLDRAGONFLYDOJI(open, high, low, close); df.insert("Dragonflydoji Candle", cdl_patterns); cdl_patterns = CTALib::CDLGRAVESTONEDOJI(open, high, low, close); df.insert("Gravestonedoji Candle", cdl_patterns); cdl_patterns = CTALib::CDLHAMMER(open, high, low, close); df.insert("Hammer Candle", cdl_patterns); cdl_patterns = CTALib::CDLINVERTEDHAMMER(open, high, low, close); df.insert("Invertedhammer Candle", cdl_patterns); cdl_patterns = CTALib::CDLSPINNINGTOP(open, high, low, close); df.insert("Spinningtop Candle", cdl_patterns); cdl_patterns = CTALib::CDLBULLISHMARUBOZU(open, high, low, close); df.insert("BullishMarubozu Candle", cdl_patterns); cdl_patterns = CTALib::CDLBEARISHMARUBOZU(open, high, low, close); df.insert("BearishMarubozu Candle", cdl_patterns); df.insert("Open", open); df.insert("High", high); df.insert("Low", low); df.insert("Close", close); df.to_csv(StringFormat("CandlestickPatterns.%s.%s.csv",symbol,EnumToString(timeframe)), true); }
◍ 用 CatBoost 啃烛条形态的预测标签怎么造
拿到日线烛条数据集后,最棘手的不是跑模型,而是怎么定义「目标变量」。常见做法是引一个 lookahead 参数,指定往前看几根柱线:lookahead=1 时,只要下一根收盘价高于当前收盘价,就打标签 1(看涨),否则打 0。 原始数据里除了阳线、阴线,绝大多数行的特殊形态列全是 0,直接喂给模型会逼它过度依赖阴阳线。丢光全零行是一种解法;给「无形态」行加 -1 的保持类信号又会引爆类不平衡,两条路都不干净。 作者选了 CatBoost 分类器,理由是类别列多、契合度高。测试集上类 1 精度 0.58、类 0 仅 0.49,整体准确率 53%——比随机猜的 50% 略强,类 0 不如直接抛硬币。特征重要性里陀螺烛最冲、十字星次之、光头阴线最弱。 外汇与贵金属属高风险品种,53% 的准确率不代表可裸奔进场,必须配合止损或固定根数平仓。lookahead 试过大于 1 反而掉点,最终锁死在 1,用下一根柱线作预测窗口。 别把零值行当噪音直接扔 全零行里藏着「无特殊形态」这个真实市场状态,硬删会令模型在实盘遇到普通阳阴线时丧失上下文。更稳的是在特征工程阶段单独标记,而非塞进标签制造伪平衡。
class="kw">import pandas as pd symbol = "XAUUSD" df = pd.read_csv(f"/kaggle/input/forex-candlestick-patterns/CandlestickPatterns.{symbol}.PERIOD_D1.csv") df lookahead = class="num">1 new_df = df.copy() new_df["future_close"] = new_df["Close"].shift(-lookahead) new_df.dropna(inplace=True) # Drop NaNs caused by the shift operation signal = [] for i in range(len(new_df)): # Iterate over rows, not columns if new_df["future_close"].iloc[i] > new_df["Close"].iloc[i]: signal.append(class="num">1) else: signal.append(class="num">0) new_df["Signal"] = signal X = new_df.drop(columns=[ "Signal", "Open", "High", "Low", "Close", "future_close" ]) y = new_df["Signal"] # Split data into train and test sets X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=class="num">0.2, random_state=class="num">42, shuffle=False) from catboost class="kw">import CatBoostClassifier from sklearn.utils.class_weight class="kw">import compute_class_weight # Automatically calculate class weights classes = np.unique(y) weights = compute_class_weight(class_weight=&class="macro">#x27;balanced&class="macro">#x27;, classes=classes, y=y) class_weights = dict(zip(classes, weights)) # Define the base model model = CatBoostClassifier( iterations=class="num">1000, learning_rate=class="num">0.01, depth=class="num">5, loss_function=&class="macro">#x27;Logloss&class="macro">#x27;, class_weights=class_weights, verbose=class="num">100 ) model.fit(X_train, y_train) # Training the classifier class="num">0: learn: class="num">0.6930586 total: class="num">3.64ms remaining: class="num">3.64s class="num">100: learn: class="num">0.6897625 total: 136ms remaining: class="num">1.21s class="num">200: learn: class="num">0.6888030 total: 269ms remaining: class="num">1.07s class="num">300: learn: class="num">0.6883559 total: 401ms remaining: 931ms
「CatBoost 训练日志与特征重要性落地」
从迭代 400 到 999,learn 指标从 0.6881469 缓降到 0.6876900,总耗时由 532ms 爬到 1.3s,剩余时间同步收敛到 0。这种逐百步的微小下降说明模型在后期已接近收敛,继续加树大概率只是吃算力。 测试集分类报告里 accuracy 只有 0.53,类 0 的 precision 0.49、recall 0.55,类 1 的 precision 0.58、recall 0.52,support 合计 940(429+511)。在外汇与贵金属这种高噪声品种上,单模型五成出头的命中率属于常态,杠杆交易风险极高,别拿这个准确率当进场依据。 下面这段把特征重要性拉出来画图,跑完你能直接看到哪几个因子权重排前。get_feature_importance() 返回的数组顺序和训练列一致,sort_values 之后横排条形图最高项置顶,一眼定位主导变量。
class="kw">import matplotlib.pyplot as plt # Get feature importances importances = model.get_feature_importance() feature_names = X_train.columns if hasattr(X_train, &class="macro">#x27;columns&class="macro">#x27;) else [f&class="macro">#x27;feature_{i}&class="macro">#x27; for i in range(X_train.shape[class="num">1])] # Create DataFrame for plotting feat_imp_df = pd.DataFrame({ &class="macro">#x27;Feature&class="macro">#x27;: feature_names, &class="macro">#x27;Importance&class="macro">#x27;: importances }).sort_values(by=&class="macro">#x27;Importance&class="macro">#x27;, ascending=False) # Plot plt.figure(figsize=(class="num">7, class="num">3)) plt.barh(feat_imp_df[&class="macro">#x27;Feature&class="macro">#x27;], feat_imp_df[&class="macro">#x27;Importance&class="macro">#x27;]) plt.gca().invert_yaxis() # Highest importance on top plt.title(&class="macro">#x27;Feature Importances&class="macro">#x27;) plt.xlabel(&class="macro">#x27;Importance&class="macro">#x27;) plt.ylabel(&class="macro">#x27;Feature&class="macro">#x27;) plt.tight_layout() plt.show()
把烛条模型塞进EA跑两年回测
模型训完不等于能赚钱,得先转成 MT5 认得的 ONNX。下面这段 Python 负责把 Catboost 模型序列化,EA 侧再读进来跑推理。
model_onnx = convert_sklearn( model, "catboost", [("input", FloatTensorType([None, X_train.shape[class="num">1]]))], target_opset={"": class="num">12, "ai.onnx.ml": class="num">2}, ) # And save. with open(f"CatBoost.CDLPatterns.{symbol}.onnx", "wb") as f: f.write(model_onnx.SerializeToString()) class="macro">#include <Trade\Trade.mqh> class=class="str">"cmt">//The trading module class="macro">#include <Trade\PositionInfo.mqh> class=class="str">"cmt">//Position handling module class="macro">#include <ta-lib.mqh> class=class="str">"cmt">//For candlestick patterns class="macro">#include <Catboost.mqh> class=class="str">"cmt">//Has a class for deploying a catboost model CTrade m_trade; CPositionInfo m_position; CCatboostClassifier catboost; input class="type">int magic_number = class="num">21042025; input class="type">int slippage = class="num">100; input class="type">class="kw">string symbol_ = "XAUUSD"; input ENUM_TIMEFRAMES timeframe_ = PERIOD_D1; input class="type">int lookahead = class="num">1; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if (!MQLInfoInteger(MQL_TESTER)) if (!ChartSetSymbolPeriod(class="num">0, symbol_, timeframe_)) { printf("%s failed to set symbol %s and timeframe %s, Check these values. Err = %d",__FUNCTION__,symbol_,EnumToString(timeframe_),GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- if (!catboost.Init(StringFormat("CatBoost.CDLPatterns.%s.onnx",symbol_), ONNX_COMMON_FOLDER)) class=class="str">"cmt">//Initialize the catboost model class="kw">return INIT_FAILED; class=class="str">"cmt">//--- m_trade.SetExpertMagicNumber(magic_number); m_trade.SetDeviationInPoints(slippage); m_trade.SetMarginMode(); m_trade.SetTypeFillingBySymbol(Symbol()); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+
model_onnx = convert_sklearn( model, "catboost", [("input", FloatTensorType([None, X_train.shape[class="num">1]]))], target_opset={"": class="num">12, "ai.onnx.ml": class="num">2}, ) # And save. with open(f"CatBoost.CDLPatterns.{symbol}.onnx", "wb") as f: f.write(model_onnx.SerializeToString()) class="macro">#include <Trade\Trade.mqh> class=class="str">"cmt">//The trading module class="macro">#include <Trade\PositionInfo.mqh> class=class="str">"cmt">//Position handling module class="macro">#include <ta-lib.mqh> class=class="str">"cmt">//For candlestick patterns class="macro">#include <Catboost.mqh> class=class="str">"cmt">//Has a class for deploying a catboost model CTrade m_trade; CPositionInfo m_position; CCatboostClassifier catboost; input class="type">int magic_number = class="num">21042025; input class="type">int slippage = class="num">100; input class="type">class="kw">string symbol_ = "XAUUSD"; input ENUM_TIMEFRAMES timeframe_ = PERIOD_D1; input class="type">int lookahead = class="num">1; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if (!MQLInfoInteger(MQL_TESTER)) if (!ChartSetSymbolPeriod(class="num">0, symbol_, timeframe_)) { printf("%s failed to set symbol %s and timeframe %s, Check these values. Err = %d",__FUNCTION__,symbol_,EnumToString(timeframe_),GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- if (!catboost.Init(StringFormat("CatBoost.CDLPatterns.%s.onnx",symbol_), ONNX_COMMON_FOLDER)) class=class="str">"cmt">//Initialize the catboost model class="kw">return INIT_FAILED; class=class="str">"cmt">//--- m_trade.SetExpertMagicNumber(magic_number); m_trade.SetDeviationInPoints(slippage); m_trade.SetMarginMode(); m_trade.SetTypeFillingBySymbol(Symbol()); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+