深度神经网络 (第 I 部)。准备数据·综合运用
🧹

深度神经网络 (第 I 部)。准备数据·综合运用

(3/3)·垃圾进垃圾出,70% 的建模时间耗在数据准备,本篇把清洗到消偏态一次跑通

实战向 第 3/3 篇
直接把终端导出的 OHLC 丢进神经网络,是多数自建模型跑不出样本外结果的根因。报价里的缺口、极端跳空和偏态分布会悄悄绑架权重。先把数据按阶段收拾干净,再谈预测。

分布偏度与峰度暴露的非正态事实

上表给出四个数字滤波通道的描述统计:第三四分位分别为 0.0273、0.0143、0.0207、0.0278,标准差在 0.0264 到 0.0579 之间,而峰度最高冲到 233.1964(第四通道),远超正态基准值 3。 偏度一列从 0.8513 到 3.0207 不等,说明这四组序列右尾普遍偏厚;外汇与贵金属价差类特征若呈现这种形态,回测里用正态假设算置信区间会系统性低估极端滑点风险。 95% 均值置信带很窄(如 LCL -0.0013 到 UCL 0.0013),表面看均值接近零,但结合峰度可知:均值稳定不代表尾部可控,样本外跳空可能倾向比正态模型预测更频繁。 下方 R 代码用 GGally 的 ggpairs 按 Class 着色,把 2:6、7:10、11:14 列分别画成 DigFilter1~3 的成对散点矩阵;在 MT5 导出同类特征 csv 后,可照此在 R 里复跑,肉眼核对聚类是否和训练标签一致。

MQL5 / C++
require(GGally)
evalq(ggpairs(dataSet, columns = class="num">2:class="num">6,
              mapping = aes(class="type">class="kw">color = Class),
              title = "DigFilter1"),
     env)
evalq(ggpairs(dataSet, columns = class="num">7:class="num">10,
              mapping = aes(class="type">class="kw">color = Class),
              title = "DigFilter2"),
     env)
evalq(ggpairs(dataSet, columns = class="num">11:class="num">14,
              mapping = aes(class="type">class="kw">color = Class),
              title = "DigFilter3"),
     env)

「数据准备七阶段与异常值实战处理」

把原始行情特征送进模型前,先走完七道准备关:插补缺失、剔零方差、切分集合、缩放范围、清异常值、调类别不平衡、降噪并筛预测器。任何模型都建议先对完整数据集做清理,而不是等训练时才补窟窿。 用 caret 的 preProcess 一次性干掉零方差、近零方差以及单类唯一值预测器,相关系数阈值默认 0.9 可改。下方代码在 7906 行样本上跑完,v.rbci 被移除,干净集合剩 13 列。 局部异常值按 IQR 法认定:超出 ±1.5*IQR 即异常,±3*IQR 外算极端。外汇与贵金属特征里这种点常由滤波人造,直接删或换成 0.05/0.95 百分位更稳。 全局异常值用 LOF 看局部密度,Rlof 包支持多核算 k=5~10 邻居。实测绝大多数观测 lofactor 落在 1.6 内,越界数量相对 4000 根训练柱线来讲不大,但测试集必须复用训练集算出的 upper/lower/cap 参数,不能各自为政。 偏度与峰度检查显示,删异常后的 x.out 与插补后的 x.cap 都已接近对称,峰度也回正常,无需额外变换。提醒:外汇贵金属杠杆高、跳空多,异常值处理不当会放大回测虚高,实盘前务必用 MT5 导出的真实tick重跑这套。

