数据科学和机器学习(第 37 部分):利用烛条形态和人工智能战胜市场
内容
概述
我曾用来交易的首个策略是基于烛条的,但现在我不会称其为策略,我的第一次开单交易源于一些蜡条形态,这些是我从本间宗久(Honma Munehisa)撰写的《烛条交易圣经》一书中学到的,这本书是我朋友推荐给我的。
金融图表中的烛条形态被交易者常用来基于过往形态判断可能的价格走势,它们由价格的上下波动生成,尽管看似随机,但交易者可利用它们来预测价格的短期走向。
这一概念起源于 1700 年代,由一位名叫本间宗久(Honma Munehisa)的交易员提出,他被认为是历史上最成功的交易员。在他所处时代被称为市场之神,他的发现若按今天的金钱计算,超过百亿美元。
宗久发现,虽然供需成本决定了大米的价格,但市场也受到人类情绪的影响。
这些人类情绪能够反应在烛条上,其中价格移动大小会带有不同颜色,黑色(阴线)蜡烛通常代表跌势,白色(阳线)蜡烛则代表涨势,这些颜色如今已无关紧要,因为它们可以在任何交易平台上改变。
烛条基础

烛条的影线或灯芯展现当天的最高和最低价格,以及它们与开盘价和收盘价的比较。蜡烛的形状取决于其开盘价、最高价、最低价、和收盘价之间的关系。宗久当年引入了许多烛条形态,最近交易者也曝露了一些新的。
基于这些烛条形态,我们就能识别、收集、并应用到机器学习模型当中,观察这些烛条形态如何为我们的 AI 交易模型增值,看看它们是否能帮助我们击败金融市场。
烛条形态
由于烛条形态有很多,我们就讨论 10 种,主要原因在于它们易于理解,且仅依赖单根柱线,即能在当前柱线上检测它们。
我们将舍弃所有需要处理多根烛条才能检测的烛条形态。
CTALib 类的编码格式灵感来自 TALib(技术分析函数库),这是一个基于 C#、C++ 和 Python 的技术分析函数库,包含若干检测烛条形态的函数。
一根白色蜡烛(阳线)
这是一个涨势烛条,其收盘价高于开盘价。这个简单的涨势烛条示意上升走势,它并不提供任何预测,它只示意蜡烛的走向。
我们能够如下为其编码。
bool CTALib::CDLWHITECANDLE(double open, double close) { return (close > open); }
一根黑色蜡烛(阴线)
对比阳线,这是看跌烛条,其开盘价高于收盘价。这个简单的看跌烛条形态示意下行走势。
类似于阳线,它并不提供任何预测,它只是示意蜡烛的走向。
bool CTALib::CDLBLACKCANDLE(double open,double close) { return (open > close); }
一个十字星蜡烛形态
一根十字星烛条的开盘价和收盘价近乎相等。它看起来像一个十字,有时是倒十字,或者一个加号。这种烛条形态较为罕见,常以簇状出现。它通常是趋势逆转信号,也可能是市场未来价格犹豫不决的迹象。
它有一些变体,如下图所示。

