深度神经网络 (第 II 部)。制定和选择预测因子·综合运用
(3/3)· 特征怎么造、因子怎么挑,这篇把前两部的铺垫收口成可跑的流程
- 用 MDLP 把连续特征切成离散档位
- 打印变量看通道状态机
- 离散化切分在训练集与测试集上的落地
- 离散化后的特征矩阵长什么样
- 特征矩阵的前几行长什么样
- 特征矩阵的前几行长什么样
- 用频率表看训练集里的状态分布
- 用贝叶斯图分数据集核验特征区分度
- 编译期残留的空壳收尾
- 把时间标签拆成可喂模型的因子
- 用 lubridate 拆出星期与小时维度
- 各周期样本量分布实况
- 挑因子的两套比对思路
- 用 smbinning 挑出真正能打的预测因子
- 特征变换与空间符号归一的执行链
- 离散化后训练测试验证集的切分落地
- 用信息值筛掉弱特征
- 用分箱切点把连续因子转成离散档位
- 用贝叶斯变分挑出五个主力预测因子
- 贝叶斯变量筛选跑完后的参数读图
- 验证集与测试集的混淆矩阵实况
- 分类器混淆矩阵外的那串统计量
- 让神经网络自己挑预测因子
- 剪枝后留下的六个特征
- 脚本落地的几处坑
用 MDLP 把连续特征切成离散档位
做价格行为建模时,连续指标直接喂分类器往往噪声太大。先把训练集按最小描述长度原则做离散化,能让后续决策边界更干净。 下面这段 R 管线先剔除零方差和近零方差列,再按 2000/1000/1000 切出训练、验证、测试三份,最后对训练集跑 mdlp 离散化。 env$mdlp.train %>% str() 的实际返回里,cutp 是一个长度为 12 的列表,对应 12 个特征的分切点;例如第一个特征切在 -0.0534 与 0.0278,第五个特征切在三处 -0.0519 / -0.0055 / 0.019。Disc.data 是 2000 行 13 列的整数档位框,ftlm 列取值 1~3,说明该特征被分成了最多三档。 这种切法不是拍脑袋分箱,而是依数据熵压缩得来;在 MT5 外接 R 做特征工程时,可直接把 cutp 拿去写 MT5 的离散化判断,省掉手动调阈值的随意性。外汇与贵金属波动剧烈,离散化只是降噪手段,信号失效风险仍在,需用小布回测交叉验证。
DTn$test %>% ggvis(~v.fatl, fill = ~Class) %>% group_by(Class) %>% layer_densities() %>% add_legend("fill", title = "DTn$test$v.fatl"), env) require(discretization) require(caret) require(pipeR) evalq( { dataSet %>% preProcess(., method = c("zv", "nzv", "conditionalX")) %>% predict(., dataSet) %>% na.omit -> dataSetClean train = class="num">1:class="num">2000 val = class="num">2001:class="num">3000 test = class="num">3001:class="num">4000 DT <- list() list(train = dataSetClean[train, ], val = dataSetClean[val, ], test = dataSetClean[test, ]) -> DT }, env) evalq( pipeline({ DT$train select(-Data) as.data.frame() mdlp()}) -> mdlp.train, envir = env)
「打印变量看通道状态机」
在 MT5 的策略调试里,把 rbci、pcci 以及 v.fatl 等系列变量用 ArrayPrint 或 Comment 打出来,能直接看到各通道指标在当前巴里的离散状态。上面这组样本来自欧盘早段 EURUSD 的 M15 调试输出,索引 1:2000 表示数组长度上限 2000,前 10 个值就是最近若干根的计算结果。 rbci 前 10 值多为 2、后段转 1,说明快速通道从偏多倾向走弱;v.fatl 出现 4、3、1 的递减,是趋势强度衰减的典型痕迹。v.rstl 全程为 1,代表慢速通道在该段几乎无方向变化。 这种打印方式不依赖图表肉眼读线,适合用「小布盯盘」的 AIGC 模块批量比对多品种。外汇与贵金属波动受杠杆放大,此类状态读数只作概率参考,实盘须控仓。
◍ 离散化切分在训练集与测试集上的落地
上面那串变量打印里,v.stlm、v.rbci、v.pcci 都是长度 2000 的整型序列,取值从 1 到 4 不等;Class 是二值因子,水平为 -1 与 1,前 5 个样本全是 1(对应水平 2)。这类数据直接喂分类器噪声偏大,所以先用 MDLP 得到的 cutp 做离散区间映射。 先筛出数值型列:把 mdlp.train$cutp 逐个判断是不是 numeric,unlist 成逻辑向量 idx,再只抽取 Disc.data 里对应列作为 train.d。测试集则从 DT$test 丢掉 Data 和 Class 两列后转成普通数据框 test.d。 真正映射靠 findInterval:对每一个数值列 i,若 idx[i] 为真,就拿训练阶段算出的 cutp[[i]] 当断点,rightmost.closed=FALSE、left.open=FALSE,把测试值落进区间编号。最后整体 +1 偏移,再 cbind 回 DT$test$Class,列名对齐 train.d,验证集 val.d 走完全一样的去列流程。 外汇与贵金属行情里这类离散特征容易过拟合,实盘前建议在 MT5 用历史数据重跑 cutp 并核对区间边界;样本外命中率可能随品种波动,属正常概率现象。
evalq( { mdlp.train$cutp %>% lapply(., function(x) is.numeric(x)) %>% unlist -> idx # class="type">bool #----训练----------------- mdlp.train$Disc.data[ ,idx] -> train.d #---测试------------ DT$test %>% select(-c(Data, Class)) %>% as.data.frame() -> test.d foreach(i = class="num">1:length(idx), .combine = &class="macro">#x27;cbind&class="macro">#x27;) %do% { if (idx[i]) {findInterval(test.d[ ,i], vec = mdlp.train$cutp[[i]], rightmost.closed = FALSE, all.inside = F, left.open = F)} } %>% as.data.frame() %>% add(class="num">1) %>% cbind(., DT$test$Class) -> test.d colnames(test.d) <- colnames(train.d) #-----验证----------------- DT$val %>% select(-c(Data, Class)) %>% as.data.frame() -> val.d foreach(i = class="num">1:length(idx), .combine = &class="macro">#x27;cbind&class="macro">#x27;) %do% {
离散化后的特征矩阵长什么样
上面这段是把连续特征按 MDLP 切分点离散化的核心逻辑:对每个特征列 i,用 findInterval 把原始值 val.d[,i] 映射到 mdlp.train$cutp[[i]] 定义的区间里,rightmost.closed 设 FALSE、left.open 设 F,意味着最右区间不闭、左端不开放,避免边界样本被吞掉。 映射结果加 1 后和 Class 列 cbind,再强行把列名对齐到 train.d,验证集 val.d 就拥有了和训练集同构的离散特征表。 直接 head(env$train.d) 能看到前 4 行:ftlm、rbci、pcci 等 9 个特征已被转成 1~4 的整数区间编码,Class 列全为 1,说明这批样本同属一类。 在 MT5 里跑类似逻辑时,重点核对 cutp 向量和 R 里是否一致;区间编码差一位,后续决策树分裂点就会整体偏移。外汇与贵金属市场波动剧烈,离散化仅降低过拟合概率,不保证信号胜率。
if (idx[i]) {findInterval(val.d[ ,i], vec = mdlp.train$cutp[[i]], rightmost.closed = FALSE, all.inside = F, left.open = F)} } %>% as.data.frame() %>% add(class="num">1) %% cbind(., DT$val$Class) -> val.d colnames(val.d) <- colnames(train.d) env$train.d %>% head() ftlm rbci pcci v.fatl v.satl v.rftl v.ftlm v.stlm v.rbci Class class="num">1 class="num">3 class="num">2 class="num">2 class="num">4 class="num">1 class="num">1 class="num">2 class="num">1 class="num">4 class="num">1 class="num">2 class="num">3 class="num">2 class="num">2 class="num">4 class="num">1 class="num">2 class="num">2 class="num">1 class="num">4 class="num">1 class="num">3 class="num">3 class="num">2 class="num">1 class="num">3 class="num">1 class="num">2 class="num">1 class="num">1 class="num">3 class="num">1 class="num">4 class="num">3 class="num">2 class="num">2 class="num">4 class="num">2 class="num">2 class="num">1 class="num">2 class="num">3 class="num">1
「特征矩阵的前几行长什么样」
把环境里的测试集用 head() 拉出来,能看到每条样本由 ftlm、rbci、pcci、v.fatl、v.satl、v.rftl、v.ftlm、v.stlm、v.rbci 这 9 个特征加一个 Class 标签组成。前 4 行样本里,Class 取值出现了 -1、-1、-1、1 两种状态,说明数据并非单边分布。 第一行特征向量为 1,1,1,1,2,1,1,1,1,对应 Class -1;第四行变成 2,1,2,3,1,1,2,2,3,Class 转为 1。这种从 -1 到 1 的切换,往往意味着趋势类指标组合发生了状态翻转,在 MT5 里可用 iCustom 抓同样序列做比对。 外汇与贵金属属高风险品种,这类特征切片只反映历史样本结构,不预示后续涨跌概率。打开 R 或 MT5 自写脚本复现这 9 列,是验证信号逻辑的第一步。
◍ 特征矩阵的前几行长什么样
把 env$val.d 这个数据框用 head() 拉出来,能看到每行是一组离散化后的指标状态,最后一列 Class 是样本归类标签。下面前 4 行里,ftlm、rbci、pcci、v.fatl、v.satl、v.rftl、v.ftlm、v.stlm、v.rbci 九列取值集中在 1~4,Class 恒为 1,说明这批头部样本被归到了同一类。 实际在 R 里跑一句 env$val.d %>% head() 就能复现:第 1 行九列依次是 2 2 2 2 2 2 2 1 2,第 4 行变成了 2 2 2 2 4 2 2 2 2 3,可见 v.satl 从 2 跳到 4、v.rbci 从 2 跳到 3,但 Class 仍是 1。 这种离散特征表是后续做状态匹配的输入。外汇与贵金属市场波动剧烈、杠杆风险高,同类状态在历史中重复出现只代表概率倾向,不等于后市必沿原方向走,拿去回测时务必用 MT5 实跑验证。
用频率表看训练集里的状态分布
把 fast trend line 的派生字段 v.fatl 单独拉出来数一遍,能直接看到样本在四个状态里的拥挤程度。训练集里 1 到 4 类分别有 211、693、519、577 个样本,状态 2 占了将近四成,说明模型平时碰到的多数是这个区间的行情。 测试集和验证集的比例基本对齐:测试集是 49 / 376 / 313 / 262,验证集是 68 / 379 / 295 / 258。这种分布一致性在外币对或黄金的小时线建模里很重要,若某子集状态 2 突然掉到一百以下,大概率是切片时间窗选歪了。 光看总数不够,得把 v.fatl 和 ftlm、v.satl 一起丢进 cross_plot,关掉自动分箱(auto_binning = F)才能对照原始档位。再用 bayesian_plot 单独验 v.fatl 对 Class 的条件概率,extra_above 和 extra_under 都设 5,能把首尾档的稀有样本也摊开。外汇与贵金属杠杆高,这类统计仅用于辅助判别,实盘仍可能因跳空而失效。
env$train.d$v.fatl %>% table() # class="num">1 class="num">2 class="num">3 class="num">4 # class="num">211 class="num">693 class="num">519 class="num">577 env$test.d$v.fatl %>% table() # class="num">1 class="num">2 class="num">3 class="num">4 # class="num">49 class="num">376 class="num">313 class="num">262 env$val.d$v.fatl %>% table() # class="num">1 class="num">2 class="num">3 class="num">4 # class="num">68 class="num">379 class="num">295 class="num">258 evalq( cross_plot(data = train.d, str_input = c("v.fatl", "ftlm", "v.satl"), str_target = "Class", auto_binning = F, plot_type = "both"), env ) evalq( cross_plot( DT$train %>% select(-Data) %>% select(c(v.satl, ftlm, v.fatl, stlm, v.rstl, Class)) %>% as.data.frame(), str_input = Cs(v.satl, ftlm, v.fatl, stlm, v.rstl), str_target = "Class", auto_binning = T, plot_type = "both"), env ) #------BayesTrain------------------- evalq( { bayesian_plot(train.d, input = "v.fatl", target = "Class", title = "Bayesian comparison train$v.fatl/Class", plot_all = F, extra_above = class="num">5, extra_under = class="num">5) },env ) evalq( { bayesian_plot(train.d, input = "ftlm", target = "Class", title = "Bayesian comparison train$ftlm/Class", plot_all = F, extra_above = class="num">5,
「用贝叶斯图分数据集核验特征区分度」
把训练集、测试集、验证集分别跑贝叶斯对比图,是看 v.fatl、ftlm、v.satl 这三个特征对 Class 标签到底有没有判别力的直接办法。每段 evalq 都是在指定环境里调用 bayesian_plot,input 换特征名、target 固定为 Class,plot_all 设 F 只画关键区间。 extra_above 和 extra_under 都取 5,意味着在分位边界上下各多展 5 个样本点,避免尾部被压缩看不清。train.d 上先扫 v.fatl 与 v.satl,test.d 与 val.d 上再补 ftlm,三集全覆盖才能判断过拟合倾向。 外汇与贵金属行情具有高杠杆高风险,这类统计区分度只说明历史样本中的概率倾向,实盘信号可能失效,开 MT5 接自己的 tick 数据重跑才知本地品种是否吃这套。
evalq( { bayesian_plot(train.d, input = "v.fatl", target = "Class", title = "Bayesian comparison train$v.fatl/Class", plot_all = F, extra_above = class="num">5, extra_under = class="num">5) },env ) evalq( { bayesian_plot(train.d, input = "v.satl", target = "Class", title = "Bayesian comparison train$v.satl/Class", plot_all = F, extra_above = class="num">5, extra_under = class="num">5) },env ) #------------BayesTest------------------------ evalq( { bayesian_plot(test.d, input = "v.fatl", target = "Class", title = "Bayesian comparison test$v.fatl/Class", plot_all = F, extra_above = class="num">5, extra_under = class="num">5) },env ) evalq( { bayesian_plot(test.d, input = "ftlm", target = "Class", title = "Bayesian comparison test$ftlm/Class", plot_all = F, extra_above = class="num">5, extra_under = class="num">5) },env ) evalq( { bayesian_plot(test.d, input = "v.satl", target = "Class", title = "Bayesian comparison test$v.satl/Class", plot_all = F, extra_above = class="num">5, extra_under = class="num">5) },env ) #-------------BayesVal--------------------------------- evalq( { bayesian_plot(val.d, input = "v.fatl", target = "Class", title = "Bayesian comparison val$v.fatl/Class", plot_all = F, extra_above = class="num">5, extra_under = class="num">5) },env ) evalq( { bayesian_plot(val.d, input = "ftlm", target = "Class", title = "Bayesian comparison val$ftlm/Class", plot_all = F, extra_above = class="num">5, extra_under = class="num">5) },env ) evalq( { bayesian_plot(val.d, input = "v.satl", target = "Class", title = "Bayesian comparison val$v.satl/Class", plot_all = F, extra_above = class="num">5, extra_under = class="num">5) },env )
◍ 编译期残留的空壳收尾
这段 MQL5 片段出现在指标源码的尾部,实际只暴露了一个右大括号与 env 参数闭合,紧跟一行 #------------------------------------------ 注释分隔线,没有任何可执行逻辑。
从结构看,它应是前一个代码块(如 OnCalculate 或自定义环境初始化)的收口,编译时不会报错,但也不参与任何运行时计算。
在 MT5 里打开对应 mq5 文件,搜索 #------------------------------------------ 能快速定位这类分段边界;若你复制源码时漏掉前面左括号或 env 定义,编译器会报 'unexpected end of file' 类错误。外汇与贵金属品种波动剧烈,这类结构残缺的代码直接挂图表可能引发指标不刷新,需先本地编译验证。
}, env ) #------------------------------------------
把时间标签拆成可喂模型的因子
做特征工程时,最容易被忽略的就是时间戳本身。一条日线或小时线背后藏着星期几、月内第几天、属于哪个交易时段等信息,这些对贵金属和外汇走势常有隐性影响——比如周一亚洲时段的流动性和周四欧盘往往不是一个量级,直接拿原始 datetime 当输入等于浪费了大部分结构。 派生思路不复杂:从已有列做变换(取对数、比值、n次幂),或把分类变量 dummy 化。更实用的做法是把时间索引展开成多列签名特征。下面这套 R 端处理(timekit 包)虽不在 MT5 里跑,但逻辑可以平移到 EA 的预处理:用 tk_augment_timeseries_signature() 把索引拆出 mday、wday.lbl、hour 等,再按星期分组嵌套。 实操里我们拿初始数据集 pr 增强后,得到 8000 行 × 33 列 的 data.frame,说明时间展开能凭空多出二十几个候选特征。其中 wday.lbl 把周日标 1、周六标 7,删掉 NA 和周六数据后,就能分组建模看各星期因子的分布差异。 别把正态当圣经 时间特征里 week.iso 和 week(自周日起)算出来会差一周,喂给模型前先确认你用的是哪种周定义,否则跨年样本会错位。 让小布替你跑这套 在 MT5 里可以用 iTime 取柱时间,自己写个按 wday 分组的统计量,验证黄金在周三、周四美盘波动是否确实偏高,外汇品种同理但风险更高,样本外可能失效。
evalq( { tk_augment_timeseries_signature(pr) %>% select(c(mday, wday.lbl, hour)) %>% cbind(pr, .) -> pr.augm pr.compl <- pr.augm[complete.cases(pr.augm), ] pr.nest <- pr.compl %>% group_by(wday.lbl) %>% nest() }, env) > str(env$pr.augm) &class="macro">#x27;data.frame&class="macro">#x27;: class="num">8000 obs. of class="num">33 variables: $ Data : POSIXct, format: "class="num">2017-class="num">01-class="num">10 class="num">11:class="num">00:class="num">00" ... $ Open : num class="num">123 class="num">123 class="num">123 class="num">123 class="num">123 ... $ High : num class="num">123 class="num">123 class="num">123 class="num">123 class="num">123 ... $ Low : num class="num">123 class="num">123 class="num">123 class="num">123 class="num">123 ... $ Close : num class="num">123 class="num">123 class="num">123 class="num">123 class="num">123 ... $ Vol : num class="num">3830 class="num">3360 class="num">3220 class="num">3241 class="num">3071 ... .................................................. $ zigz : num class="num">123 class="num">123 class="num">123 class="num">123 class="num">123 ... $ dz : num NA -class="num">0.0162 -class="num">0.0162 -class="num">0.0162 -class="num">0.0162 ...
「用 lubridate 拆出星期与小时维度」
把 MT5 导出的分时数据框 pr 接上 lubridate,能快速把时间戳拆成可分组因子。下面这段在 R 环境里给每一行补了星期简称、日期和小时,并顺手剔掉周六,存进 pr1。
require(lubridate)
evalq({pr %>% mutate(.,
wday = wday(Data), label = TRUE, abbr = TRUE),
day = day(Data),
hour = hour(Data)) %>%
filter(wday != "Sat") -> pr1
pr1.nest <- pr1 %>% na.omit %>%
group_by(wday) %>% nest()},
env
)
跑完 str(env$pr1) 会看到 7924 行、33 列的数据框,时间从 2017-01-10 11:00:00 起,OHLC 与成交量(Vol 首行 3830、次行 3360)都在。sig 列前 10 个非缺失值全是 -1,说明样本开头处于一致的空头信号状态。
按 wday 嵌套后变成 5 行的 tibble(周日、周六已剥离),每组里藏着完整子数据框。你可以直接开 MT5 导出 EURUSD 的 M15 历史,用同样管道验证自己数据的星期分布是否也偏周中集中。外汇与贵金属杠杆高,信号聚合仅描述历史概率,不预示后续方向。
require(lubridate) evalq({pr %>% mutate(., wday = wday(Data), class="macro">#label = TRUE, abbr = TRUE), day = day(Data), hour = hour(Data)) %>% filter(wday != "Sat") -> pr1 pr1.nest <- pr1 %>% na.omit %>% group_by(wday) %>% nest()}, env )
◍ 各周期样本量分布实况
把五个交易周期按编号排开,样本行数落在 1448 到 1632 之间,列宽统一为 32 维特征。 具体看:周期 2 仅 1448 行,周期 3 为 1536 行,周期 1 有 1593 行,周期 6 为 1624 行,周期 5 最多 1632 行。 这种非均衡分布意味着在 MT5 里做跨周期模型训练时,小样本周期(如编号 2)的泛化误差可能偏高,建议先按行数加权或做窗口补齐再喂给特征工程。
<class="type">int> <list> class="num">1 class="num">4 <tibble [class="num">1,class="num">593 x class="num">32]> class="num">2 class="num">5 <tibble [class="num">1,class="num">632 x class="num">32]> class="num">3 class="num">6 <tibble [class="num">1,class="num">624 x class="num">32]> class="num">4 class="num">2 <tibble [class="num">1,class="num">448 x class="num">32]> class="num">5 class="num">3 <tibble [class="num">1,class="num">536 x class="num">32]>
挑因子的两套比对思路
评估预测因子重要性有一堆现成方法,前文已拆过部分。本文既然主打可视化,就只拿一个可视方案和一个分析方案做对照,看哪种更能把因子权重拉开。 实操上,可视方案适合快速扫一眼哪些因子在样本里区分度明显;分析方案则能给每个因子一个量化评分,方便后续按阈值剔除。两者结合,比单用一种更不容易漏掉边缘有效因子。 外汇与贵金属市场高波动、高杠杆,因子重要性随 regime 切换会漂移,任何筛选结果都只是历史样本下的概率倾向,开 MT5 用自己品种复算一遍才作数。
「用 smbinning 挑出真正能打的预测因子」
做特征筛选时,可视化关系虽直观,但面对对数、正弦、tanh 和常规化等多套变换数据,得靠 smbinning 包把每个集合里的相关预测因子揪出来。目标变量必须是 0/1 数值型,且变量名里不能带点号,这两点在写变换函数时要先处理掉。 跑完重要性评估后,五个因子 v_fatl、ftlm、v_satl、rbci、v_rbci 在全部集合里都排前列,只是顺序有浮动;pcci、v_ftlm、v_stlm、v_rftl 强度居中;v_pcci 和 stlm 偏弱。按 IV>0.1 掐线,最后三个弱变量可直接丢,只留五个最强加四个平均。 以 v_fatl 为例,smbinning 给出的 res 对象里含有最佳分箱切点,本例切成 4 个区间,把变量范围和 Cl 目标变量的连接画出来就能肉眼看边界。ftlm 同理,切点不同但思路一致。 值得留意的是,mdlp::discretization 和 smbinning::smbinning 认定的重要变量重合,但分割区间不一样。实盘建模前,两种离散化方案都得在 MT5 外接 R 环境里回测一遍,看哪个变体对汇率或贵金属方向的预测概率更稳。外汇与贵金属杠杆高,信号再强也只是倾向而非确定性。
#----清理--------------------- require(caret) require(pipeR) evalq( { train = class="num">1:class="num">2000 val = class="num">2001:class="num">3000 test = class="num">3001:class="num">4000 DT <- list() dataSet %>% preProcess(., method = c("zv", "nzv", "conditionalX")) %>% predict(., dataSet) %>% na.omit -> dataSetClean list(train = dataSetClean[train, ], val = dataSetClean[val, ], test = dataSetClean[test, ]) -> DT rm(dataSetClean, train, val, test) }, env) #------异常值------------- evalq({ # 定义结果的新列表 DTcap <- list() # 遍历三个套合 foreach(i = class="num">1:class="num">3) %do% { DT[[i]] %>% # 删除 (数据, 类) 列 select(-c(Data, Class)) %>% # 转换成 data.frame 并存储在临时变量 x 中 as.data.frame() -> x if (i == class="num">1) { # 定义第一个输入中异常值的参数 foreach(i = class="num">1:ncol(x), .combine = "cbind") %do% { prep.outlier(x[ ,i]) %>% unlist() } -> pre.outl colnames(pre.outl) <- colnames(x) } # 将异常值替换为 class="num">5/class="num">95%, 并将结果存储在 x.cap 中 foreach(i = class="num">1:ncol(x), .combine = "cbind") %do% { stopifnot(exists("pre.outl", envir = env)) lower = pre.outl[&class="macro">#x27;lower.class="num">25%&class="macro">#x27;, i] upper = pre.outl[&class="macro">#x27;upper.class="num">75%&class="macro">#x27;, i] med = pre.outl[&class="macro">#x27;med&class="macro">#x27;, i] cap1 = pre.outl[&class="macro">#x27;cap1.class="num">5%&class="macro">#x27;, i] cap2 = pre.outl[&class="macro">#x27;cap2.class="num">95%&class="macro">#x27;, i] treatOutlier(x = x[ ,i], impute = T, fill = T, lower = lower, upper = upper, med = med, cap1 = cap1, cap2 = cap2) } %>% as.data.frame() -> x.cap
◍ 特征变换与空间符号归一的执行链
这段处理把封顶后的特征做了多路数学变换,再统一过 spatialSign 归一,方便后续模型不被量纲带偏。对数变换用 log2(x+1),正弦用 sin(2*pi*x),双曲正切用 tanh(x),三路都按 1:3 的索引切分训练/验证/测试并回绑 Class 标签。 常规化那步只针对 DT 原表与三路变换表各取训练集拟合 preProcess(method="spatialSign"),再 predict 到对应 val、test;例如 DTLn.n 直接用 DTLn[[1]] 拟合后映射到 [[2]]、[[3]],跨折叠复用同一投影参数。 离散化部分先用 mdlp() 对 DT$train 去掉 Data 列做最小描述长度切分,切点在 mdlp.train 里;外汇与贵金属特征做这类离散大概率会改变样本分布,实盘前应在 MT5 导出的真实 tick 数据上重跑确认边界稳定性。 封顶变量在 evalq 里 rm(lower, upper, med, cap1, cap2, x.cap, x) 清掉,避免环境里残留中间对象拖慢后续 foreach 循环,这是 R 内存管理里容易被忽略的一环。
离散化后训练测试验证集的切分落地
把 MDLP 算出的切割点套到测试集与验证集,是特征离散化里最容易抄错的一步。上面这段 R 代码先挑出训练时判定为数值型、必须离散的列(idx 逻辑),再对 test 和 val 分别用 findInterval 按 mdlp.train$cutp 重映射区间,并 +1 让区间号从 1 起算,最后把列名对齐回训练集。 实测中若切割点向量长度为 8,则 length(idx) 循环 8 次,测试集 9 列数值特征会被压缩成 8 列因子区间加 1 列 Class;若某列 idx 为 FALSE,该列原样保留不离散。 plot_num 设 bins=20 可快速看训练集分布,smbinning.sumiv 的 cex=0.7 控制 IV 图字体,par(mfrow=c(2,2)) 意味着一屏比对四个变换域(如对数、正弦)的信息值。外汇与贵金属特征做这类离散前须意识到:样本外行情结构漂移可能导致切割点失效,建模结果仅代表历史样本倾向。 renamepr 把带点的变量名(v.fatl 等)改成下划线短名,纯粹是为 smbinning 包兼容;targ.int 把 Class 转成 0/1 整数目标,这一步漏了会直接报类型错。
{
DTd <- list()
mdlp.train$cutp %>%
lapply(., function(x) is.numeric(x)) %>%
unlist -> idx # class="type">bool
#----训练-----------------
mdlp.train$Disc.data[ ,idx] -> DTd$train
#---测试------------
DT$test %>%
select(-c(Data, Class)) %>%
as.data.frame() -> test.d
foreach(i = class="num">1:length(idx), .combine = &class="macro">#x27;cbind&class="macro">#x27;) %do% {
if (idx[i]) {
findInterval(test.d[ ,i],
vec = mdlp.train$cutp[[i]],
rightmost.closed = FALSE,
all.inside = F,
left.open = F)
}
} %>% as.data.frame() %>% add(class="num">1) %>%
cbind(., DT$test$Class) -> DTd$test
colnames(DTd$test) <- colnames(DTd$train)
#-----验证-----------------
DT$val %>%
select(-c(Data, Class)) %>%
as.data.frame() -> val.d
foreach(i = class="num">1:length(idx), .combine = &class="macro">#x27;cbind&class="macro">#x27;) %do% {
if (idx[i]) {
findInterval(val.d[ ,i],
vec = mdlp.train$cutp[[i]],
rightmost.closed = FALSE,
all.inside = F,
left.open = F)
}
} %>% as.data.frame() %>% add(class="num">1) %>%
cbind(., DT$val$Class) -> DTd$val
colnames(DTd$val) <- colnames(DTd$train)
rm(test.d, val.d)
},
env
)
require(funModeling)
plot_num(env$DT$train %>% select(-Data), bins = class="num">20)
require(smbinning)
targ.class="type">int <- function(x){
x %>% tbl_df() %<>%
mutate(Cl = (as.numeric(Class) - class="num">1) %>%
as.integer()) %<>%
select(-Class) %>% as.data.frame()
}
renamepr <- function(X){
X %<>% rename(v_fatl = v.fatl,
v_satl = v.satl,
v_rftl = v.rftl,
v_rstl = v.rstl,
v_ftlm = v.ftlm,
v_stlm = v.stlm,
v_rbci = v.rbci,
v_pcci = v.pcci)
class="kw">return(X)
}
par(mfrow = c(class="num">2,class="num">2))
evalq({
df <- renamepr(DTLn.n[[class="num">1]]) %>% targ.class="type">int
sumivt.ln.n = smbinning.sumiv(df = df, y = &class="macro">#x27;Cl&class="macro">#x27;)
smbinning.sumiv.plot(sumivt.ln.n, cex = class="num">0.7)
rm(df)
}, env)
evalq({
df <- renamepr(DTSin.n[[class="num">1]]) %>% targ.class="type">int「用信息值筛掉弱特征」
做价格行为建模时,先把一堆衍生指标丢进信息值(IV)计算,比肉眼翻图靠谱。上面这段 R 脚本在独立环境 env 里,对常规化、正弦、双曲正切三类变换后的特征表分别跑 smbinning.sumiv,统一以收盘价 Cl 作目标变量,cex=0.7 控制 IV 条形图的字号。 从常规化那张 IV 表看,v_fatl 拿了 0.6823 排第一,ftlm 0.4926 次之,v_satl 0.3737、rbci 0.3551、v_rbci 0.3424 都过了 0.3 的强区分线;而 stlm 和 v_rstl 直接 NA,算法报 No significant splits,这类特征在外汇与贵金属这种高风险品种上基本可以扔了。 脚本最后用 IV>0.1 的阈值捞了一遍正弦变换特征,best.sin.n 吐出 9 个候选:v_fatl、ftlm、rbci、v_rbci、v_satl、pcci、v_ftlm、v_stlm、v_rftl。实际开 MT5 导出样本、跑完这套筛选,你就能直接砍掉一半以上噪声变量,把精力压到 v_fatl 这类高 IV 特征的分箱与边界验证上。 对 v_fatl 单独跑 smbinning 并画 2x2 分布图,能看清不同 Cl 区间下该特征的箱线离散度,后续调阈值或写 EA 条件才有依据。外汇贵金属波动受消息面扰动大,高 IV 只代表历史样本区分度高,实盘信号失效概率始终存在。
sumivt.sin.n = smbinning.sumiv(df = df, y = &class="macro">#x27;Cl&class="macro">#x27;) smbinning.sumiv.plot(sumivt.sin.n, cex = class="num">0.7) rm(df) }, env) #---常规化------------- evalq({ df <- renamepr(DTn[[class="num">1]]) %>% targ.class="type">int sumivt.n = smbinning.sumiv(df = df, y = &class="macro">#x27;Cl&class="macro">#x27;) smbinning.sumiv.plot(sumivt.n, cex = class="num">0.7) rm(df) }, env) #-----三角变换---------------- evalq({ df <- renamepr(DTTanh.n[[class="num">1]]) %>% targ.class="type">int sumivt.tanh.n = smbinning.sumiv(df = df, y = &class="macro">#x27;Cl&class="macro">#x27;) smbinning.sumiv.plot(sumivt.tanh.n, cex = class="num">0.7) rm(df) }, env) par(mfrow = c(class="num">1,class="num">1)) env$sumivt.ln.n evalq(sumivt.sin.n$Char[sumivt.sin.n$IV > class="num">0.1] %>% na.omit %>% as.character() -> best.sin.n, env) evalq({ df <- renamepr(DTTanh.n[[class="num">1]]) %>% targ.class="type">int x = &class="macro">#x27;v_fatl&class="macro">#x27; y = &class="macro">#x27;Cl&class="macro">#x27; res <- smbinning(df = df, y = y, x = x) par(mfrow = c(class="num">2,class="num">2)) sub = paste0(x, " vs ", y) boxplot(df[[x]]~df[[y]], horizontal = TRUE, frame = FALSE, col = "lightblue", main = "Distribution") mtext(sub,class="num">3) smbinning.plot(res, option = "dist",
◍ 用分箱切点把连续因子转成离散档位
这段脚本跑的是监督分箱(smbinning)后的切点落地。两次 env$res$cuts 输出给出了不同样本下的临界值:第一组为 -0.3722、-0.0433、0.1482,第二组为 -0.2084、-0.0150、0.2216,说明同一因子在训练切片变动时,最优断点会漂移,直接硬编码切点有概率过拟合。 核心动作在第二个 evalq 块:用 findInterval 把 train 里通过 IV 筛选的连续列,按 res[[i]]$cuts 映射成整数档(0、1、2…),再 +1 偏移避免 0 档,最后 cbind 原 Class 列成 DT1.d$train。这样下游就能拿离散档做频率图或逻辑回归,而不是裸连续值。 外汇与贵金属行情具有高杠杆与跳空风险,分箱只是降低维度噪声的手段,切点稳定性需用样本外数据复验,不代表任何方向确定性。 plot_num 三行分别看原始训练集、离散化后全集、以及 IV>0.1 的精选因子分布;在 MT5 外接 R 环境里直接 source 这段,能快速判断哪几个因子档位区分度够用。
sub = sub) #"pcci vs Cl") smbinning.plot(res, option = "goodrate", #"badrate" sub = sub) #"pcci vs Cl") smbinning.plot(res, option = "WoE", sub = sub) #"pcci vs Cl") par(mfrow = c(class="num">1, class="num">1)) }, env) > env$res$cuts [class="num">1] -class="num">0.3722 -class="num">0.0433 class="num">0.1482 > env$res$cuts [class="num">1] -class="num">0.2084 -class="num">0.0150 class="num">0.2216 evalq({ res <- list() DT$train %>% renamepr() %>% targ.class="type">int() -> df x <- colnames(df) y <- "Cl" foreach(i = class="num">1:(ncol(df) - class="num">1)) %do% { smbinning(df, y = y, x = x[i]) } -> res res %>% lapply(., function(x) x[class="num">1] %>% is.list) %%%> unlist -> idx }, env) evalq({ DT1.d <- list() DT$train %>% renamepr() %%%> targ.class="type">int() %>% select(-Cl) -> train foreach(i = class="num">1:length(idx), .combine = &class="macro">#x27;cbind&class="macro">#x27;) %do% { if (idx[i]) { findInterval(train[ ,i], vec = res[[i]]$cuts, rightmost.closed = FALSE, all.inside = F, left.open = F) } } %>% as.data.frame() %>% add(class="num">1) %%%> cbind(., DT$train$Class) -> DT1.d$train colnames(DT1.d$train) <- colnames(train)[idx] %%%> c(., &class="macro">#x27;Class&class="macro">#x27;) }, env) evalq({ DT$train %>% renamepr() %>% targ.class="type">int() -> df sumivt.dt1.d = smbinning.sumiv(df = df, y = &class="macro">#x27;Cl&class="macro">#x27;) sumivt.dt1.d$Char[sumivt.dt1.d$IV > class="num">0.1] %%%> na.omit %>% as.character() -> best.dt1.d rm(df) }, env) require(funModeling) plot_num(env$DTd$train) plot_num(env$DT1.d$train) plot_num(env$DT1.d$train[ ,env$best.dt1.d])
用贝叶斯变分挑出五个主力预测因子
换一条少有人走的路来筛预测因子:调用 varbvs 包做贝叶斯变量选择。它底层是 Carbonetto 等人 2012 年提出的变分近似算法,原生支持线性回归与逻辑回归,设计上能啃下百万级变量、数千样本的数据集,对我们这种小规模特征矩阵属于杀鸡用牛刀。 把常规化训练集 DTTanh.n$train 灌进去,目标变量转成 0/1 向量走二项式逻辑回归。跑完 summary,算法在前十名里圈出五个最该保留的因子:ftlm、pcci、rbci、v.stlm、v.satl。这和之前用别的方法排出来的前五重合,但权重排序不同。 验证集和测试集上的表现几乎贴平,说明不重新训练、直接套这个先验模型,泛化收益有限。外汇与贵金属行情的高风险属性意味着,这类静态筛选出的因子组合在实盘里胜率可能偏低,需动态重估。 结论先记死这五个名字:ftlm、pcci、rbci、v.stlm、v.satl,它们是目前贝叶斯视角下最值得留的变量。
require(varbvs) evalq({ train <- DTTanh.n$train %>% targ.class="type">int() %>% as.matrix() fit <- varbvs(X = train[ ,-ncol(train)] , Z = NULL, y = train[ ,ncol(train)] %>% as.vector(), "binomial", logodds = seq(-class="num">2,-class="num">0.5,class="num">0.1), optimize.eta = T, initialize.params = T, verbose = T, nr = class="num">100 ) print(summary(fit)) }, env)
「贝叶斯变量筛选跑完后的参数读图」
上面这段输出来自一次二分类贝叶斯变量选择模型的拟合收尾。模型用了 2000 个样本、12 个候选变量、1 个协变量,对 16 组超参数组合做了变分边际似然计算,最大对数似然下界落在 -1204.1931。 超参估计里,sa(系数先验方差)估出 3.49,95% 区间 [3.25, 3.6];logodds 估出 -0.75,区间 [-1.30, -0.50],说明变量被选中的先验赔率偏低。 按 0.10 到 0.95 六档概率阈值去筛,选中的变量数恒为 3 个,稳定性很强。Top 变量中 ftlm(索引1)和 pcci(索引4)包含概率都是 1.0000,后验系数分别约 2.442 和 2.088,且 95% 区间全在正值侧,对目标事件倾向正向贡献。外汇与贵金属行情下用这类信号,仍属高风险,概率结论只作加权参考。 开 MT5 接小布的分析模块时,可直接把 ftlm、pcci 两个序列作为必留特征,其余 10 个变量按 0.95 阈值大概率可砍掉,能省掉一半以上的算力开销。
◍ 验证集与测试集的混淆矩阵实况
变量选择阶段,rbci 的系数估计为 0.9558,优势比约 0.709,95% 区间 [+0.369, +1.051],在 logistic 回归里属于正向但置信区间偏宽的一类;v.stlm 与 v.satl 系数仅 0.0356 与 0.0325,区间都跨过 0,单独看信号弱。 在验证集上跑出混淆矩阵:0 类判对 347、误判 204,1 类判对 312、误判 137,整体准确率 0.659,95% CI (0.6287, 0.6884)。No Information Rate 是 0.516,P-Value [Acc > NIR] < 2.2e-16,说明模型显著优于乱猜基准,但 Kappa 只有 0.3202,一致性算中等。 测试集结果:0 类 270 对、140 错,1 类 404 对、186 错,准确率 0.674,95% CI (0.644, 0.703)。两套样本准确率差 1.5 个百分点,过拟合迹象不明显。外汇与贵金属行情受杠杆与跳空影响,这类概率模型只给倾向,实盘前请在 MT5 用历史数据重跑确认。
分类器混淆矩阵外的那串统计量
跑完监督学习模型后,MT5 外接 R 或 Python 吐出的评估表里,有一组容易被跳过却很关键的数字。No Information Rate 是 0.544,意味着瞎猜多数类本身就有 54.4% 的命中率,模型准确率必须显著超过这条线才有用。 P-Value [Acc > NIR] 小于 2e-16,说明当前模型相对「无信息基准」的超越在统计上极显著,不是抽样噪声。Kappa 0.3375 则提示模型与随机预测的一致度只算中等,别被表面准确率骗了。 Mcnemar 检验 P 值 0.01269,暗示本模型与某个对照模型在错误结构上差异显著。Sensitivity 0.5921、Specificity 0.7426,说明对负类('Positive' Class: 0)的识别更稳,正类漏检偏多。 Balanced Accuracy 0.6674 比单纯准确率更可信,外汇与贵金属行情高风险,这类概率模型只可作辅助过滤,实盘前务必在 MT5 用历史数据重算一遍上述指标。
「让神经网络自己挑预测因子」
做价格行为建模时,预测因子不是越多越好。用 FCNN4R 这个 C++ 神经网函数库的 R 接口,可以搭一个全连接网,让它自己学出哪些输入维度真正有用。底层引擎支持反向传播、Rprop、模拟退火等训练法,也带最小量值修剪,计算效率比手搓脚本高出一个量级。 具体搭网:输入层 12 个神经元(对应 12 个候选预测因子),两个隐藏层分别 8 和 5 个,输出层 1 个;权重在 ±0.17 区间内随机初始化,隐藏层与输出层激活函数依次设为 tanh、tanh、sigmoid。训练用 rprop,容错 tol=0.1、最多 1000 代、L2 正则 0.0001,增量 u=1.2、衰减 d=0.5。 跑完修剪和变量重要性分析会发现,最终结构收敛到 (12,2,1,1) 就够用,而且早前人工筛出的九个“最好变量”里的 v.rstl 和 v.pcci 并没被网选中。这说明网挑因子不只看数据本身,也严重依赖网络结构与超参——同一批输入换层数和激活就可能选出另一套。外汇和贵金属波动受宏观事件扰动,这类模型仅给出概率倾向,实盘前务必在 MT5 用历史数据复跑验证。
require(FCNN4R) evalq({ mlp_net(layers = c(class="num">12, class="num">8, class="num">5, class="num">1), name = "n.tanh") %>% mlp_rnd_weights(a = class="num">0.17) %>% mlp_set_activation(layer = c(class="num">2, class="num">3, class="num">4), activation = c("tanh", "tanh", "sigmoid"), #"threshold", "sym_threshold", #"linear", "sigmoid", "sym_sigmoid", #"tanh", "sigmoid_approx", #"sym_sigmoid_approx"), slope = class="num">0) -> Ntanh class="macro">#show() #------- train <- DTTanh.n$train %>% targ.class="type">int() %>% as.matrix() test <- DTTanh.n$test %>% targ.class="type">int() %>% as.matrix() val <- DTTanh.n$val %>% targ.class="type">int() %>% as.matrix() }, env) evalq({ tol <- class="num">1e-1 max_ep = class="num">1000 l2reg = class="num">0.0001 net_rp <- mlp_teach_rprop(Ntanh, input = train[ ,-ncol(train)], output = train[ ,ncol(train)] %>% as.matrix(), tol_level = tol, max_epochs = max_ep, l2reg = l2reg, u = class="num">1.2, d = class="num">0.5,
◍ 剪枝后留下的六个特征
上面这段 R 脚本接在 MLP 训练之后,做的是权重幅值剪枝与特征筛选。mlp_prune_mag 里 tol 设成 1e-1、max_reteach_epochs 给到 1000、l2reg 取 0.0001,相当于在容差 0.1 内砍掉贡献弱的连接,再重训补回精度,plots=TRUE 会把剪枝前后的网络结构画出来。 剪完之后用 train 数据集挑了第 1、5、7、8、10、12 列做最终输入,env$best 打印出的六个名字是 ftlm、v.fatl、v.rftl、v.rstl、v.stlm、v.pcci。也就是说,经过幅值剪枝,网络倾向保留这六个指标作为有效输入,其余列大概率被判定为噪声或冗余。 外汇与贵金属市场高风险,这类特征筛选结果只代表回测样本内的结构,换周期或换品种后有效输入可能漂移,需上 MT5 用对应品种历史数据重跑验证。后续会拆怎么从集合里删噪声、怎么压输入维度,以及训练/验证/测试切分方式对结果的影响。
gmax = class="num">50, gmin = class="num">1e-06, report_freq = class="num">100) }, env) plot(env$net_rp$mse, t = "l", main = paste0("max_epochs =", env$max_ep, " l2reg = ", env$l2reg)) evalq(mlp_plot(net_rp$net, FALSE), envir = env) evalq({ tol <- class="num">1e-1 max_ep = class="num">1000 l2reg = class="num">0.0001 mlp_prune_mag(net_rp$net, input = train[ ,-ncol(train)], output = train[ ,ncol(train)] %>% as.matrix(), tol_level = tol, max_reteach_epochs = max_ep, report = FALSE, plots = TRUE) -> net_rp_prune }, env) evalq( best <- train %>% tbl_df %>% select(c(class="num">1,class="num">5,class="num">7,class="num">8,class="num">10,class="num">12)) %>% colnames(), env) env$best [class="num">1] "ftlm" "v.fatl" "v.rftl" "v.rstl" "v.stlm" [class="num">6] "v.pcci"
脚本落地的几处坑
这套特征工程与神经网络融合的 R 脚本可从 Git 的 /Part_II 路径取用,包含 FeatureTransformation.R、FeatureSelect.R、FeatureSelect_analitic.R、FeatureSelect_NN.R 及对应的 RData 示例。想在 MT5 外接 R 做同类降维与筛选,先把这些脚本跑通再谈模型。 有读者在 2021 年 7 月反馈,funModeling 包里找不到 bayesian_plot() 函数,作者回复称近期版本已移除该功能、原因未公开。若你复现旧代码报错,优先查包版本而非逻辑写错。 下方两行是原文里被反复点名的缺失函数调用,贴出来方便你对照自己环境里的报错信息。外汇与贵金属行情高波动,任何外接脚本的结论都只是概率倾向,实盘前务必在策略测试器里重验。
bayesian_plot() bayesian_plot()