MQL5 / C++
require(caret)
evalq({preProClean <- preProcess(x = dataSet,method = c("zv", "nzv", "conditionalX", "corr"))
      dataSetClean <- predict(preProClean, dataSet %>% na.omit)},
env)
> env$preProClean$method$remove
#[class="num">1] "v.rbci"
> dim(env$dataSetClean)
[class="num">1] class="num">7906   class="num">13
> colnames(env$dataSetClean)
[class="num">1] "Data"   "ftlm"   "stlm"   "rbci"   "pcci"  
[class="num">6] "v.fatl" "v.satl" "v.rftl" "v.rstl" "v.ftlm"
[class="num">11] "v.stlm" "v.pcci" "Class"
evalq(ggplot(dataSetClean, aes(x = factor(class="num">0),

◍ 用箱线法清洗指标离群值

做特征工程时,ftlm 这类技术指标序列里常混着极端跳变点。直接用 R 的 boxplot.stats 剔除,和按四分位距(IQR)手写阈值过滤,结果完全一致:all.equal 返回 TRUE,说明两套逻辑等价。 在样本集上跑一遍,原始数据行数减去保留下来的 ftlm 有效值,差了 402 条,也就是被判定为离群被丢掉的观测量。外汇与贵金属波动剧烈,这类异常值可能来自跳空或流动性真空,直接进模型会扭曲边界。 remove_outliers 函数把低于 Q1-1.5×IQR 或高于 Q3+1.5×IQR 的点设成 NA,用 foreach 按列批量处理很顺手。 如果不想删数据,capping_outliers 把离群值压到 5% 和 95% 分位上,箱体图对比能看出分布被「削峰」但样本量保住了。MT5 导出的 csv 丢进 R 照抄这段,能马上看到你自己的指标里有多少离群点。

MQL5 / C++
remove_outliers <- function(x, na.rm = TRUE, ...) {
  qnt <- quantile(x, probs = c(.class="num">25, .class="num">75), na.rm = na.rm, ...)
  H <- class="num">1.5 * IQR(x, na.rm = na.rm)
  y <- x
  y[x < (qnt[class="num">1] - H)] <- NA
  y[x > (qnt[class="num">2] + H)] <- NA
  y
}
capping_outliers <- function(x, na.rm = TRUE, ...) {
  qnt <- quantile(x, probs = c(.class="num">25, .class="num">75), na.rm = na.rm, ...)
  caps <- quantile(x, probs = c(.class="num">05, .class="num">95), na.rm = na.rm, ...)
  H <- class="num">1.5 * IQR(x, na.rm = na.rm)
  y <- x
  y[x < (qnt[class="num">1] - H)] <- caps[class="num">1]
  y[x > (qnt[class="num">2] + H)] <- caps[class="num">2]
  y
}

用局部离群因子和分位数给特征掐头去尾

这段脚本在 R 环境里先对原始特征矩阵 x 和裁剪版 x.cap 跑 LOF 离群检测:DMwR2 的 lofactor 用 k=10 算一遍,Rlof 的 lof 则对 k=5:10 并行跑 Minkowski 距离。把 Rlof.x.cap 第 6 列(k=10 结果)做直方图后,筛选值 ≥1.6 的样本数得出 32 个,说明在该邻域半径下约有 32 个高异常度点,外汇与贵金属特征里这类点多半是跳空或流动性断裂造成的噪声,需谨慎对待。 自写 prep.outlier 函数不走 LOF,而是用四分位距(IQR×1.5)定上下栅栏,再用 5%/95% 分位数做 cap 边界。对训练集前 4000 行逐列套用后,ftlm 列的 lower 约 -0.222、upper 约 0.221,cap1/cap2 落在 ±0.17 附近;stlm 的上下栅栏则在 ±0.596,cap 边界 ±0.40。这些数字直接可作为 MT5 自定义指标里 winsorize 的阈值参考。 实操上,把 cap1/cap2 两行当特征裁剪上下限,能在不删样本的前提下压住极端值。外汇与贵金属波动高危,阈值最好随品种波动率重算,别直接套用上面这组静态数。

MQL5 / C++
mapping = aes(class="type">class="kw">color = Class),
              title = "PredCap1"),
       env)
evalq(ggpairs(dataSetCap, columns = class="num">8:class="num">13,
              mapping = aes(class="type">class="kw">color = Class),
              title = "PredCap2"),
       env)
##------DMwR2-------------------
require(DMwR2)
evalq(lof.x <- lofactor(x,class="num">10), env)
evalq(lof.x.cap <- lofactor(x.cap,class="num">10), env)
par(mfrow = c(class="num">1, class="num">3))
boxplot(env$lof.x, main = "lof.x",
        boxwex = class="num">0.5)
boxplot(env$lof.x.cap, main = "lof.x.cap",
        boxwex = class="num">0.5)
hist(env$lof.x.cap, breaks = class="num">20)
par(mfrow = c(class="num">1, class="num">1))
require(Rlof)
evalq(Rlof.x <- lof(x, c(class="num">5:class="num">10), cores = class="num">2,
                                 method = &class="macro">#x27;minkowski&class="macro">#x27;),
       env)
  evalq(Rlof.x.cap <- lof(x.cap, c(class="num">5:class="num">10),
                                  cores = class="num">2,
                                  method = &class="macro">#x27;minkowski&class="macro">#x27;),
       env)
par(mfrow = c(class="num">2, class="num">3))  
hist(env$Rlof.x.cap[ ,class="num">6], breaks = class="num">20)
hist(env$Rlof.x.cap[ ,class="num">5], breaks = class="num">20)
hist(env$Rlof.x.cap[ ,class="num">4], breaks = class="num">20)
hist(env$Rlof.x.cap[ ,class="num">3], breaks = class="num">20)
hist(env$Rlof.x.cap[ ,class="num">2], breaks = class="num">20)
hist(env$Rlof.x.cap[ ,class="num">1], breaks = class="num">20)
par(mfrow = c(class="num">1, class="num">1))
> sum(env$Rlof.x.cap[ ,class="num">6] >= class="num">1.6)
[class="num">1] class="num">32
#-----prep.outlier--------------
prep.outlier <- function(x, na.rm = TRUE, ...) {
  qnt <- quantile(x, probs = c(.class="num">25, .class="num">75),
                  na.rm = na.rm, ...)
  H <- class="num">1.5 * IQR(x, na.rm = na.rm)
  caps <- quantile(x, probs = c(.class="num">05, .class="num">95),
                  na.rm = na.rm, ...)
  list(lower = qnt[class="num">1] - H, upper = qnt[class="num">2] + H,
       med = median(x),
       cap1 = caps[class="num">1], cap2 = caps[class="num">2])
}
evalq(
  {train <- x[class="num">1:class="num">4000, ]
  foreach(i = class="num">1:ncol(train), .combine = "cbind") %do% {
    prep.outlier(train[ ,i]) %>% unlist()
  } -> pre.outl
  colnames(pre.outl) <- colnames(x)
  class="macro">#pre.outl %<>% t()
  },
  env)

「用分位数截断给特征去极值」

上面那张表是训练集前 4000 行四个指标的分位数快照:v.ftlm 的 lower.25% 是 -0.0901、upper.75% 是 0.0888,而 cap1.5% 与 cap2.95% 分别卡在 -0.0659 和 0.0662。也就是说,超出 5%/95% 分位的点会被压回这两个边界,中位数 med 仅 -0.00086,分布基本对称。 第二张表是另一组三个指标(v.ftlm、v.stlm、v.pcci)的同样统计:v.pcci 的 upper.75% 达 0.1119,cap2.95% 为 0.0770,说明原始数据里约有 5% 样本被向外拉了将近一半的幅度。外汇与贵金属行情跳空时这类离群值很常见,直接进模型会扭曲权重,属于高风险数据预处理环节。 treatOutlier 这个函数给了三种处理路径:impute=TRUE 时把越界值直接替换成 cap1/cap2;fill=FALSE 时打成 NA;else 分支填中位数 med。实战里我倾向 impute 填边界,保留样本量比删行更稳。 下面两段 evalq 把规则套到 train 和 test(取 4001:6000 行)上,用 pre.outl 里存好的分位数逐列处理,最后两行 boxplot 能让你在 R 里直观对比去极值前后的须线变化。开 MT5 把同类指标导出到 csv,再用这套阈值跑一遍,离群压缩效果立刻能验证。

MQL5 / C++
#---------treatOutlier---------------------------------
  treatOutlier <- function(x, impute = TRUE, fill = FALSE,
                         lower, upper, med, cap1, cap2){
    if (impute) {
      x[x < lower] <- cap1
      x[x > upper] <- cap2
      class="kw">return(x)
    }
    if (!fill) {
      x[x < lower | x > upper] <- NA
      class="kw">return(x)  
    } else {
      x[x < lower | x > upper] <- med
      class="kw">return(x)
    }
}
#------------
evalq(
  {
  foreach(i = class="num">1:ncol(train), .combine = &class="macro">#x27;cbind&class="macro">#x27;) %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 = train[ ,i], impute = T, fill = T, lower = lower,
                 upper = upper, med = med, cap1 = cap1, cap2 = cap2)
  } -> train.out
  colnames(train.out) <- colnames(train)
  },
  env
)
#-------------
evalq(
  {test <- x[class="num">4001:class="num">6000, ]
  foreach(i = class="num">1:ncol(test), .combine = &class="macro">#x27;cbind&class="macro">#x27;) %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 = test[ ,i], impute = T, fill = T, lower = lower,
                 upper = upper, med = med, cap1 = cap1, cap2 = cap2)
  } -> test.out
  colnames(test.out) <- colnames(test)
  },
  env
)
#---------------
evalq(boxplot(train, main = "train  with outliers"), env)
evalq(boxplot(train.out, main = "train.out  without outliers"), env)

◍ 剔除极值后分布形态瞬间归位

把原始序列和去掉离群值后的版本都丢进偏度、峰度函数,差距一眼就能看出来。原始 x 里 ftlm 偏度 4.22、pcci 偏度 6.49,峰度更是夸张——pcci 超额峰度 247.98,v.fatl 也有 145.95,典型厚尾尖峰。 去掉 outliers 之后,sk.out 里所有通道偏度都落到 -0.08 到 0.02 之间,k.out 的超额峰度全部在 -0.16 到 -0.03 附近,基本贴着正态。说明那些极端跳空和报价异常才是把分布扯变形的元凶。 对外汇和贵金属来说,这种厚尾是常态高风险,欧美秒跳、黄金突发事件都会制造离群点。做策略回测时若不分桶剔除,参数拟合可能只是被少数极端 K 线带偏。 直接复现这段 R 评估代码,在 MT5 导出的 tick 或分钟序列上跑一遍,看你自己品种的 sk 和 k 落点。若原始峰度超 50,先清洗再建模。

MQL5 / C++
evalq(boxplot(test.out, main = "test.out  without outliers"), env)
#------------------------
evalq({
  sk <- skewness(x)
  sk.out <- skewness(x.out)
  sk.cap <- skewness(x.cap)
  },
  env)
> env$sk
                     ftlm      stlm      rbci      pcci    v.fatl
Skewness class="num">4.219857 class="num">1.785286 class="num">2.304655 class="num">6.491546 class="num">5.274871
                   v.satl   v.rftl   v.rstl   v.ftlm     v.stlm
Skewness class="num">2.677162 class="num">3.954098 class="num">1.568675 class="num">1.207227 class="num">0.8516043
                    v.pcci
Skewness class="num">3.031012
> env$sk.out
                        ftlm           stlm           rbci           pcci
Skewness -class="num">0.04272076 -class="num">0.07893945 -class="num">0.02460354 class="num">0.01485785
                   v.fatl        v.satl        v.rftl        v.rstl
Skewness class="num">0.00780424 -class="num">0.02640635 -class="num">0.04663711 -class="num">0.04290957
                       v.ftlm        v.stlm          v.pcci
Skewness -class="num">0.0009597876 class="num">0.01997082 class="num">0.0007462494
> env$sk.cap
                       ftlm           stlm           rbci           pcci
Skewness -class="num">0.03329392 -class="num">0.07911245 -class="num">0.02847851 class="num">0.01915228
                   v.fatl        v.satl        v.rftl        v.rstl
Skewness class="num">0.01412182 -class="num">0.02617518 -class="num">0.03412228 -class="num">0.04596505
                      v.ftlm       v.stlm       v.pcci
Skewness class="num">0.008181183 class="num">0.009661169 class="num">0.002252508
require(PerformanceAnalytics)
evalq({
  k <- kurtosis(x)
  k.out <- kurtosis(x.out)
  k.cap <- kurtosis(x.cap)
},
env)
> env$k
                               ftlm      stlm      rbci      pcci
Excess Kurtosis class="num">84.61177 class="num">16.77141 class="num">45.01858 class="num">247.9795
                         v.fatl  v.satl  v.rftl  v.rstl
Excess Kurtosis class="num">145.9547 class="num">36.99944 class="num">74.4307 class="num">13.57613
                         v.ftlm  v.stlm  v.pcci
Excess Kurtosis class="num">86.36448 class="num">23.06635 class="num">233.5408
> env$k.out
                                  ftlm        stlm        rbci
Excess Kurtosis -class="num">0.003083449 -class="num">0.1668102 -class="num">0.1197043
                                pcci        v.fatl        v.satl
Excess Kurtosis -class="num">0.05113439 -class="num">0.02738558 -class="num">0.04341552
                       v.rftl       v.rstl       v.ftlm
Excess Kurtosis -class="num">0.01219999 -class="num">0.1316499 -class="num">0.0287925
                      v.stlm         v.pcci
Excess Kurtosis -class="num">0.1530424 -class="num">0.09950709

峰度偏负暗示指标分布更平

把 ftlm、stlm、rbci 等九条常用辅助线的超额峰度(Excess Kurtosis)拉出来看,数值全部落在 -0.218 到 -0.307 之间,没有一条为正。 这意味着这些序列的尾部比正态分布更薄,中间段更集中,极端值出现的概率偏低。做外汇或贵金属时这类分布容易让人误判突破力度,属于高风险误判点。 直接在 MT5 里用 kurtosis 函数对对应指标缓冲数组跑一遍,就能复现上表;若你换周期后峰度转正,原有关键位参考就需要重估。

MQL5 / C++
env$k.cap
                    ftlm      stlm      rbci
Excess Kurtosis -class="num">0.2314336 -class="num">0.3075185 -class="num">0.2982044
                    pcci   v.fatl   v.satl
Excess Kurtosis -class="num">0.2452504 -class="num">0.2389486 -class="num">0.2331203
                   v.rftl   v.rstl   v.ftlm
Excess Kurtosis -class="num">0.2438431 -class="num">0.2673441 -class="num">0.2180059
                   v.stlm    v.pcci
Excess Kurtosis -class="num">0.2763058 -class="num">0.2698028

「三个压缩包分别装了什么」

DARCH12_1.zip 里是系列第一部分的 R 脚本集合:dataRaw.R、PrepareData.R、FUNCTION.R,外加一张展示 Rstudio 初始会话的示意图,底层数据文件是 Cotir.RData。把它解压进 Rstudio 后,三个脚本会直接出现在工程目录里,可立即改路径跑通数据预处理。 如果想对照另一套思路,ACTF.zip 收的是 V. Kravchuk 那篇《跟随趋势和市场周期的新自适应方法》原文,适合做方法交叉验证。 R_intro.zip 则是 R 语言本身的参考资料包,给没踩过 R 坑的 MT5 玩家补环境基础。三个包也都能从 Git/Part_I 拉取,不用死守网盘链接。

◍ 收束

上面那段 R 代码是整套深度神经网络系列里最容易被忽略的落点:用轻量环境隔离数据、再用 ZigZag 把价格序列压成拐点向量。env <- new .env() 建的是一个独立命名空间,ls(env) 返回 character(0) 说明初始为空,往里塞 env$a <- 23 后 ls(env) 才出现 "a",这种隔离能避免不同工具变量互相污染。 ZZ 函数里 par 默认 c(25,5),ch=25 在 Dig 未知时会走 ch <- ch/(10^(Dig-1)) 做小数位修正;mode=5 时取 High、Low 双列交给 TTR::ZigZag,change=25 点、percent=F、lastExtreme=T,循环把 NA 向前填充。外汇与贵金属波动大,这类拐点提取只是特征工程一步,实盘信号仍可能失效,务必在 MT5 外接 R 跑一遍验证。 系列写到这,从预测因子制定到降维、贝叶斯调参、袋封融合都铺完了。剩下要做的不是再读文章,而是把 ZIP 里的 R-intro(约 15.3 MB)下下来,把 Dig 和 env 在脚本头部补上,让 ZigZag 在你的品种上先跑出第一条折线。

MQL5 / C++
env <- new .env()
ls(env)
character( class="num">0 )
env$a <- class="num">23
ls(env)
[ class="num">1 ] "a"
> env$a
[ class="num">1 ] class="num">23 
#------ZZ-----------------------------------
par <- c(class="num">25, class="num">5)
ZZ <- function(x, par) {
# x - vector
  require(TTR)
  require(magrittr)
  ch = par[class="num">1] 
  mode = par[class="num">2]
  if (ch > class="num">1) ch <- ch/(class="num">10 ^ (Dig - class="num">1))
  class="kw">switch(mode, xx <- x$Close,
         xx <- x$Med, xx <- x$Typ,
         xx <- x$Wd, xx <- x %>% select(High,Low))
  zz <- ZigZag(xx, change = ch, percent = F, 
               retrace = F, lastExtreme = T)
  n <- class="num">1:length(zz)
  for (i in n) { if (is.na(zz[i])) zz[i] = zz[i - class="num">1]}
  class="kw">return(zz)
}
把重复劳动交给小布
这些诊断与小布盯盘的 AIGC 已内置,打开对应品种页即可看到异常报价与分布偏态提示,你只需专注特征构造与决策。

常见问题

可用 summary() 与 str() 做总体统计和结构体浏览,再配合 ggplot2 做分布可视化,定位缺失与异常。
基于已收盘的 N 根 K 线构造滞后标签,例如未来 M 根收益符号,严禁混入当前未闭合的 tick 或订单流。
常用 log、Box-Cox 或秩变换,降低重尾能让梯度更新更稳,但需对训练与测试集同参拟合避免前视。
可以,小布的品种页会标记跳空与统计离群点,并给出分布偏度参考,省去手写 EDA 脚本的重复劳动。
系列聚焦多层全连接这一可控基线,把数据管道跑顺后再扩到合奏或新 darch 功能,避免复杂度过早叠加。