数据科学和机器学习(第 29 部分):为 AI 训练目的而选择最佳外汇数据的基本技巧·进阶篇
📘

数据科学和机器学习(第 29 部分):为 AI 训练目的而选择最佳外汇数据的基本技巧·进阶篇

第 2/3 篇

◍ 用卡方筛掉日历类噪声特征

卡方检验干的事很简单:拿列联表比一比「实际出现次数」和「假设无关时的预期次数」,看两个类别变量是否真有关联。它只吃类别变量,连续数值得先离散化,否则跑出来没意义。 做日线级别行情特征工程时,常见的类别输入有 DAYOFMONTH、DAYOFWEEK、DAYOFYEAR、MONTH。要让卡方有用,得先造一个二分类目标——比如当根日线收盘大于开盘就标 1,否则标 0,这样才能衡量「某天属性」和「涨否」是否显著挂钩。 下面这段 sklearn 流程直接把四个日历特征丢进 SelectKBest(chi2),k='all' 表示全保留只看分数。逐行看:先 import chi2 与 SelectKBest;循环打 target 标签;X 只装四个类别列;fit 后取 scores_ 打印。 跑出来的分数很说明问题:DAYOFYEAR 的 Chi2 分数 14.618,而 DAYOFWEEK 只有 0.047、MONTH 0.490、DAYOFMONTH 0.623。日线天然绑定「年内的第几天」,所以 DAYOFYEAR 对目标的影响倾向最强,做特征筛选时它该留,其余几个大概率只是噪声。外汇与贵金属波动受多重因素驱动,这类统计关联仅作特征参考,实盘仍属高风险。