编写函数检测十字星烛条可能比较棘手,简单说原因在于十字星蜡烛的定义。为把一根蜡烛视为十字星,开盘价和收盘价两者必须相等。如今的市场因为体量和波动性,即使价格过于接近,也极少见到开盘价和收盘价相等;开盘价 = 1.0000,收盘价 = 1.0001,对于计算机来讲这俩并不相等。
为此,我们能用容差值,若开盘价和收盘价的差值在该值范围内,我们就视价格过于接近,即那是一个十字星烛条形态。
bool CTALib::CDLDOJI(double open,double close, double torrelance=3) { return (fabs(open-close)<torrelance*_Point); //A torrelance in market points }
蜻蜓十字星
蜻蜓十字星烛条形态是证券价格潜在出现逆转的信号。如果蜻蜓十字星形态出现在下降趋势中,这是好信号,即价格即将上涨、或下跌趋势或许结束。

由于这只是一个下影线比上影线更长的十字星烛条,在代码中,我们能够先确认蜡烛是十字星,然后必须明确保证下影线长度是上影线的两倍以上。
bool CTALib::CDLDRAGONFLYDOJI(double open, double high, double low, double close, double body_torrelance = 3, double shadow_ratio = 2.0) { double body_size = MathAbs(open - close); double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); //--- Body is very small (like a Doji) if (CDLDOJI(open, close, body_torrelance)) { //--- Lower shadow is significantly longer than upper shadow if (lower_shadow > upper_shadow * shadow_ratio) return true; } return false; }
墓碑十字星
这是一个带有长上影线(灯芯)的十字星烛条。这是一个看跌逆转策略,即示意市场方向发生变化。
当这根蜡烛出现在上升趋势时,示意当前趋势或许结束了,下降趋势即将发生。

由于这只是一根上影线比下影线更长的十字星蜡烛,在代码中,我们可以先确认蜡烛是十字星,然后必须明确保证上影线长度是下影线两倍以上。
bool CTALib::CDLGRAVESTONEDOJI(double open, double high, double low, double close, double body_torrelance = 3, double shadow_ratio = 2.0) { double body_size = MathAbs(open - close); double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); //--- Body is very small (like a Doji) if (CDLDOJI(open, close, body_torrelance)) { //--- Lower shadow is significantly longer than upper shadow if (upper_shadow > lower_shadow * shadow_ratio) return true; } return false; }
锤子
这是一根顶部有一个小实体,且下影线很长的蜡烛。这是一个涨势逆转信号,当出现在下跌趋势时,意味着前方潜在涨势。

烛条形态抽象、且令人困惑,有时形态会因您如何看待它、以及您此刻正寻找什么而变化。锤子形态会与蜻蜓十字星混淆,因为两者的不同之处只是实体大小,但它们的形状特征相同。
这是我们在与烛条形态打交道时经常遇到的问题之一。
这个问题可以通过设定明确的规则和阈值来应对,这些阈值能根据特定金融产品(品种)、和其它需求加以调整。
bool CTALib::CDLHAMMER(double open, double high, double low, double close, double min_body_percentage = 0.2, // To avoid being a doji double lower_shadow_ratio = 2.0, // Lower shadow at least 2x the body double upper_shadow_max_ratio = 0.3) // Upper shadow must be small { double body_size = MathAbs(open - close); double total_range = high - low + DBL_EPSILON; double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); double body_percentage = body_size / total_range; return ( body_percentage >= min_body_percentage && lower_shadow >= lower_shadow_ratio * body_size && upper_shadow <= upper_shadow_max_ratio * body_size ); }
由于蜻蜓十字星和锤子蜡烛两者的下影线都较长,实体较小,上影线较短,我们不得不引入 min_body_percentage,来检查蜡烛实体相对于总体范围(最高价-最低价)有多小,同时还要检查下影线长度是否大于上影线两倍以上。
倒锤
这与锤子类似,但底部较小,上影线较长,下影线较短。
这种形态若在下跌趋势中发现,就意味着市场即将出现涨势逆转。

我们能按锤子的类似方式来编码倒锤,仅略微修改阴影线。
bool CTALib::CDLINVERTEDHAMMER(double open, double high, double low, double close, double min_body_percentage = 0.2, // Avoid doji double upper_shadow_ratio = 2.0, // Upper shadow must be long double lower_shadow_max_ratio = 0.3) // Lower shadow should be small { double body_size = MathAbs(open - close); double total_range = high - low + DBL_EPSILON; double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); double body_percentage = body_size / total_range; return ( body_percentage >= min_body_percentage && upper_shadow >= upper_shadow_ratio * body_size && lower_shadow <= lower_shadow_max_ratio * body_size ); }
陀螺
这是一根中央有一个小实体,两端有长影线的烛条。

当这根蜡烛出现时,意味着市场犹豫不决,往往意味着当前趋势持续,买卖双方都没有把握。
由于该蜡烛类似于十字星烛条(仅在于其实体更大),我们必须明确确保实体不像十字星那样小,且它位于相对于实体长得多的影线中心。
bool CTALib::CDLSPINNINGTOP(double open, double high, double low, double close, double body_percentage_threshold = 0.3, double shadow_ratio = 2.0, double shadow_symmetry_tolerance = 0.3) { double body_size = MathAbs(open - close); double total_range = high - low + DBL_EPSILON; double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); double body_percentage = body_size / total_range; //--- Calculate shadow symmetry ratio double shadow_diff = MathAbs(upper_shadow - lower_shadow); double shadow_sum = upper_shadow + lower_shadow + DBL_EPSILON; double symmetry_ratio = shadow_diff / shadow_sum; // Closer to 0 = more balanced return ( body_percentage < body_percentage_threshold && // Body is small compared to candle size upper_shadow > body_size * shadow_ratio && // Both shadows are significantly larger than the body lower_shadow > body_size * shadow_ratio && symmetry_ratio <= shadow_symmetry_tolerance //Shadows are roughly equal (symmetrical) ); }
光头阳线
“光头”这个名字来源于日语中“短寸”的意思,指代没有影线的蜡烛。
光头阳线蜡烛是带有较小或没有上下影线(灯芯)的阳线,它是动量强烈的看涨信号。

我们能够增加以点数为单位的容差值,以检查开盘价和收盘价是否非常接近最高价/最低价。
bool CTALib::CDLBULLISHMARUBOZU(double open, double high, double low, double close, double tolerance = 2) { return (MathAbs(open - low) <= (tolerance*_Point) && MathAbs(close - high) <= (tolerance*_Point) && close > open); }
光头阴线
光头阴线蜡烛是带有较小或没有上下影线(灯芯)的跌势蜡烛,它是动量强烈的跌势信号。

类似于光头阳线烛条,我们有一个容差点数,用于检查开盘价和收盘价是否非常接近最高价/最低价。
bool CTALib::CDLBEARISHMARUBOZU(double open, double high, double low, double close, double tolerance = 2) { return (MathAbs(open - high) <= (tolerance*_Point) && MathAbs(close - low) <= (tolerance*_Point) && close < open); }
目前,我们仅研究基于外观检测烛条形态及其信号,但据我的消息来源,从烛条提取信号的正确途径,必须包括趋势检测,例如,若视锤子为涨势信号,则它必须出现在下行跌势之中。
趋势是方程式中至关重要的一部分,如果您打算进一步推进该项目,或许需要考虑这一点。
烛台形态检测指标
我们用推导它们的代码来可视化烛条形态。很简单,因为我经常发现这些形态抽象、且令人困惑,而且我们打算将这些数据用于机器学习目的,其中我们知道数据品质最为重要。通过可视化这些形态,我们确保您了解这些形态是如何计算的,及其视觉效果。
根据您的议员,调整一些参数和修改代码,无需犹豫。
我们确保至少刚写的代码能够识别这些作为人类也能从市场上发现的形态。
这个基于烛条的指标在主图上有 5 个缓冲区和一个图表。
文件名: Candlestick Identifier.mq5
#property indicator_chart_window #property indicator_buffers 5 #property indicator_plots 1 #property indicator_type1 DRAW_COLOR_CANDLES #property indicator_color1 clrDodgerBlue, clrOrange, clrRed #property indicator_style1 STYLE_SOLID #property indicator_width1 1 double OpenBuff[]; double HighBuff[]; double LowBuff[]; double CloseBuff[]; double ColorBuff[]; #include <ta-lib.mqh> //!important for candlestick patterns
由于 ta-lib.mqh 函数库是静态类,无需初始化其类,我们可以在 OnCalculate 函数内直接调用检测烛条形态的函数。
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<1) return rates_total; for(int i = prev_calculated; i < rates_total; i++) { OpenBuff[i] = open[i]; HighBuff[i] = high[i]; LowBuff[i] = low[i]; CloseBuff[i] = close[i]; //--- if (close[i]>open[i]) ColorBuff[i] = 1.0; else ColorBuff[i] = 0.0; //--- double padding = MathAbs(high[i] - low[i]) * 0.2; // 20% padding if (CTALib::CDLDOJI(open[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding, "Doji", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLDRAGONFLYDOJI(open[i], high[i], low[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"DragonFly Doji", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLGRAVESTONEDOJI(open[i], high[i], low[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"GraveStone Doji", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLHAMMER(open[i], high[i], low[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Hammer", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLINVERTEDHAMMER(open[i], high[i], low[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Inverted Hammer", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLSPINNINGTOP(open[i], high[i], low[i], close[i], 0.3, 2.0)) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Spinning Top", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLBULLISHMARUBOZU(open[i], high[i], low[i], close[i], 2)) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bullish Marubozu", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLBEARISHMARUBOZU(open[i], high[i], low[i], close[i], 2)) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bearish Marubozu", clrBlack, 90.0); ColorBuff[i] = 2.0; } } //--- return value of prev_calculated for next call return(rates_total); }
默认情况下,阳线是橙色,阴线是蓝色,若检测到任何本文讨论过的形态都会以红色标注,并伴随 90 度角的文字说明,标示该红色烛条所属的烛条形态类型。

正如您在上图中所见,我们的烛条检测指标识别这些烛条形态时干得不错,现在我们对代码中所应用的逻辑充满信心。
现在我们先继续收集这些形态,并用脚本把它们存储到 CSV 文件之中,然后用这些信息来训练机器学习模型。
收集烛条形态以便机器学习
由于某些烛条形态在市场上并不常见,尤其是较高时间帧,其历史数据仅有少量柱线,我们先收集从 2005 年 1 月 1 日到 2023 年 1 月 1 日的数据。
这 18 年的时间区段应能为我们给出大量日线时间帧的柱线,从而为我们的机器学习模型提供大量可观察的形态。
#include <ta-lib.mqh> //Contains CTALib class for candlestick patterns detection #include <MALE5\Pandas\pandas.mqh> //https://www.mql5.com/en/articles/17030 input datetime start_date = D'2005.01.01'; input datetime end_date = D'2023.01.01'; input string symbol = "XAUUSD"; input ENUM_TIMEFRAMES timeframe = PERIOD_D1; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ 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; 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); }
我们还为目标变量收集了 OHLC(开盘价、最高价、最低价、收盘价)数值,以备万一有啥事发生,或我们要用到它们。
训练 AI 模型来基于烛条形态进行预测
现在我们有了数据集,我们利用 Python 脚本(Jupyter 笔记簿)加载这些数据。
import pandas as pd symbol = "XAUUSD" df = pd.read_csv(f"/kaggle/input/forex-candlestick-patterns/CandlestickPatterns.{symbol}.PERIOD_D1.csv") df
输出
| 阳线 | 阴线 | 十字星蜡烛 | 蜻蜓十字星蜡烛 | 墓碑十字星蜡烛 | 锤头蜡烛 | 倒锤蜡烛 | 陀螺蜡烛 | 光头阳线 | 光头阴线 | 开盘价 | 最高价 | 最低价 | 收盘价 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 438.45 | 438.71 | 426.72 | 429.55 |
| 1 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 429.52 | 430.18 | 423.71 | 427.51 |
| 2 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 427.50 | 428.77 | 425.10 | 426.58 |
| 3 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 426.31 | 427.85 | 420.17 | 421.37 |
| 4 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 421.39 | 425.48 | 416.57 | 419.02 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
当处置这些烛条数据时,浮现的最大挑战是如何设定目标变量。
在大多数与金融市场相关的分类问题中,我们往往基于未来价格走势定义目标变量,用到一个我们常称为 "lookahead" 的参数。
该参数指定我们往前看多少根柱线(或时间步)数据。例如,如果 lookahead 设置为 1,我们比较当前柱线与下一根柱线的收盘价:
If Close[next bar] > Close[current bar],表示看涨走势,故我们分配一个目标标签 1。
否则,如果不满足该条件,则表示看跌走势,故我们分配一个标签 0。
我们也能于此做同样的事,确保提前采用特征预测信息,但如上表概括,数据中有许多零,除了阳线和阴线外,我们往往在每一行没有检测出任何特殊的柱线,这些蜡烛不是特殊形态,我们也不把它们视为烛条形态。
这意味着我们会经常给机器学习模型零值,强迫模型理解这种关系,并在没有给出数值的情况下预测目标变量,这会导致模型过度依赖阳线和阴线,这是我们不希望看到的。
我们能够丢弃所有烛条形态中全部为零值的行,并用纯烛条数据训练模型来应对这个问题,这也要求我们避免模型运行时出现同样的状况。
另一种应对途径是在所有烛条形态均为 0(假)的行中引入保持类信号,可由 -1 示意,除了阳线和阴线列,但这会引发我们在前一篇文章中讨论过的巨大问题:类不平衡,但即便如此,这种方式仍不能解决该问题。
至于现在,我们无论如何都要准备目标变量。
lookahead = 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(1) else: signal.append(0) new_df["Signal"] = signal
然后我们将预测变量拆分为名为 X 的二维数组,同时除去不想要的特征,如 OHLC 数值、我们打算预测的列(目标)、以及推导目标列的 future_close 贴近特征。我们还将名为 Signal 的目标列分配到 y 数组。
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=0.2, random_state=42, shuffle=False)
对于这个问题,我选择用 Catboost 模型来处置,在于我们有很多类别列,理论上应当能配合 Catboost 分类器良好工作。
from catboost import CatBoostClassifier from sklearn.utils.class_weight import compute_class_weight # Automatically calculate class weights classes = np.unique(y) weights = compute_class_weight(class_weight='balanced', classes=classes, y=y) class_weights = dict(zip(classes, weights)) # Define the base model model = CatBoostClassifier( iterations=1000, learning_rate=0.01, depth=5, loss_function='Logloss', class_weights=class_weights, verbose=100 ) model.fit(X_train, y_train) # Training the classifier
输出:
0: learn: 0.6930586 total: 3.64ms remaining: 3.64s 100: learn: 0.6897625 total: 136ms remaining: 1.21s 200: learn: 0.6888030 total: 269ms remaining: 1.07s 300: learn: 0.6883559 total: 401ms remaining: 931ms 400: learn: 0.6881469 total: 532ms remaining: 795ms 500: learn: 0.6879966 total: 661ms remaining: 658ms 600: learn: 0.6879013 total: 789ms remaining: 524ms 700: learn: 0.6878311 total: 916ms remaining: 391ms 800: learn: 0.6877729 total: 1.04s remaining: 260ms 900: learn: 0.6877273 total: 1.17s remaining: 129ms 999: learn: 0.6876900 total: 1.3s remaining: 0us <catboost.core.CatBoostClassifier at 0x798cc6d08dd0>
我们据模型之前从所未见的数据上(测试样本)来评估。
y_pred = model.predict(X_test)
print("\nClassification Report:\n", classification_report(y_test, y_pred)) 输出:
Classification Report: precision recall f1-score support 0 0.49 0.55 0.52 429 1 0.58 0.52 0.55 511 accuracy 0.53 940 macro avg 0.53 0.53 0.53 940 weighted avg 0.54 0.53 0.53 940
结果示意,对于类 1 和类 0,模型的精度分别为 0.58 和 0.49,而我们能够依赖该模型预测类 1,且其做到 58% 确定性,但我们不能依赖它预测类 0,我们在这种情况下盲猜更好。
整体准确率为 53%,满分100%,这在该交易领域是现实的,这比抛硬币、或随机盲猜的 50/50 胜率更有保证。
我们查看特征重要性制图,观察哪些特征对该模型影响最大。
import matplotlib.pyplot as plt # Get feature importances importances = model.get_feature_importance() feature_names = X_train.columns if hasattr(X_train, 'columns') else [f'feature_{i}' for i in range(X_train.shape[1])] # Create DataFrame for plotting feat_imp_df = pd.DataFrame({ 'Feature': feature_names, 'Importance': importances }).sort_values(by='Importance', ascending=False) # Plot plt.figure(figsize=(7, 3)) plt.barh(feat_imp_df['Feature'], feat_imp_df['Importance']) plt.gca().invert_yaxis() # Highest importance on top plt.title('Feature Importances') plt.xlabel('Importance') plt.ylabel('Feature') plt.tight_layout() plt.show()
输出:

陀螺烛条形态对于该模型是最具冲击力的特征,其次是十字星烛条,光头阴线烛条的影响力较小。
根据我对这些烛条形态的了解,有些形态意在指明趋势,或覆盖某个宽泛区间、或横向范围的市场变化,例如,十字星蜡烛出现在上行趋势顶端,可能标示下行趋势即将在某一刻发生。
我们基于 lookahead 参数值 1 创建了目标变量,故我们用这些烛条形态来标示前方一根柱线,替代前方一定数量的柱线。
故可随意探索大于 1 的不同 lookahead 值,观察这些烛条形态在某段宽泛区间、或不同横向范围的影响,在我迄今的探索中,我发现按 lookahead 值为 1 训练,产生了最准确的模型。
我们正坚持 lookahead 值为 1,因为我相信我们能够通过在最终交易机器人中设置止损和止盈值,或者在经过一定数量的柱线后平仓来应对预测性横向范围问题。
在交易机器人里完结它
现在我们已得到基于烛条形态训练的模型,我们在实际交易环境中测试它,看看烛条形态在人工智能(AI)领域是否实用。
首先,我们必须将模型保存为 MQL5 和 MetaTrader 5 兼容的 ONNX 格式。
model_onnx = convert_sklearn( model, "catboost", [("input", FloatTensorType([None, X_train.shape[1]]))], target_opset={"": 12, "ai.onnx.ml": 2}, ) # And save. with open(f"CatBoost.CDLPatterns.{symbol}.onnx", "wb") as f: f.write(model_onnx.SerializeToString())
关于如何保存该 Catboost 模型的更多信息,能在这里找到。
我们的智能系统(EA)相当简单。
#include <Trade\Trade.mqh> //The trading module #include <Trade\PositionInfo.mqh> //Position handling module #include <ta-lib.mqh> //For candlestick patterns #include <Catboost.mqh> //Has a class for deploying a catboost model CTrade m_trade; CPositionInfo m_position; CCatboostClassifier catboost; input int magic_number = 21042025; input int slippage = 100; input string symbol_ = "XAUUSD"; input ENUM_TIMEFRAMES timeframe_ = PERIOD_D1; input int lookahead = 1; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if (!MQLInfoInteger(MQL_TESTER)) if (!ChartSetSymbolPeriod(0, symbol_, timeframe_)) { printf("%s failed to set symbol %s and timeframe %s, Check these values. Err = %d",__FUNCTION__,symbol_,EnumToString(timeframe_),GetLastError()); return INIT_FAILED; } //--- if (!catboost.Init(StringFormat("CatBoost.CDLPatterns.%s.onnx",symbol_), ONNX_COMMON_FOLDER)) //Initialize the catboost model return INIT_FAILED; //--- m_trade.SetExpertMagicNumber(magic_number); m_trade.SetDeviationInPoints(slippage); m_trade.SetMarginMode(); m_trade.SetTypeFillingBySymbol(Symbol()); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- double open = iOpen(Symbol(), Period(), 1), high = iHigh(Symbol(), Period(), 1), low = iLow(Symbol(), Period(), 1), close = iClose(Symbol(), Period(), 1); vector x = { CTALib::CDLWHITECANDLE(open, close), CTALib::CDLBLACKCANDLE(open, close), CTALib::CDLDOJI(open, close), CTALib::CDLDRAGONFLYDOJI(open, high, low, close), CTALib::CDLGRAVESTONEDOJI(open, high, low, close), CTALib::CDLHAMMER(open, high, low, close), CTALib::CDLINVERTEDHAMMER(open, high, low, close), CTALib::CDLSPINNINGTOP(open, high, low, close), CTALib::CDLBULLISHMARUBOZU(open, high, low, close), CTALib::CDLBEARISHMARUBOZU(open, high, low, close) }; long signal = catboost.predict(x).cls; //Predicted class MqlTick ticks; if (!SymbolInfoTick(Symbol(), ticks)) { printf("Failed to obtain ticks information, Error = %d",GetLastError()); return; } double volume_ = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); if (signal == 1) { if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL)) m_trade.Buy(volume_, Symbol(), ticks.ask,0,0); } if (signal == 0) { if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY)) m_trade.Sell(volume_, Symbol(), ticks.bid,0,0); } CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe }
初始化被保存在 Common 文件夹之中, ONNX 格式的 Catboost 模型之后。
在 OnTick 函数内,我们获取之前收盘柱线的开盘价、最高价、最低价、和收盘价,并用来自 CTALib 的函数解析它们,检测烛条形态,然后我们用其成果在一个名为 x 的矢量中进行预测。
我们必须注意这些特性及其顺序,在于 Python 脚本训练最终 Catboost 模型时会用到它们。
X_train.columns
在我们的最终模型中,我们已得到。
Index(['White Candle', 'Black Candle', 'Doji Candle', 'Dragonflydoji Candle', 'Gravestonedoji Candle', 'Hammer Candle', 'Invertedhammer Candle', 'Spinningtop Candle', 'BullishMarubozu Candle', 'BearishMarubozu Candle'], dtype='object')
在智能系统中会预留该顺序。
目前,我们没有止损及其相应的止盈值,故在给定时间帧经历 lookahead 指定数量的柱线之后,我们平仓。
测试器配置。

结果

令我着迷的是成果当中多空交易相似,在两年期内空头交易数量为 257 笔。比多头交易少 2 笔,多头交易为 259 笔。
这不太相适,我们可以说,尽管模型涵盖了所有烛条形态,但影响最大的还是阳线和阴线,因为在每根柱线上它们都会出现其一,我们还在一根柱子后平仓(lookahead = 1),该问题源于我们如何准备目标变量,且训练模型时采用的是许多特征中包含零(假)值的数据。
为了确保遵循唯一的烛条形态,我们必须检查所有特殊烛条形态为 0(假)的时刻 — 模型未检测到,并当其发生时,防止开立交易。
我们打算仅在检测到除阴线、阳线烛条外的独特烛条形态时才开立交易。
void OnTick() { //--- double open = iOpen(Symbol(), Period(), 1), high = iHigh(Symbol(), Period(), 1), low = iLow(Symbol(), Period(), 1), close = iClose(Symbol(), Period(), 1); vector x = { CTALib::CDLWHITECANDLE(open, close), CTALib::CDLBLACKCANDLE(open, close), CTALib::CDLDOJI(open, close), CTALib::CDLDRAGONFLYDOJI(open, high, low, close), CTALib::CDLGRAVESTONEDOJI(open, high, low, close), CTALib::CDLHAMMER(open, high, low, close), CTALib::CDLINVERTEDHAMMER(open, high, low, close), CTALib::CDLSPINNINGTOP(open, high, low, close), CTALib::CDLBULLISHMARUBOZU(open, high, low, close), CTALib::CDLBEARISHMARUBOZU(open, high, low, close) }; vector patterns = { CTALib::CDLDOJI(open, close), CTALib::CDLDRAGONFLYDOJI(open, high, low, close), CTALib::CDLGRAVESTONEDOJI(open, high, low, close), CTALib::CDLHAMMER(open, high, low, close), CTALib::CDLINVERTEDHAMMER(open, high, low, close), CTALib::CDLSPINNINGTOP(open, high, low, close), CTALib::CDLBULLISHMARUBOZU(open, high, low, close), CTALib::CDLBEARISHMARUBOZU(open, high, low, close) }; //Store all the special patterns long signal = catboost.predict(x).cls; //Predicted class MqlTick ticks; if (!SymbolInfoTick(Symbol(), ticks)) { printf("Failed to obtain ticks information, Error = %d",GetLastError()); return; } double volume_ = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); if (signal == 1 && patterns.Sum()>0) //Check if there are is atleast a special pattern before opening a trade { if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL)) m_trade.Buy(volume_, Symbol(), ticks.ask,0,0); } if (signal == 0 && patterns.Sum()>0) //Check if there are is atleast a special pattern before opening a trade { if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY)) m_trade.Sell(volume_, Symbol(), ticks.bid,0,0); } CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe }
测试结果。


看似现在好多了,开立了少量交易,这在于我们基于日线时间帧训练模型,这与在更高时间帧上这些烛条形态的稀缺性相呼应。
盈利交易的百分比为 54.55%,与分类报告中获得的 0.53(53%)总体准确率非常接近,这种相似性表明我们正走在正确的路途上。
结束语
故此,在配合人工智能(AI)模型工作时,利用烛条形态进行预测是可能的,并在最终成果上进行市场预测,然而,不同于常用指标和数学计算,像是我们常用来预测市场的典型数据,烛台形态在收集数据、及创建市场中可观测的烛条特征时,需要对细节进行充分研究和细致关注,对蜡烛的一点小误解,都能够导致截然不同的成果。
话说我们的欲望和信念影响我们如何感知和解读信息。换言之,我们所见是我们想看到的。
我相信在处置烛条形态时,这会很大程度上发生,如果您正在找锤子,那一个蜻蜓十字星看起来也像锤子,反之亦然。
我相信为了让机器学习获取最优性能,在准备基于烛条形态的数据时,需要大量试错。
此致敬意。
源码 & 参考
附件表
| 文件名 | 说明/用法 |
|---|---|
| Experts\CandlestickPatterns AI-EA.mq5 | 一个智能系统(EA),部署 catboost 模型,基于烛条形态进行预测。 |
| Indicators\Candlestick Identifier.mq5 | 在图表上显示烛条形态的指标。 |
| Scripts\Candlestick Patterns Collect.mq5 | 一个收集烛条形态,并将这些信息存储到 CSV 文件中的脚本。 |
| Include\Catboost.mqh | 一个包含加载、初始化、和部署 catboost 分类器类的函数库,用于市场预测。 |
| Include\pandas.mqh | 类似 Python 的 Pandas 模块,用于数据存储和操纵。 |
| Include\ta-lib.mqh | 技术分析函数库,由检测烛条形态的类组成。 |
| Common\Files\*.csv | 包含烛条数据的 CSV 文件,用于机器学习。 |
| Common\Files\*.onnx | ONNX 格式的机器学习模型 |
| CandlestickMarket Prediction.ipynb | 一个用于训练 Catboost 模型的 Python 脚本(Jupyter 记事簿)。 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/17832
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
在 MQL5 中构建自优化智能交易系统(第八部分):多策略分析
新手在交易中的10个基本错误
我在文章中已经解释过这一点。
现在,我们只考虑根据烛台的外观来检测烛台形态及其信号,但根据我的资料,从烛台中提取信号的正确方法必须包括趋势检测,例如,要将锤子线视为看涨信号,它必须出现在下降趋势中。
趋势是等式中的一个关键部分,如果你想进一步研究这个项目,不妨考虑一下。
价格行为很重要,这一点无可否认。
我在文章中已经解释过了。
现在,我们只考虑根据烛台的外观来检测烛台形态及其信号,但根据我的资料,从烛台中提取信号的正确方法必须包括趋势检测,例如,要将锤子视为看涨信号,它必须出现在下降趋势中。
趋势是等式中的一个关键部分,如果你想进一步研究这个项目,不妨考虑一下。
价格行为很重要,这一点无可否认。
你的引述与我观点的主旨无关。
你好项目 肯定会基于这些思路。
祝您圣诞快乐