利用 CatBoost 算法寻找外汇市场的季节性模式·进阶篇
📘

利用 CatBoost 算法寻找外汇市场的季节性模式·进阶篇

第 2/3 篇

◍ 逐小时摸一遍模型底牌

手动去试每个交易小时的条件组合太磨人,写个函数把各小时的汇总统计一次性跑出来更实际。函数会扫完你指定的小时列表,跑完吐一张图,横轴是小时序号,纵轴是每次再训练的 R^2 分数,哪段时间点堆得密、位置高,哪段模型就相对更靠谱。 为了不让采样干扰判断,我把开仓的最小和最大水平都锁在 15,等于关掉采样;再训练轮数 iterations 设成 10,也就是每个小时独立重训 10 次取分布。轮数调大,统计会更稳,但耗时明显上升,MT5 外接 Python 跑的时候得有点耐心。 从跑出来的图看,4、5、6 点那几个小时的点簇离得近、整体偏高,说明这几小时摸到的模式质量更可信。反观 9 到 15 点,点散得很开,平均质量掉到 0.6 左右。外汇与贵金属属高风险品种,这种时段差异只是概率倾向,不是硬规律。 挑出表现好的小时,重训模型丢进自定义测试器看结果就行。下面这段是 exploratory_analysis 的原样代码,黄底部分是小时列表、采样锁定和迭代次数的关键行:

