利用 CatBoost 算法寻找外汇市场的季节性模式·进阶篇
◍ 逐小时摸一遍模型底牌
手动去试每个交易小时的条件组合太磨人,写个函数把各小时的汇总统计一次性跑出来更实际。函数会扫完你指定的小时列表,跑完吐一张图,横轴是小时序号,纵轴是每次再训练的 R^2 分数,哪段时间点堆得密、位置高,哪段模型就相对更靠谱。 为了不让采样干扰判断,我把开仓的最小和最大水平都锁在 15,等于关掉采样;再训练轮数 iterations 设成 10,也就是每个小时独立重训 10 次取分布。轮数调大,统计会更稳,但耗时明显上升,MT5 外接 Python 跑的时候得有点耐心。 从跑出来的图看,4、5、6 点那几个小时的点簇离得近、整体偏高,说明这几小时摸到的模式质量更可信。反观 9 到 15 点,点散得很开,平均质量掉到 0.6 左右。外汇与贵金属属高风险品种,这种时段差异只是概率倾向,不是硬规律。 挑出表现好的小时,重训模型丢进自定义测试器看结果就行。下面这段是 exploratory_analysis 的原样代码,黄底部分是小时列表、采样锁定和迭代次数的关键行:
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 做贝叶斯聚类,仅作思路验证,未纳入主流程。
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,样本内拟合确实随星期浮动。
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 上方,就能判断你那套时段划分有没有过拟合。
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