MQL5 / C++
from sklearn.feature_selection class="kw">import chi2
from sklearn.feature_selection class="kw">import SelectKBest
target = []
# Loop through each row in the DataFrame to create the target variable
for i in range(len(df)):
    if df.loc[i, &class="macro">#x27;TARGET_CLOSE&class="macro">#x27;] > df.loc[i, &class="macro">#x27;TARGET_OPEN&class="macro">#x27;]:
        target.append(class="num">1)
    else:
        target.append(class="num">0)
        
X = pd.DataFrame({
    &class="macro">#x27;DAYOFMONTH&class="macro">#x27;: df[&class="macro">#x27;DAYOFMONTH&class="macro">#x27;],
    &class="macro">#x27;DAYOFWEEK&class="macro">#x27;: df[&class="macro">#x27;DAYOFWEEK&class="macro">#x27;],
    &class="macro">#x27;DAYOFYEAR&class="macro">#x27;: df[&class="macro">#x27;DAYOFYEAR&class="macro">#x27;],
    &class="macro">#x27;MONTH&class="macro">#x27;: df[&class="macro">#x27;MONTH&class="macro">#x27;]
})
chi2_selector = SelectKBest(chi2, k=&class="macro">#x27;all&class="macro">#x27;)
chi2_selector.fit(X, target)
chi2_scores = chi2_selector.scores_
# Output scores for each feature
feature_scores = pd.DataFrame({&class="macro">#x27;Feature&class="macro">#x27;: X.columns, &class="macro">#x27;Chi2 Score&class="macro">#x27;: chi2_scores})
print(feature_scores)
        Feature   Chi2 Score
class="num">0   DAYOFMONTH    class="num">0.622628
class="num">1    DAYOFWEEK    class="num">0.047481
class="num">2    DAYOFYEAR   class="num">14.618057
class="num">3        MONTH    class="num">0.489713

「用方差分析给行情特征排座次」

做特征选择时,若目标变量是类别型(比如「后市涨/跌」二分类),ANOVA 能一次性比较三组及以上特征的均值差异,看哪些连续特征跟分类结果真有统计关联。它算的是组间方差除以组内方差得到的 F 统计量:F 越大,说明不同类别下该特征的均值拉得越开,关系越可能强。 具体跑法是用 Scikit-learn 的 f_classif 做单变量检验,再借 SelectKBest 按分数挑前 K 个。下面这段是直接对行情数据集做的全量 ANOVA,顺手把 F 值和 p 值都打出来。 [CODE] from sklearn.feature_selection import f_classif # We start by dropping the categorical variables in the dataset X = df.drop(columns=[ "DAYOFMONTH", "DAYOFWEEK", "DAYOFYEAR", "MONTH", "TARGET_CLOSE", "TARGET_OPEN" ]) # Perform ANOVA test selector = SelectKBest(score_func=f_classif, k='all') selector.fit(X, target) # Get the F-scores and p-values anova_scores = selector.scores_ anova_pvalues = selector.pvalues_ # Create a DataFrame to display results anova_results = pd.DataFrame({'Feature': X.columns, 'F-Score': anova_scores, 'p-Value': anova_pvalues}) print(anova_results) Feature F-Score p-Value

  • OPEN 3.483736 0.062268
  • HIGH 3.627995 0.057103
  • LOW 3.400320 0.065480
  • CLOSE 3.666813 0.055792
  • OPEN_LAG1 3.160177 0.075759
  • HIGH_LAG1 3.363306 0.066962
  • LOW_LAG1 3.309483 0.069181
  • CLOSE_LAG1 3.529789 0.060567
  • OPEN_LAG2 3.015757 0.082767
  • HIGH_LAG2 3.034694 0.081810
  • LOW_LAG2 3.259887 0.071295
  • CLOSE_LAG2 3.206956 0.073629
  • OPEN_LAG3 3.236211 0.072329
  • HIGH_LAG3 3.022234 0.082439
  • LOW_LAG3 3.020219 0.082541
  • CLOSE_LAG3 3.075698 0.079777
  • 30DAY_SMA 2.665990 0.102829
  • 7DAY_STDDEV 0.639071 0.424238
  • DIFF_LAG1_OPEN 1.237127 0.266293
  • DIFF_LAG1_HIGH 0.991862 0.319529
  • DIFF_LAG1_LOW 0.131002 0.717472
  • DIFF_LAG1_CLOSE 0.198001 0.656435

selector = SelectKBest(score_func=f_classif, k=10) X_selected = selector.fit_transform(X, target) # print the selected feature names selected_features = X.columns[selector.get_support()] print("Selected Features:", selected_features) Selected Features: Index(['OPEN', 'HIGH', 'LOW', 'CLOSE', 'HIGH_LAG1', 'LOW_LAG1', 'CLOSE_LAG1', 'LOW_LAG2', 'CLOSE_LAG2', 'OPEN_LAG3'], dtype='object') [/CODE] 逐行看这段代码:先 drop 掉日历类和目标类字段,只留数值特征进 X;SelectKBest 配 f_classif、k='all' 是跑全量不裁剪,fit 后从 scores_ 和 pvalues_ 取数;最后换 k=10 重新 fit_transform,get_support 掩码挑出分数最高的十个特征。 从打印结果能读出硬数据:CLOSE 的 F 值 3.667 为全场最高,p 值 0.0558 最接近 0.05 显著线;而 DIFF_LAG1_LOW 的 F 仅 0.131、p 值 0.717,基本可判定与原分类目标无关。所有特征 p 值均大于 0.05,说明在样本内它们对分类的贡献都只是「边缘显著」,外汇与贵金属市场本身高风险,这种统计微弱关联拿去实盘前务必扩大样本重测。 挑完前十特征后,后续建模就只用 OPEN/HIGH/LOW/CLOSE 及部分延迟项,省掉 7DAY_STDDEV、DIFF_LAG1_* 这类噪声。下一层包装器思路会换种玩法来评估子集。

MQL5 / C++
from sklearn.feature_selection class="kw">import f_classif
# We start by dropping the categorical variables in the dataset
X = df.drop(columns=[
    "DAYOFMONTH",
    "DAYOFWEEK",
    "DAYOFYEAR",
    "MONTH",
    "TARGET_CLOSE",
    "TARGET_OPEN"
])
# Perform ANOVA test
selector = SelectKBest(score_func=f_classif, k=&class="macro">#x27;all&class="macro">#x27;)
selector.fit(X, target)
# Get the F-scores and p-values
anova_scores = selector.scores_
anova_pvalues = selector.pvalues_
# Create a DataFrame to display results
anova_results = pd.DataFrame({&class="macro">#x27;Feature&class="macro">#x27;: X.columns, &class="macro">#x27;F-Score&class="macro">#x27;: anova_scores, &class="macro">#x27;p-Value&class="macro">#x27;: anova_pvalues})
print(anova_results)
                    Feature   F-Score   p-Value
class="num">0                    OPEN   class="num">3.483736   class="num">0.062268
class="num">1                    HIGH   class="num">3.627995   class="num">0.057103
class="num">2                     LOW   class="num">3.400320   class="num">0.065480
class="num">3                   CLOSE   class="num">3.666813   class="num">0.055792
class="num">4                OPEN_LAG1   class="num">3.160177   class="num">0.075759
class="num">5                HIGH_LAG1   class="num">3.363306   class="num">0.066962
class="num">6                 LOW_LAG1   class="num">3.309483   class="num">0.069181
class="num">7               CLOSE_LAG1   class="num">3.529789   class="num">0.060567
class="num">8                OPEN_LAG2   class="num">3.015757   class="num">0.082767
class="num">9                HIGH_LAG2   class="num">3.034694   class="num">0.081810
class="num">10                LOW_LAG2   class="num">3.259887   class="num">0.071295
class="num">11            CLOSE_LAG2   class="num">3.206956   class="num">0.073629
class="num">12               OPEN_LAG3   class="num">3.236211   class="num">0.072329
class="num">13               HIGH_LAG3   class="num">3.022234   class="num">0.082439
class="num">14                LOW_LAG3   class="num">3.020219   class="num">0.082541
class="num">15            CLOSE_LAG3   class="num">3.075698   class="num">0.079777
class="num">16             30DAY_SMA   class="num">2.665990   class="num">0.102829
class="num">17            7DAY_STDDEV   class="num">0.639071   class="num">0.424238
class="num">18        DIFF_LAG1_OPEN   class="num">1.237127   class="num">0.266293
class="num">19        DIFF_LAG1_HIGH   class="num">0.991862   class="num">0.319529
class="num">20          DIFF_LAG1_LOW   class="num">0.131002   class="num">0.717472
class="num">21       DIFF_LAG1_CLOSE   class="num">0.198001   class="num">0.656435
selector = SelectKBest(score_func=f_classif, k=class="num">10)
X_selected = selector.fit_transform(X, target)
# print the selected feature names
selected_features = X.columns[selector.get_support()]
print("Selected Features:", selected_features)
Selected Features: Index([&class="macro">#x27;OPEN&class="macro">#x27;, &class="macro">#x27;HIGH&class="macro">#x27;, &class="macro">#x27;LOW&class="macro">#x27;, &class="macro">#x27;CLOSE&class="macro">#x27;, &class="macro">#x27;HIGH_LAG1&class="macro">#x27;, &class="macro">#x27;LOW_LAG1&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG1&class="macro">#x27;,
       &class="macro">#x27;LOW_LAG2&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG2&class="macro">#x27;, &class="macro">#x27;OPEN_LAG3&class="macro">#x27;],
      dtype=&class="macro">#x27;object&class="macro">#x27;)

用递归淘汰给特征集瘦身

递归特征消除(RFE)的核心动作很直接:先拿全部特征训一个能排重要度的模型,再每轮踢掉最不重要的那批,直到剩下你指定的数量。上面这段脚本用逻辑回归当底座,硬挑出 10 个特征,从 25 个原始列里筛完,OPEN/HIGH/LOW/CLOSE 及部分 LAG1~LAG3 留了下来,而 30DAY_SMA、7DAY_STDDEV、日历类字段全被标成 False。 RFE 不挑模型,只要你能从模型里拿到特征重要性排名就能挂上用。砍掉噪声列后,过拟合概率会降,样本外表现倾向更稳。但它是贪心算法,未必能掏出全局最优子集;遇到神经网络或大样本,反复重训的算力账单会很难看。 实盘拿去跑外汇或贵金属信号前,先认清这类筛选只是降维手段,不是盈利保证。杠杆品种波动剧烈,特征再干净也挡不住黑天鹅,验证时务必用walk-forward切分。 下面这段代码就是可直接丢进 notebook 跑的瘦身流程,改 n_features_to_select 就能换筛选强度:

MQL5 / C++
from sklearn.feature_selection class="kw">import RFE
from sklearn.linear_model class="kw">import LogisticRegression
# Prepare the target variable, again
y = []
# Loop through each row in the DataFrame to create the target variable
for i in range(len(df)):
    if df.loc[i, &class="macro">#x27;TARGET_CLOSE&class="macro">#x27;] > df.loc[i, &class="macro">#x27;TARGET_OPEN&class="macro">#x27;]:
        y.append(class="num">1)
    else:
        y.append(class="num">0)
# Drop future variables from the feature set
X = df.drop(columns=["TARGET_CLOSE", "TARGET_OPEN"])
# Initialize the model
model = LogisticRegression(max_iter=class="num">10000)
# Initialize RFE with the model and number of features to select
rfe = RFE(estimator=model, n_features_to_select=class="num">10)
# Fit RFE
rfe.fit(X, y)
selected_features_mask = rfe.support_
# Getting the names of the selected features
feature_names = X.columns
selected_feature_names = feature_names[selected_features_mask]
selected_features = pd.DataFrame({
    "Name": feature_names,
    "Mask": selected_features_mask
})
selected_features.head(-class="num">1)
# Filter the dataset to keep only the selected features
X_selected = X.loc[:, selected_features_mask]
class="macro">#for better readability, we convert this into pandas dataframe
X_selected_df = pd.DataFrame(X_selected, columns=selected_feature_names)
print("Selected Features")
X_selected_df.head()

◍ 逐步筛特征的前向与后向路径

包装式特征选择里,顺序法靠对模型贡献的增减来搭特征集,分前向与后向两条路。前向从空集起手,逐个塞特征,直到凑够数量或再加也不提升性能;后向反过来,全量开局,每次踢掉最不重要的那个,留到指定数目为止。 两者手法相反,却往往收敛到相同解、产出同等规模特征子集。好处是逻辑直白、能和任意机器学习算法搭档,挑出最相关特征后模型表现可能更干净。 短板也实在:特征多或样本大时跑得慢,且只看局部改进,未必摸得到全局最优集。外汇与贵金属行情噪声大,用这类筛选做信号模型须认清过拟合风险偏高。 下面这段 Python 演示了用顺序选择器在 10 个特征上做前向与后向,输出索引与名称差异明显——前向选了 [1,7,8,12,17,19,22,23,24,25],后向选了 [2,3,7,10,11,12,13,14,15,16],重合仅 7、12 附近少数滞后项。

MQL5 / C++
from sklearn.feature_selection class="kw">import SequentialFeatureSelector
# Create a logistic regression model
model = LogisticRegression(max_iter=class="num">10000)
# Create a SequentialFeatureSelector object
sfs = SequentialFeatureSelector(model, n_features_to_select=class="num">10, direction=&class="macro">#x27;forward&class="macro">#x27;)
# Fit the SFS object to the training data
sfs.fit(X, target)
# Get the selected feature indices
selected_features = sfs.get_support(indices=True)
selected_features_names = X.columns[selected_features] # get the feature names
# Print the selected features
print("Selected feature indices:", selected_features)
print("Selected feature names:", selected_feature_names)
Selected feature indices: [ class="num">1  class="num">7  class="num">8 class="num">12 class="num">17 class="num">19 class="num">22 class="num">23 class="num">24 class="num">25]
Selected feature names: Index([&class="macro">#x27;OPEN&class="macro">#x27;, &class="macro">#x27;HIGH&class="macro">#x27;, &class="macro">#x27;LOW&class="macro">#x27;, &class="macro">#x27;CLOSE&class="macro">#x27;, &class="macro">#x27;HIGH_LAG1&class="macro">#x27;, &class="macro">#x27;LOW_LAG1&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG1&class="macro">#x27;,
       &class="macro">#x27;LOW_LAG2&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG2&class="macro">#x27;, &class="macro">#x27;OPEN_LAG3&class="macro">#x27;],
      dtype=&class="macro">#x27;object&class="macro">#x27;)
# Create a logistic regression model
model = LogisticRegression(max_iter=class="num">10000)
# Create a SequentialFeatureSelector object
sfs = SequentialFeatureSelector(model, n_features_to_select=class="num">10, direction=&class="macro">#x27;backward&class="macro">#x27;)
# Fit the SFS object to the training data
sfs.fit(X, target)
# Get the selected feature indices
selected_features = sfs.get_support(indices=True)
selected_features_names = X.columns[selected_features] # get the feature names
# Print the selected features
print("Selected feature indices:", selected_features)
print("Selected feature names:", selected_feature_names)
Selected feature indices: [ class="num">2  class="num">3  class="num">7 class="num">10 class="num">11 class="num">12 class="num">13 class="num">14 class="num">15 class="num">16]
Selected feature names: Index([&class="macro">#x27;OPEN&class="macro">#x27;, &class="macro">#x27;HIGH&class="macro">#x27;, &class="macro">#x27;LOW&class="macro">#x27;, &class="macro">#x27;CLOSE&class="macro">#x27;, &class="macro">#x27;HIGH_LAG1&class="macro">#x27;, &class="macro">#x27;LOW_LAG1&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG1&class="macro">#x27;,
       &class="macro">#x27;LOW_LAG2&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG2&class="macro">#x27;, &class="macro">#x27;OPEN_LAG3&class="macro">#x27;],
      dtype=&class="macro">#x27;object&class="macro">#x27;)

「训练里顺手挑特征」

嵌入式方法把特征选择塞进模型训练过程里一起跑,而不是先筛完再训。典型流程就三步:先训机器学习模型,再从中导出特征重要性,最后只保留排名靠前的预测变量。 最常见的两类是 Lasso 回归和基于决策树的方法。Lasso 靠 L1 惩罚把不重要变量的系数直接压到 0,决策树类则按分裂增益给出特征贡献度,两者都能在 MT5 的 Python 环境或外接训练脚本里直接验证。 想落地的话,先用历史 tick 或分钟 Bar 拉一组贵金属波动特征,跑一次带 L1 的回归,看几个系数归零,就能判断哪些因子在当下样本里纯属噪声。外汇与贵金属杠杆高、滑点突变频繁,归零结论只代表回测窗口内的概率倾向,换周期可能反转。

常见问题

用卡方检验算特征与目标变量的独立性,p 值高于 0.05 大概率是无用噪声,直接从训练集剔除。
跑一遍方差分析(ANOVA),按 F 值从高到低排,F 值低且差异不显著的可以优先砍掉。
小布可在对应品种页自动跑递归淘汰和前向/后向路径,把筛好的特征集和训练建议直接推给你。
每轮训练后删掉权重最低的几个特征,再用剩余特征重训,直到验证集误差不再明显下降为止。
小样本更稳的是后向淘汰,从全特征往下减,避免前向早期漏掉组合效应;外汇高风险,结论仅作概率参考。