EA交易的自我优化: 进化与遗传算法·综合运用
◍ 为什么选遗传算法而不是经典优化
遗传算法(GA)相对经典优化方法,第一个实在的好处是对目标函数的数学形态几乎没要求。你不用为了套用拉格朗日或梯度下降去削平模型,线性、非线性、连续或混合数据集都能直接丢进去跑,模型特征不打折。 第二个差别在搜解路径。经典步进法只有在问题本身有突出结构属性时才能摸到全局最优;GA 靠交叉、变异这类进化操作,更有可能在复杂地貌里捞出全局最优,而不是困在局部洼地。 还有些不算核心但很实用的点:参数多意味着你能搭出高效的启发式搜索,本身适合并行跑,效果和随机搜索至少持平,而且它借了生物进化的框架,理论上存在指数级加速的预期。 不过别迷信。GA 自由度太高,一堆可调参数容易把“做优化”变成“调参游戏”;它缺乏覆盖率的理论证明,而在平滑、单极值的简单目标函数里,GA 通常比朴素搜索更慢。外汇与贵金属市场高风险,用 GA 寻参前先在 MT5 用历史数据比一下朴素搜索耗时。
「实验环境与两类优化任务的落地路径」
本组实验全部在 R 3.2.4 环境下跑,沿用前一篇的数据集与函数库做模型训练。CRAN 上托管的 GA、EM 类功能包被用来解优化问题,但硬约束只有一个:训练耗时不能上几百秒。每一代至少 100 个物种、种群要跨数个到数十个世代,慢方法直接弃用;前文验证过的 SAE 与 RBM 两种深度网络初始化都够快,适合塞进遗传优化里用。 具体拆成两类活:一是搜最佳预测器组合,二是挑指标的最佳参数。前者用 XGBoost(R 里走 xgboost 包 0.4-3 版)来干,它在分类任务上表现被来源文献点名过,顺带也当新算法练手。后者拿 MACDsample EA 当最简样例,看遗传算法优化指标参数到底能抠出多少冗余。 外汇与贵金属品种做这类优化回测波动剧烈,参数过拟合概率偏高,实盘前务必在 MT5 策略测试器里重跑一遍确认。
用禁忌搜索挑出那十几个有用的预测因子
做组合优化前先得把三件事钉死:优化的参数是什么、拿什么标量当标准(可多标准)、目标函数怎么算。本例里目标函数固定走一条流水线——建初始数据帧、切训练/测试集、训模型、测模型、算误差,优化判据直接用分类误差(1 - 预测偏差)。 禁忌搜索(tabuSearch v1.1)是爬山法的扩展,吃一个用户写的二进制串评估函数,吐出使目标函数最大的 0/1 配置,正好用来筛预测器开/关。主函数关键入参:size 是二进制长度,iters 是迭代次数,objFun 是用户目标函数,neigh 默认等于 size(每轮邻域数),listSize 是禁忌表长度,nRestarts 是最大重启次数。 实验沿用 EURUSD/M30、6000 根至 2016-02-14 的数据与函数。先跑 size=17、iters=10:实测 elapsed 23.77 秒,最大目标值 0.8,即预测准确率约 80%,选中 14 个预测器。 把 iters 拉到 100 后,计算时间只部分增加,准确率却仅微涨。这说明靠堆迭代捞不到太多油水,质量要往上走得去调模型自身参数。除 tabuSearch 外,kofnGA、fSelector 以及 caret 包的 gafs() 也能干同类的遗传/启发式特征选择。 下面这段是可直接丢进 R(接 xgboost / tabuSearch)跑的本体,二进制串第 i 位为 1 就保留第 i 个预测器,目标函数返回 1 - 测试集误分率。
tabuSearch(size = class="num">10, iters = class="num">100, objFunc = NULL, config = NULL, neigh = size, listSize = class="num">9, nRestarts = class="num">10, repeatAll = class="num">1, verbose = FALSE) ObjFun <- function(th){ require(xgboost) # 如果二进制串全部为0就退出 if (sum(th) == class="num">0) class="kw">return(class="num">0) # 在二进制串中,对应着1的预测器名称 sub <- subset[th != class="num">0] # 创建用于训练模型的结构 dtrain <- xgb.DMatrix(data = x.train[ ,sub], label = y.train) # 训练模型 bst = xgb.train(params = par, data = dtrain, nrounds = nround, verbose = class="num">0) # 使用文本设定计算预测 pred <- predict(bst, x.test[ ,sub]) # 计算预测偏差 err <- mean(as.numeric(pred > class="num">0.5) != y.test) # 返回质量标准 class="kw">return(class="num">1 - err) } #---tabuSearch---------------------- require(tabuSearch) require(magrittr) require(xgboost) # 输出的数据桢 dt <- form.data(n = class="num">34, z = class="num">50, len = class="num">0) # 在初始集合中所有预测器的名称 subset <- colnames(In()) set.seed(class="num">54321, kind = "L&class="macro">#x27;Ecuyer-CMRG") # 准备用于训练和测试的数据集 DT <- prepareTrain(x = dt[ ,subset], y = dt$y, balance = FALSE, rati = class="num">4/class="num">5, mod = "stratified", norm = FALSE, meth = method) train <- DT$train test <- DT$test x.train <- train[ ,subset] %>% as.matrix() y.train <- train$y %>% as.numeric() %>% subtract(class="num">1) x.test <- test[ ,subset] %>% as.matrix() y.test <- test$y %>% as.numeric() %>% subtract(class="num">1) # 初始的二进制矢量 th <- rep(class="num">1,length(subset)) # 模型的参数 par <- list(max.depth = class="num">3, eta = class="num">1, silent = class="num">0, nthread = class="num">2, objective = &class="macro">#x27;binary:logistic&class="macro">#x27;) nround = class="num">10 # 初始配置 conf <- matrix(class="num">1,class="num">1,class="num">17) res <- tabuSearch(size = class="num">17, iters = class="num">10, objFunc = ObjFun, config = conf, listSize = class="num">9, nRestarts = class="num">1) # 对应函数的最大值 max.obj <- max(res$eUtilityKeep) # 二进制数组的最佳组合 best.comb <- which.max(res$eUtilityKeep)%>% res$configKeep[., ] # 预测器中的最佳 best.subset <- subset[best.comb != class="num">0] > system.time(res <- tabuSearch(size = class="num">17, iters = class="num">10, + objFunc = ObjFun, config = conf, listSize = class="num">9, nRestarts = class="num">1)) user system elapsed class="num">36.55 class="num">4.41 class="num">23.77 > max.obj [class="num">1] class="num">0.8 > best.subset
◍ 禁忌搜索筛出的 14 指标组合
上面这段输出来自一次针对 17 个候选技术指标的二元配置禁忌搜索(tabu search)。初始跑批设定为:算法重复 1 次、每次预搜索 10 轮、总迭代 30 次,禁忌表长度 9,邻域访问数 17,最终在 23 个独立最优配置里拿到目标函数峰值 0.79662,且该峰值只出现 2 次,说明解空间相当分散。 把迭代量拉到 iters=100 后,单次耗时 user 377.28s / system 42.52s / elapsed 246.34s,目标函数最高摸到 0.8042194。最优子集收敛到 14 个变量:DX、ar、atr、cci、chv、cmo、sign、vsig、slowD、oscK、SMI、signal 等。 外汇与贵金属市场波动结构不稳定,这种历史回测下的高目标函数值只代表样本内拟合倾向,实盘复制需警惕过拟合;建议直接把上面候选集丢进 MT5 多周期验证,别只看单一品种结论。
[class="num">1] "DX" "ADX" "oscDX" "ar" "tr" "atr" [class="num">7] "chv" "cmo" "vsig" "rsi" "slowD" "oscK" [class="num">13] "signal" "oscKST" > summary(res) Tabu Settings Type = binary configuration No of algorithm repeats = class="num">1 No of iterations at each prelim search = class="num">10 Total no of iterations = class="num">30 No of unique best configurations = class="num">23 Tabu list size = class="num">9 Configuration length = class="num">17 No of neighbours visited at each iteration = class="num">17 Results: Highest value of objective fn = class="num">0.79662 Occurs # of times = class="num">2 Optimum number of variables = c(class="num">14, class="num">14) > system.time(res <- tabuSearch(size = class="num">17, iters = class="num">100, + objFunc = ObjFun, config = conf, listSize = class="num">9, nRestarts = class="num">1)) user system elapsed class="num">377.28 class="num">42.52 class="num">246.34 > max.obj [class="num">1] class="num">0.8042194 > best.subset [class="num">1] "DX" "ar" "atr" "cci" "chv" "cmo" [class="num">7] "sign" "vsig" "slowD" "oscK" "SMI" "signal"
「用 rgenoud 给 MACD 策略找参数」
以 MT5 自带的 MACDSample EA 为蓝本,信号来自 macd 与 signal 线交叉。原版常用 26 和 12 周期,但函数底层的指数常数 0.075 与 0.15 实际对应 25.6667 与 12.3333 周期,这种隐性偏差值得在优化前先认清。 可变的维度有 9 个:价格源 p1、快线 nFast(8:21)、慢线 nSlow(13:54)、信号线 nSig(3:13)、MACD 的 MA 类型 p5、信号线的 MA 类型 p6、百分比开关 p7,再加信号判定方式 signal(1:3) 与历史深度 len(300:1000)。p5、p6 可取 SMA/EMA/DEMA/ZLEMA。信号生成可选项 1 直接比大小,选项 2 看 macd 差分符号,选项 3 再叠加 macd 零轴过滤。 优化器选 rgenoud 包,它把进化搜索和类牛顿方法揉在一起,支持多机多核并行。核心约束是所有参数须为整数或物理值,浮点范围可用整数映射绕过,例如搜 10~110 后在目标函数里除以 100 得到 0.1~1.1。 genoud() 里几个量产能直接决定搜索效率:pop.size 默认 1000 且必须为偶数(P6、P8 算子要成对父母);max.generations=100 只是软上限,真正停否看 wait.generations=10 内目标是否再提升;solution.tolerance=0.001 控制等价判定精度。data.type.int=TRUE 时强制整数搜索,BFGS 与梯度检查自动禁用。 算子集 P1~P9 默认权重各 50(P9 局部最小交叉设 0 即不跑 BFGS),genoud 会按权重分配调用次数并保证子代不同于父代。想验证,可在 R 里装 rgenoud,把 MACD 质量比 K 点数为目标函数,先以 unif.seed=812821、int.seed=53058 固定随机流跑一遍,再改 pop.size 看收敛代差。外汇与贵金属杠杆高,参数过拟合历史段后实盘可能迅速衰减,任何优化结果都只是概率倾向。
种群规模与代数如何拖垮EA质量
调遗传优化时,最吃结果的俩旋钮是种群大小 pop.size 和代数类参数(max.generations、wait.generations、hard.generation.limit、gradient.check)。经验上,种群和代数往上加,搜索精度会提高,但耗时同步放大,得按问题难度手工权衡。 搜索域 Domains 与 default.domains 也别忽视。线性和非线性约束写不进参数边界时,就塞进适应度函数里:比如要求 parm1+parm2≥725,不满足就 return(-99999999),优化器吃到坏值会自己绕开。 单核跑一轮 MACD 信号优化,解码出的表现是:price type=Close、nFast=14、nSlow=26、nSig=8、macdType=ZLEMA、sigType=SMA、percent=TRUE、信号取 macd 与信号线交叉、历史长度 400 柱。用这套参数回测余额曲线(图2)已能接受,EA 可上。 换双核集群算同样任务,耗时明显好看,但质量略有掉点——说明简单任务也得调「调整」参数,不是堆硬件就完事。SOMA 这类固定种群迁移算法不会生成新世代,不易陷局部极值,在有限参数范围内用小代价出结果也值得试。外汇与贵金属杠杆高,回测可接受不等于实盘稳,参数过拟合风险始终在。
<b>MACD(x, nFast = <span class="number">class="num">12</span>, nSlow = <span class="number">class="num">26</span>, nSig = <span class="number">class="num">9</span>, maType, percent = <span class="macro">TRUE</span>, ...)</b> <b>genoud(fn, nvars, max = <span class="macro">FALSE</span>, pop.size = <span class="number">class="num">1000</span>, max.generations = <span class="number">class="num">100</span>, wait.generations = <span class="number">class="num">10</span>, hard.generation.limit = <span class="macro">TRUE</span>, starting.values = <span class="macro">NULL</span>, MemoryMatrix = <span class="macro">TRUE</span>, Domains = <span class="macro">NULL</span>, <span class="keyword">class="kw">default</span>.domains = <span class="number">class="num">10</span>, solution.tolerance = <span class="number">class="num">0.001</span>, gr = <span class="macro">NULL</span>, boundary.enforcement = <span class="number">class="num">0</span>, lexical = <span class="macro">FALSE</span>, gradient.check = <span class="macro">TRUE</span>, BFGS = <span class="macro">TRUE</span>, data.type.<span class="keyword">class="type">int</span> = <span class="macro">FALSE</span>, hessian = <span class="macro">FALSE</span>, unif.seed = <span class="number">class="num">812821</span>, <span class="keyword">class="type">int</span>.seed = <span class="number">class="num">53058</span>, print.level = <span class="number">class="num">2</span>, share.type = <span class="number">class="num">0</span>, instance.number = <span class="number">class="num">0</span>, output.path = <span class="class="type">class="kw">string">"stdout"</span>, output.append = <span class="macro">FALSE</span>, project.path = <span class="macro">NULL</span>, P1 = <span class="number">class="num">50</span>, P2 = <span class="number">class="num">50</span>, P3 = <span class="number">class="num">50</span>, P4 = <span class="number">class="num">50</span>, P5 = <span class="number">class="num">50</span>, P6 = <span class="number">class="num">50</span>, P7 = <span class="number">class="num">50</span>, P8 = <span class="number">class="num">50</span>, P9 = <span class="number">class="num">0</span>, P9mix = <span class="macro">NULL</span>, BFGSburnin = <span class="number">class="num">0</span>, BFGSfn = <span class="macro">NULL</span>, BFGShelp = <span class="macro">NULL</span>, control = list(), optim.method = ifelse(boundary.enforcement < <span class="number">class="num">2</span>, <span class="class="type">class="kw">string">"BFGS"</span>, <span class="class="type">class="kw">string">"L-BFGS-B"</span>), transform = <span class="macro">FALSE</span>, debug = <span class="macro">FALSE</span>, cluster = <span class="macro">FALSE</span>, balance = <span class="macro">FALSE</span>, ...)</b> # 适应度函数-------------------------fitness <- function(param, test = FALSE){ require(TTR) require(magrittr) # 定义变量 x <- pr[param[<span class="number">class="num">1</span>]] nFast <- param[<span class="number">class="num">2</span>] nSlow <- param[<span class="number">class="num">3</span>] nSig <- param[<span class="number">class="num">4</span>] macdType <- MaType[param[<span class="number">class="num">5</span>]] sigType <- MaType[param[<span class="number">class="num">6</span>]] percent <- per[param[<span class="number">class="num">7</span>]] len <- param[<span class="number">class="num">9</span>]*<span class="number">class="num">100</span> # macd的线性限制 <span class="keyword">if</span> (nSlow <= nFast) <span class="keyword">class="kw">return</span>(-Inf) # 计算 macd md <- MACD(x = x, nFast = nFast, nSlow = nSlow, nSig = nSig, percent = TRUE, maType = list(list(macdType), list(macdType), list(sigType))) # 计算 signals 并右移 <span class="number">class="num">1</span> 个柱 sig <- signal(md, param[<span class="number">class="num">8</span>]) %>% Lag() class="macro">#使用 len 长度计算历史余额 bal <- cumsum(tail(sig, len) * tail(price[ ,<span class="class="type">class="kw">string">&class="macro">#x27;CO&class="macro">#x27;</span>], len)) <span class="keyword">if</span>(test) {bal <<- cumsum(tail(sig, len) * tail(price[ ,<span class="class="type">class="kw">string">&class="macro">#x27;CO&class="macro">#x27;</span>], len))} # 计算质量分数 (转换为整数)
◍ 用遗传算法跑通 MACD 参数寻优的骨架
这段 R 脚本把 MACD 类系统的参数寻优拆成了可复现的工程骨架:价格类型给了 4 种(收盘、中位、典型、加权收盘),均线类型用 Hmisc 的 Cs 绑定 SMA/EMA/DEMA/ZLEMA 四种,信号函数支持三种判定逻辑——交叉符号、差分符号、以及差分与方向共振。 搜索域 dom 矩阵写得很实在:快线周期锁在 8–21、慢线 13–54、信号线 3–13,历史长度映射成 300–1000 根 K 线(代码里用 3–10 乘 100)。初始 par 设为 c(2,12,26,9,2,1,1,3,5),相当于从经典 (12,26,9) 附近起跳。 先用 fitnes(par, test=TRUE) 做冒烟测试,返回 K=0 说明目标函数能跑通但没优化收益;plot(bal) 可以把资金曲线拉出来肉眼看。 真正下算用的是 genoud:pop.size=500、max.generation=300、wait.generation=50,跑满 50 代无显著改进就停。外汇与贵金属杠杆高、滑点跳空频繁,这套域里的参数在 EURUSD 15M 回测可能过拟合,上 MT5 前务必换品种重验。 并行部分 puskCluster 用 doParallel 抓满 detectCores() 个核,但 genoud 调用时 cluster=FALSE,真要提速就把这参数翻成 TRUE 并先建好 PSOCK 集群。
require(Hmisc) # 平均的类型 = class="num">4 ------------------------------------------- MaType <- Cs(SMA, EMA, DEMA, ZLEMA) require(dplyr) # 价格类型 = class="num">4 ----------------------------------------------- pr <- transmute(as.data.frame(price), Close = Close, Med = Med, Typ = (High + Low + Close)/class="num">3, WClose = (High + Low + class="num">2*Close)/class="num">4) # 如何计算? per <- c(TRUE, FALSE) # 信号类型 = class="num">3 -------------------------- signal <- function(x, type){ x <- na.omit(x) dx <- diff(x[ ,class="num">1]) %>% na.omit() x <- tail(x, length(dx)) class="kw">switch(type, (x[ ,class="num">1] - x[ ,class="num">2]) %>% sign(), sign(dx), ifelse(sign(dx) == class="num">1 & sign(x[ ,class="num">1]) == class="num">1, class="num">1, ifelse(sign(dx) == -class="num">1 & sign(x[ ,class="num">1]) == -class="num">1,-class="num">1, class="num">0)) ) } # 初始配置--------------------------- par <- c(class="num">2, class="num">12, class="num">26, class="num">9, class="num">2, class="num">1, class="num">1, class="num">3, class="num">5) # 搜索区域-------------------------------------- dom <- matrix(c(class="num">1, class="num">4, # 用于价格类型 class="num">8, class="num">21, # 快速MA周期数 class="num">13, class="num">54, # 慢速MA周期数 class="num">3, class="num">13, # signal MA 周期数 class="num">1, class="num">4, # 快速和慢速MA类型 class="num">1, class="num">4, # 信号 MA 类型 class="num">1, class="num">2, # 百分比类型 class="num">1, class="num">3, # 信号选项 class="num">3,class="num">10), # 历史长度 [class="num">300:class="num">1000] ncol = class="num">2, byrow = TRUE) # 创建用于处理核心的集群 puskCluster<-function(){ library(doParallel) library(foreach) cores<-detectCores() cl<-makePSOCKcluster(cores) registerDoParallel(cl) class="macro">#clusterSetRNGStream(cl) class="kw">return(cl) }
「遗传算法跑出来的参数与耗时对照」
用 genoud 做整数域参数寻优时,适应度(fitness)直接反映策略在历史样本上的综合得分。第一次集群并行跑完 56 代,第 5 代就锁定了最优解,适应度 16.0,九维参数分别是 1 / 14 / 26 / 8 / 4 / 1 / 1 / 1 / 4,总耗时 8 分 13 秒。 把 wait.generation 收紧到 50 再跑一轮,解在第 10 代出现、跑了 61 代终止,适应度掉到 13.0,参数变成 1 / 19 / 20 / 3 / 1 / 2 / 1 / 2 / 4,耗时压缩到 3 分 34 秒——早停机制确实能省时间,但适应度也跟着降了。 关掉集群、hard.generation.limit 设为 TRUE、最多 100 代且 wait.generation=10 的第三轮,适应度回到 15.0,解落在第 3 / 11 / 13 / 3 / 1 / 3 / 2 这组前缀上。外汇与贵金属品种波动跳空多,这组数值仅代表回测样本下的倾向,实盘迁移前建议在 MT5 用同周期数据重跑一遍 genoud 验证。 让小布替你跑这套 把 clusterExport 里的变量名和你 EA 里的全局变量对齐,print.level 在 EA 里设 0;pop.size=500 在 9 维整数空间下偏轻量,想挖更深处可提到 1000 以上,但单机耗时可能翻倍。
cl <- puskCluster() clusterExport(cl, list("price", "pr", "MaType", "par", "dom", "signal", "fitnes", "Lag", "Dig", "per") ) pr.max <- genoud(fitnes, nvars = class="num">9, max = TRUE, pop.size = class="num">500, max.generation = class="num">300, wait.generation = class="num">50, hard.generation.limit = FALSE, starting.values = par, Domains = dom, boundary.enforcement = class="num">1, data.type.class="type">int = TRUE, solution.tolerance = class="num">0.01, cluster = cl, print.level = class="num">2) stopCluster(cl)
遗传算法跑完后的终端输出长什么样
把自组织映射(soma)这类群体优化函数丢进 MT5 配套的 R 桥或外部回测壳里,跑完不会只给你一句「完成」。典型的一屏输出会把每一代的最优染色体权重直接打印出来,比如第 8 代 X[8] 收敛到 1.000000e+00、第 9 代 X[9] 跳到 4.000000e+00,说明搜索前期还在广域试探。 接着会吐出收敛信息:Solution Found Generation 3 代表第 3 代就撞到了满足约束的解,但 Number of Generations Run 14 表示实际跑满 14 代才停。时间戳 Thu Mar 24 13:54:06 2016 加 Total run time : 0 hours 2 minutes and 32 seconds,给你一个硬数据点——这套参数边界下 14 代演化耗时 2 分 32 秒,外汇与贵金属品种上做这类实验前要先想清楚实时性代价。 代码块里 > k 回显 [1] 15,是告诉你当前最佳个体索引落在第 15 号;plot(bal, t="l") 则是把权益曲线按折线画出来,肉眼看回撤比看数字快。soma(costFunction, bounds, options=list(), strategy="all2one", …) 这一行才是真正入口,strategy 选 all2one 意味着所有粒子只跟全局最优做差分,参数敏感性比环形拓扑高,调 bounds 前建议先在小样本历史段验证。
X[ class="num">8] : class="num">1.000000e+00 X[ class="num">9] : class="num">4.000000e+00 Solution Found Generation class="num">3 Number of Generations Run class="num">14 Thu Mar class="num">24 class="num">13:class="num">54:class="num">06 class="num">2016 Total run time : class="num">0 hours class="num">2 minutes and class="num">32 seconds</b> <b>> k [<span class="number">class="num">1</span>] <span class="number">class="num">15</span> > plot(bal, t=<span class="class="type">class="kw">string">"l"</span>)</b> <b>soma(costFunction, bounds, options = list(), strategy = <span class="class="type">class="kw">string">"all2one"</span>, …) </b>
◍ SOMA 与模拟退火的两套参数怎么填
做 EA 参数寻优时,SOMA 把代价函数当成只接受数值向量、返回标量的黑盒:第一个参数是待优化参数组成的向量,返回值是相对代价。bounds 给每个参数设了 min/max 上下墙,options 里塞算法行为,strategy 目前只认 "all2one" 一种迁移模式。 Zelinka (2004) 给的 SOMA 默认值能直接跑:pathLength=3(物种到迁移目标距离,大于 1 才受控)、stepLength=0.11(最小步长,别取能整除距离的值)、perturbationChance=0.1(单步改参概率)、minAbsoluteSep=0(永不因绝对差终止)、MinRelativeSep=0.001、nMigrations=20、populationSize=10(要比参数个数多、且 ≥2)。算法只做最小化,所以适应度函数要取反号再喂进去。 实测一次 SOMA 跑完最佳适应度值到 11,对实盘够用但还有提升空间;特点是快,但结果抖动大,得手调。下面这段是当时控制台的真实输出,第 7 号个体代价 -11,相对代价差 -2.14 跌破阈值直接停: require(soma) x <- soma(fitnes, bounds = list(min=c(1,8,13,3,1,1,1,1,3), max = c(4,21,54,13,4,4,2,3,10)), options = list(minAbsoluteSep = 3, minRelativeSep = -1, nMigrations = 20, populationSize = 20), opp = TRUE)
- INFO: Relative cost separation (-2.14) is below threshold (-1) - stopping
- INFO: Leader is #7, with cost -11
x$population[,7] 四舍五入后得到 1 15 37 9 1 2 1 1 3,这就是可直接回填 MT5 的输入组。 通用模拟退火走 GenSA 包,par 是初值(NULL 时自生成),fn 是待最小化函数,lower/upper 是边界向量,control 管行为。普通复杂度任务它找解很快,想早停就设 threshold.stop(达到期待函数值)、max.time(跑满秒数)、或 max.call(调满次数)。复杂任务才加 maxit 和 temperature。 我们限 max.time=60 秒跑过一轮,适应度值和参数出来后画出的余额线质量指标出乎意料地好且算得快。多目标别用这类单函数包,倾向换 mco 包做多标准优化。外汇和贵金属波动剧烈,这类寻优只降低过拟合概率,实盘仍属高风险,参数组务必在 MT5 策略测试器复核。
require(soma) x <- soma(fitnes, bounds = list(min=c(class="num">1,class="num">8,class="num">13,class="num">3,class="num">1,class="num">1,class="num">1,class="num">1,class="num">3), max = c(class="num">4,class="num">21,class="num">54,class="num">13,class="num">4,class="num">4,class="num">2,class="num">3,class="num">10)), options = list(minAbsoluteSep = class="num">3, minRelativeSep = -class="num">1, nMigrations = class="num">20, populationSize = class="num">20), opp = TRUE) Output level is not set; defaulting to "Info" * INFO: Starting SOMA optimisation * INFO: Relative cost separation(-class="num">2.14) is below threshold(-class="num">1) - stopping * INFO: Leader is #class="num">7, with cost -class="num">11 > x$population[ ,class="num">7] [class="num">1] class="num">1.532332 class="num">15.391757 class="num">37.348099 class="num">9.860676 class="num">1.918848 [class="num">6] class="num">2.222211 class="num">1.002087 class="num">1.182209 class="num">3.288627 Round to > x$population[ ,class="num">7] %>% floor [class="num">1] class="num">1 class="num">15 class="num">37 class="num">9 class="num">1 class="num">2 class="num">1 class="num">1 class="num">3 GenSA(par, fn, lower, upper, control=list(), …) require(GenSA) pr.max <- GenSA(par, fitnes, lower = c(class="num">1,class="num">8,class="num">13,class="num">3,class="num">1,class="num">1,class="num">1,class="num">1,class="num">3), upper = c(class="num">4,class="num">21,class="num">54,class="num">13,class="num">4,class="num">4,class="num">2,class="num">3,class="num">10),
「把优化参数落地成整数再跑一遍」
上面这段 R 控制台输出,是把差分进化找到的最优解做一次取整后重新回测的过程。pr.max$value 乘 -1 得到 16,说明该组连续参数在样本内的最大回撤深度约为 16 个单位净值,而 pr.max$par 原始浮点值为 1.789901、14.992866、43.854988、5.714345、1.843307、1.979723、1.324855、2.639683、3.166084。 用 floor 直接向下取整后变成 1 14 43 5 1 1 1 2 3,这一步很关键:MT5 里很多指标周期和阈值只吃整数,浮点参数看着漂亮但实盘根本填不进去。 重新调用 fitnes(par1, test=TRUE) 并用 plot(-1*bal,type="l") 画出资金曲线,你能在 R 里直观看到取整后曲线是否还贴着原最优路径。若偏离不大,这组 1 14 43 5 1 1 1 2 3 就可以原样抄进 MT5 的 input 数组做验证。外汇与贵金属杠杆高,样本内回撤 16 不代表实盘可控,上 MT5 用至少三个月 Tick 重跑再定。
control = list(verbose = TRUE, simple.function = TRUE, max.time = class="num">60), opp = TRUE) > pr.max$value * (-class="num">1) [class="num">1] class="num">16 > par1 <- pr.max$par > par1 [class="num">1] class="num">1.789901 class="num">14.992866 class="num">43.854988 class="num">5.714345 class="num">1.843307 [class="num">6] class="num">1.979723 class="num">1.324855 class="num">2.639683 class="num">3.166084 > par1 <- pr.max$par %>% floor [class="num">1] class="num">1 class="num">14 class="num">43 class="num">5 class="num">1 class="num">1 class="num">1 class="num">2 class="num">3 > f1 <- fitnes(par1, test = TRUE) > plot(-class="num">1 * bal, t="l")
把 GA 指标再榨一层油
前面几轮遗传算法跑下来,已经能看出它在找好指标上的效率,但产出的质量还有往上推的空间。 直接在 MT5 里多开几组探索比手动调参更划算。比如用多标准优化,同时盯质量分数和最大回撤,MQL5 的 "mco" 封包就干这事,能避免只优化收益却炸了回撤。 再进一步,可以试动态自组织的 GA 参数,"GA" 封包给了大范围的选择、交叉、变异算子,理论上能自己调自己。把它接进交易系统做遗传编程测试,可能比固定参数更抗不同行情。外汇和贵金属波动大、杠杆高,这类自优化模型也可能在极端周吃瘪,上实盘前先用历史数据复算一遍。
◍ 把适应度函数写对就够用了
前面用 MACDSample EA 跑过一轮参数优化,即便只在最基础的交易逻辑上动参数,遗传算法也能在不开市的情况下搜出更合适的组合,说明这套思路的门槛没想象中高。 进化算法里类型可以混用、阶段可以切分,但真正决定优化质量的只有一件事:适应度函数是否如实刻画了你想要的收益与风险结构。 外汇和贵金属杠杆高、滑点跳空频繁,直接把单笔利润当适应度很容易过拟合历史。打开 MT5 把自己的 EA 改一版适应度函数,用策略测试器跑不同优化类型对比曲线,比反复调阈值更有用。