利用 CatBoost 算法寻找外汇市场的季节性模式·综合运用
(3/3)·从时间过滤器函数到长历史回测,用机器学习剥离人为因素后的真实表现
◍ 用高斯混合给伦敦盘初段价格打标签
这段脚本把回测窗口锁在每天 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 波动。
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 年前仍显示不出数据生存性——市场结构可能变了,这事得单开大样本研究。 选定区间并证伪完某段稳定性后,再把测试样本并回全区间训一次,才能考虑实盘。外汇与贵金属杠杆高,这类周期筛选只降低过拟合概率,不承诺收益。
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。 外汇和贵金属杠杆高、滑点跳空频繁,这类按小时触发的机器人实盘前务必在策略测试器用对应品种跑至少三年历史。模型导出只是起点,过滤器组合和时区对齐才是能不能跑通的关键。
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; }