利用 CatBoost 算法寻找外汇市场的季节性模式·综合运用
📅

利用 CatBoost 算法寻找外汇市场的季节性模式·综合运用

(3/3)·从时间过滤器函数到长历史回测,用机器学习剥离人为因素后的真实表现

进阶 第 3/3 篇
手动框定「周二 15:00 才出手」听起来简单,但跨品种、跨周期逐个试错极耗精力。CatBoost 配合时间过滤器能把这类季节性约束直接变成训练条件,让人为偏见退出循环。本篇把前两步的统计基础收口为可运行的算法交易框架。

◍ 用高斯混合给伦敦盘初段价格打标签

这段脚本把回测窗口锁在每天 3 点(小时级过滤),先拉取 START_DATE 到 STOP_DATE 的报价,再用 add_labels 以 5~25 点波动带打分类标签,time_filter 保证只处理目标时段。外汇与贵金属属高风险品种,此类标签仅反映历史统计分布,不预示未来。 聚类部分用 GaussianMixture(n_components 自定义、covariance_type='full'、n_init=1)对价格矩阵第 2 列往后的特征拟合,放弃贝叶斯版本以提速。随后跑 10 轮 brute_force(每轮 1 万次搜索),打印每轮 R^2,最后按结果排序取最优模型做 test_model 验证。 实操上,把 hours = [3] 改成 [8,9] 可切到亚盘重叠段;n_init=1 在样本少时可能欠稳,可提到 5 对比 R^2 波动。

MQL5 / C++
hours = [class="num">3]
# make dataset
pr = get_prices(START_DATE, STOP_DATE)
pr = add_labels(pr, min=class="num">5, max=class="num">25, 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])

持仓周期怎么挑才不拖累模型

想看持仓周期(以柱数计)对模型质量的干扰,可以写一个扫描函数,把 1 到 49 根柱的固定生存期挨个跑一遍,记录每次的 R^2。我在 GBPUSD 第 5 小时切片上做了这轮迭代,X 轴是交易频率(柱数),Y 轴是 R^2 分数。 结果很直白:0–5 柱的极短持仓对拟合有明显负面作用,15–23 柱区间得分最优;超过 30 柱反而把结果拖坏。另有一个 6–9 柱的小簇,峰值最高,但实测并不稳。 拿 8 柱生存期从 2013 年回测,余额曲线并不平滑;而 6–9 簇自 2015 年后图好看,更早的历史段却垮掉。换 15–23 簇多次重训(因抽样随机),模型在 2015 年前仍显示不出数据生存性——市场结构可能变了,这事得单开大样本研究。 选定区间并证伪完某段稳定性后,再把测试样本并回全区间训一次,才能考虑实盘。外汇与贵金属杠杆高,这类周期筛选只降低过拟合概率,不承诺收益。

MQL5 / C++
def deals_frequency_analyzer():
    freq = [x for x in range(class="num">1, class="num">50)]
    result = pd.DataFrame()
    for _h in freq:
        pr = get_prices(START_DATE, STOP_DATE)
        pr = add_labels(pr, min=_h, max=_h, 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">5
        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; deal lifetime = &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.xticks(np.arange(class="num">0, len(freq)+class="num">1, class="num">1))
    plt.title("Performance by deals lifetime")
    plt.xlabel("deals frequency")
    plt.ylabel("R^class="num">2 estimation")
    plt.show()
    class="kw">return result
pr = add_labels(pr, min=class="num">15, max=class="num">23, filter=time_filter)

「拉长到三十年样本会怎样」

把回测窗口从近几年直接拉到长历史,能逼出模型在 regimes 切换下的真实表现。这套系统用 2000 年之后的数据做训练,再拿 1990 年以来的全样本做测试,属于典型的「训练段短、测试段长」交叉验证。 从余额曲线看,三十年跨度里模型对价格模式的捕捉明显变稀疏——很多 2000 年后的微观结构在 90 年代根本不存在,所以拟合度下降是预期内的。 但净结果依旧站在正数一侧,说明该信号逻辑在多个 decade 里没有彻底失效。外汇与贵金属属高风险品种,长历史正收益不代表未来样本同分布,上 MT5 用「1990.01.01–2024.12.31」做可视化回测,重点看曲线在 2008 与 2020 两端的回撤形状。

◍ 把季节性模型搬进 MT5 实盘前的一处时区坑

前文提到的季节性分析框架并不局限于外汇品种,对 FORTS 类期货、贵金属等工具同样可以套用不同过滤器做回测,但落地到 MT5 自动化时有个容易踩的偏移。评论区作者明确提示:Python 里算出的小时序号,在 MT5 中要向右移 1——比如 Python 第 4 小时,MT5 里对应的是第 5 小时。 这个偏移直接决定挂单触发窗口。下面这段从终端逻辑里抽出的代码片段,就是按「hour == 5」来卡时间的:先用 TimeToStruct 取当前结构,新 K 线才跑判断,没持仓且信号 sig<0.5 才发买单。你复制去测时,若发现季节窗口整体晚了一根小时线,先查是不是漏了这 +1。 外汇和贵金属杠杆高、滑点跳空频繁,这类按小时触发的机器人实盘前务必在策略测试器用对应品种跑至少三年历史。模型导出只是起点,过滤器组合和时区对齐才是能不能跑通的关键。

MQL5 / C++
class="type">MqlDateTime hours;
class="type">void OnTick() {
class=class="str">"cmt">//---
   if(!isNewBar()) class="kw">return;
   TimeToStruct(TimeCurrent(), hours);
}
if(hours.hour == class="num">5 && countOrders() == class="num">0 && CheckMoneyForTrade(_Symbol,LotsOptimized(),ORDER_TYPE_BUY)) {
      if(sig < class="num">0.5)
         OrderSend(Symbol(),OP_BUY,LotsOptimized(), Ask, class="num">0, Bid-stoploss*_Point, Ask+takeprofit*_Point, NULL, OrderMagic);
      else if(sig > class="num">0.5)
         OrderSend(Symbol(),OP_SELL,LotsOptimized(), Bid, class="num">0, Ask+stoploss*_Point, Bid-takeprofit*_Point, NULL, OrderMagic);
      class="kw">return;
   }
让小布替你跑这套
把时间过滤与逐小时探索性分析交给小布盯盘的后台 AIGC,你只需打开对应外汇品种页查看哪些时段 R^2 更稳定,专注决定是否跟单。

常见问题

标记 2 代表不满足时间过滤器条件的柱,留着会稀释特定时段模式,删除后只保留由过滤器圈定的买卖样本用于训练。
测试起始无持仓设为 2,遍历数据时仅当过滤器满足才触发交易,平仓不受过滤约束,从而还原特定小时的进出场逻辑。
目前小布提供各品种的时段胜率与稳定性诊断,过滤器脚本需自行部署,但参考输出可直接比对小布给出的时段分布。
关采样保纯度,加迭代平滑单小时 R^2 噪声,避免把偶然高分误判为稳健季节性。
外汇高风险,旧机制可能失效,长历史回测仅揭示概率倾向,实盘前仍需分段验证。