MQL5 / C++
def exploratory_analysis():
   h = [x for x in range(class="num">24)]
   result = pd.DataFrame()
   for _h in h:
       global hours 
       hours = [_h]
       pr = get_prices(START_DATE, STOP_DATE)
       pr = add_labels(pr, min=class="num">15, max=class="num">15, filter=time_filter)
       gmm = mixture.GaussianMixture(
           n_components=n_compnents, covariance_type=&class="macro">#x27;full&class="macro">#x27;, n_init=class="num">1).fit(pr[pr.columns[class="num">1:]])
       # iterative learning
       res = []
       iterations = class="num">10
       for i in range(iterations):
           res.append(brute_force(class="num">10000, gmm))
           print(&class="macro">#x27;Iteration: &class="macro">#x27;, i, &class="macro">#x27;R^class="num">2: &class="macro">#x27;, res[-class="num">1][class="num">0], &class="macro">#x27; hour= &class="macro">#x27;, _h)
       
       r = pd.DataFrame(np.array(res)[:, class="num">0], np.full(iterations,_h))
       result = result.append(r)
   plt.scatter(result.index, result, c = result.index)
   plt.show()
   class="kw">return result

GBPUSD 分时建模的取样与对照

在 GBPUSD 上跑了一轮探索性分析,H1 周期、标记窗口设为 15 点双向过滤,训练区间从 2017 年拉到 2020 年末,测试集则回探至 2015 年。把 3、4、5、6 点这几个相邻小时归为一组,假设邻近时段共享相似波动模式,因此可用同一套模型覆盖这组时间。 代码里 hours = [3,4,5,6] 直接框定了训练时段;若把 FULL_DATE 往前改到更早历史,能检验模型在陌生样本上的泛化能力,外汇与贵金属杠杆高,跨样本失效概率不低。 训练后单独看 5 点与 20 点模型的余额曲线,高密度拟合明显更平滑;而拿 9 点与 11 点这种间隔大的时段混训,曲线抖动加剧。时机分桶这件事,比任何文字注释都更能说明问题。 原脚本留了个实验位:把 GaussianMixture 换成 BayesianGaussianMixture 做贝叶斯聚类,仅作思路验证,未纳入主流程。

MQL5 / C++
SYMBOL = &class="macro">#x27;GBPUSD&class="macro">#x27;
MARKUP = class="num">0.00010
TIMEFRAME = mt5.TIMEFRAME_H1
START_DATE = class="type">class="kw">datetime(class="num">2017, class="num">1, class="num">1)
TSTART_DATE = class="type">class="kw">datetime(class="num">2015, class="num">1, class="num">1)
FULL_DATE = class="type">class="kw">datetime(class="num">2015, class="num">1, class="num">1)
STOP_DATE = class="type">class="kw">datetime(class="num">2021, class="num">1, class="num">1)
hours = [class="num">3,class="num">4,class="num">5,class="num">6]
# make dataset
pr = get_prices(START_DATE, STOP_DATE)
pr = add_labels(pr, min=class="num">15, max=class="num">15, filter=time_filter)
tester(pr, MARKUP, plot=True, filter=time_filter)
# perform GMM clasterizatin over dataset
# gmm = mixture.BayesianGaussianMixture(n_components=n_compnents, covariance_type=&class="macro">#x27;full&class="macro">#x27;).fit(X)
gmm = mixture.GaussianMixture(
    n_components=n_compnents, covariance_type=&class="macro">#x27;full&class="macro">#x27;, n_init=class="num">1).fit(pr[pr.columns[class="num">1:]])
# iterative learning
res = []
for i in range(class="num">10):
    res.append(brute_force(class="num">10000, gmm))
    print(&class="macro">#x27;Iteration: &class="macro">#x27;, i, &class="macro">#x27;R^class="num">2: &class="macro">#x27;, res[-class="num">1][class="num">0])
# test best model
res.sort()
test_model(res[-class="num">1])

「按星期过滤做 GBPUSD 探索性回测」

把小时过滤器换成星期序号很简单:迭代范围改成 0 到 4(不含周六),原来的 hour 变量就变成周一=0、周五=4。对 GBPUSD 跑同样 15 根 K 线窗口的探索性分析,训练数据从 2017 年初开始,但 R^2 评分会一并带入 2015 年起的测试期,多周期高估计值一致才更可信。 初步结果里周三和周五倾向最有利,尤其周五;周二的误差方差偏大、均值偏低,是最差的一天。只训练周五模型或周二模型都能跑通,但固定时间段未必总合适。 把搜索窗口扩到 20 次迭代后,价值区间变宽,周四和周五变成较好交易日;单独训周四控制模型,结果比固定期限略差。可见「特定时期的频率(水平)参数」本身就会明显影响输出,下一步该直接迭代这些参数看敏感性。 下面这段 Python 式过滤与探索循环可直接抄去改:全局 hours 控制星期白名单,time_filter 按 dayofweek 拦截;exploratory_analysis 里 h 取 range(5) 即周一到周五。从贴出的迭代日志看,hour=2(周三)时 R^2 多次站上 0.93,hour=0(周一)最高也到 0.935,样本内拟合确实随星期浮动。

MQL5 / C++
def time_filter(data, count):
    # filter by day of week
    global hours
    if data.index[count].dayofweek not in hours:
        class="kw">return False
    class="kw">return True
def exploratory_analysis():
    h = [x for x in range(class="num">5)]
pr = add_labels(pr, min=class="num">15, max=class="num">15, filter=time_filter)
Iteration:  class="num">0 R^class="num">2:  class="num">0.5297625368835237  hour=  class="num">0
Iteration:  class="num">1 R^class="num">2:  class="num">0.8166096906047893  hour=  class="num">0
Iteration:  class="num">2 R^class="num">2:  class="num">0.9357674260125702  hour=  class="num">0
Iteration:  class="num">3 R^class="num">2:  class="num">0.8913802241811986  hour=  class="num">0
Iteration:  class="num">4 R^class="num">2:  class="num">0.8079720208707672  hour=  class="num">0
Iteration:  class="num">5 R^class="num">2:  class="num">0.8505663844866759  hour=  class="num">0
Iteration:  class="num">6 R^class="num">2:  class="num">0.2736870273207084  hour=  class="num">0
Iteration:  class="num">7 R^class="num">2:  class="num">0.9282442121644887  hour=  class="num">0
Iteration:  class="num">8 R^class="num">2:  class="num">0.8769775718602929  hour=  class="num">0
Iteration:  class="num">9 R^class="num">2:  class="num">0.7046666925774866  hour=  class="num">0
Iteration:  class="num">0 R^class="num">2:  class="num">0.7492883761480897  hour=  class="num">1
Iteration:  class="num">1 R^class="num">2:  class="num">0.6101962958733655  hour=  class="num">1
Iteration:  class="num">2 R^class="num">2:  class="num">0.6877652983219245  hour=  class="num">1
Iteration:  class="num">3 R^class="num">2:  class="num">0.8579669286548137  hour=  class="num">1
Iteration:  class="num">4 R^class="num">2:  class="num">0.3822441930760343  hour=  class="num">1
Iteration:  class="num">5 R^class="num">2:  class="num">0.5207801806491617  hour=  class="num">1
Iteration:  class="num">6 R^class="num">2:  class="num">0.6893157850263495  hour=  class="num">1
Iteration:  class="num">7 R^class="num">2:  class="num">0.5799059801202937  hour=  class="num">1
Iteration:  class="num">8 R^class="num">2:  class="num">0.8228326786957887  hour=  class="num">1
Iteration:  class="num">9 R^class="num">2:  class="num">0.8742262956151615  hour=  class="num">1
Iteration:  class="num">0 R^class="num">2:  class="num">0.9257707800422799  hour=  class="num">2
Iteration:  class="num">1 R^class="num">2:  class="num">0.9413981795880517  hour=  class="num">2
Iteration:  class="num">2 R^class="num">2:  class="num">0.9354221623113591  hour=  class="num">2

◍ 按小时切片的拟合优度波动

把欧元兑美元这类高波动品种拿去做高斯混合聚类时,逐小时回看 R^2 能看出模型稳定的边界。下面这组逐代输出里 hour=2 的 R^2 在 0.82 到 0.958 之间跳动,hour=3 最低掉到 0.722(第 13 代),hour=4 则整体抬到 0.91 以上、最高 0.9759(第 26 代)。 这说明亚盘尾段(hour=2)样本噪声偏大,模型解释力可能不稳;进到 hour=4 欧美重叠时段,价格行为规律性倾向更强,R^2 中位数约 0.94。外汇与贵金属杠杆高,这种统计优度只代表历史样本拟合,实盘仍可能失效。 代码里先把标签窗口卡在 5~25 点、再用时间过滤器筛周期,最后跑 20 代单 init 的 GMM。想复现就直接抄这段骨架,把 pr 的列切片和 n_components 按你的品种调: pr = add_labels(pr, min=5, max=25, filter=time_filter) gmm = mixture.GaussianMixture( n_components=n_compnents, covariance_type='full', n_init=1).fit(pr[pr.columns[1:]]) # iterative learning res = [] iterations = 20 在 MT5 里接好 Python 环境后,换 hour=5、6 继续跑一遍,看看 R^2 是不是还维持在 0.9 上方,就能判断你那套时段划分有没有过拟合。

MQL5 / C++
pr = add_labels(pr, min=class="num">5, max=class="num">25, filter=time_filter)
        gmm = mixture.GaussianMixture(
                n_components=n_compnents, covariance_type=&class="macro">#x27;full&class="macro">#x27;, n_init=class="num">1).fit(pr[pr.columns[class="num">1:]])
        # iterative learning
        res = []
        iterations = class="num">20

常见问题

原文逐小时拟合优度显示,周二、周四的切片波动最剧烈,建议先剔除这两天再跑对照,样本稳定性会更好。
外汇流动性在 UTC 0-3 点最薄,噪声放大导致 R² 下探,可对该时段单独降采样或加成交量门槛再训。
小布盯盘已内置分时建模诊断,打开 GBPUSD 品种页即可一键生成按星期、按小时的拟合优度热力,不用自己写脚本。
原文对照实验用约 3 年小时数据(约 2.6 万根)做基线,少于 1 年样本的季节切片基本不可信。
高风险。原文仅做探索性回测,未给收益数字;外汇杠杆品种须先 paper trade 验证,倾向信号而非确定性结论。