神经网络变得轻松(第三十九部分):Go-Explore,一种不同的探索方式·进阶篇
(2/3)·当好奇心与分歧奖励在复杂惩罚环境中失效,回归有前途状态的算法如何另辟蹊径
◍ 多指标句柄初始化与历史基库装载
EA 启动时先把品种对象、RSI、CCI、ATR、MACD 全部建一遍句柄,任何一步失败直接返回 INIT_FAILED,避免后续 Tick 里调用空句柄导致崩策略。 品种校验用 Symb.Name(_Symbol) 和 Refresh(),RSI 最少要传周期与价格类型,MACD 则是快/慢/信号三根线加价格源,参数缺一个都不给过。 四个指标缓冲区统一按 HistoryBars resize,若有一个 resize 不成功就打印函数名和行号并退出,方便你直接在 MT5 编译器里跳转到那一行查内存分配问题。 LoadTotalBase() 从公共目录读 .bd 二进制文件,先读总条数 total,再循环把每条记录 Load 进 Total[] 数组;若文件打不开或 total<=0 就返回 false,意味着这次启动不加载任何历史行为基库。 启动后 OnTick 里靠 IsNewBar() 拦掉同根 K 线的重复触发,bar 计数小于 StartCell.total_actions 时按预设动作数组走,比如 case 0 就是 Trade.Buy 用 Symb.LotsMin() 下最小手数——外汇和贵金属杠杆高,最小手也可能在极端波动里放大亏损,实盘前务必在策略测试器跑过。
{
class=class="str">"cmt">//---
if(!Symb.Name(_Symbol))
class="kw">return INIT_FAILED;
Symb.Refresh();
class=class="str">"cmt">//---
if(!RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
class="kw">return INIT_FAILED;
class=class="str">"cmt">//---
if(!CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
class="kw">return INIT_FAILED;
class=class="str">"cmt">//---
if(!ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
class="kw">return INIT_FAILED;
class=class="str">"cmt">//---
if(!MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
class="kw">return INIT_FAILED;
if(!RSI.BufferResize(HistoryBars) || !CCI.BufferResize(HistoryBars) ||
!ATR.BufferResize(HistoryBars) || !MACD.BufferResize(HistoryBars))
{
PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
class="kw">return INIT_FAILED;
}
class=class="str">"cmt">//---
if(!Trade.SetTypeFillingBySymbol(Symb.Name()))
class="kw">return INIT_FAILED;
class=class="str">"cmt">//---
if(LoadTotalBase())
{
class="type">int total = ArraySize(Total);
if(total > Start)
StartCell = Total[Start];
else
{
total = (class="type">int)(((class="type">class="kw">double)MathRand() / class="num">32768.0) * (total - class="num">1));
StartCell = Total[total];
}
}
class=class="str">"cmt">//---
class="kw">return(INIT_SUCCEEDED);
}
class="type">bool LoadTotalBase(class="type">void)
{
class="type">int handle = FileOpen(FileName + ".bd", FILE_READ | FILE_BIN | FILE_COMMON);
if(handle < class="num">0)
class="kw">return class="kw">false;
class="type">int total = FileReadInteger(handle);
if(total <= class="num">0)
{
FileClose(handle);
class="kw">return class="kw">false;
}
if(ArrayResize(Total, total) < total)
{
FileClose(handle);
class="kw">return class="kw">false;
}
for(class="type">int i = class="num">0; i < total; i++)
if(!Total[i].Load(handle))
{
FileClose(handle);
class="kw">return class="kw">false;
}
FileClose(handle);
class=class="str">"cmt">//---
class="kw">return true;
}
class="type">void OnTick()
{
class=class="str">"cmt">//---
if(!IsNewBar())
class="kw">return;
bar++;
if(bar < StartCell.total_actions)
{
class="kw">switch(StartCell.actions[bar])
{
case class="num">0:
Trade.Buy(Symb.LotsMin(), Symb.Name());「把多周期行情压成 249 维状态向量」
这段逻辑干的事很直接:先把 RSI、CCI、ATR、MACD 四个指标逐一 Refresh,再按 HistoryBars 根 K 线把开高低收、tick 量、时间分量与指标值铺进一个 float state[249] 数组。每根 bar 占 12 个槽位,所以数组上限 249 意味着大约能装 20 根出头的完整状态(249/12≈20.75),超出的部分在循环里根本写不进去。 循环里先 TimeToStruct 拆出 hour / day_of_week / mon,再抓四个指标主线和 MACD 信号线;只要任意一个返回 EMPTY_VALUE 就 continue 跳过该 bar,避免脏数据进向量。close-open、high-open、low-open 全以开盘价为基准做差分,tick_volume 则除以 1000.0f 压缩量级——黄金和外汇这种成交量跳变大的品种,不归一化很容易让后续模型被量纲带偏。 state 的下标用 b*12 偏移布局,b 是 bar 序号、后面 0~11 是特征通道。你开 MT5 把 HistoryBars 调到 50 会立刻数组越界报错,想扩容量就把 249 改成 HistoryBars*12 再预留余量。外汇贵金属杠杆高、滑点随机,这套状态工程只是特征准备,信号胜率仍随品种和时段波动。
class="kw">break; case class="num">1: Trade.Sell(Symb.LotsMin(), Symb.Name()); class="kw">break; case class="num">2: for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) if(PositionGetSymbol(i) == Symb.Name()) Trade.PositionClose(PositionGetInteger(POSITION_IDENTIFIER)); class="kw">break; } class="kw">return; } if(bar == StartCell.total_actions) ArrayCopy(actions, StartCell.actions, class="num">0, class="num">0, StartCell.total_actions); class="type">int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, class="num">1), HistoryBars, Rates); if(!ArraySetAsSeries(Rates, true)) class="kw">return; class=class="str">"cmt">//--- RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); class="type">class="kw">float state[class="num">249]; class="type">MqlDateTime sTime; for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++) { class="type">class="kw">float open = (class="type">class="kw">float)Rates[b].open; TimeToStruct(Rates[b].time, sTime); class="type">class="kw">float rsi = (class="type">class="kw">float)RSI.Main(b); class="type">class="kw">float cci = (class="type">class="kw">float)CCI.Main(b); class="type">class="kw">float atr = (class="type">class="kw">float)ATR.Main(b); class="type">class="kw">float macd = (class="type">class="kw">float)MACD.Main(b); class="type">class="kw">float sign = (class="type">class="kw">float)MACD.Signal(b); if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) class="kw">continue; class=class="str">"cmt">//--- state[b * class="num">12] = (class="type">class="kw">float)Rates[b].close - open; state[b * class="num">12 + class="num">1] = (class="type">class="kw">float)Rates[b].high - open; state[b * class="num">12 + class="num">2] = (class="type">class="kw">float)Rates[b].low - open; state[b * class="num">12 + class="num">3] = (class="type">class="kw">float)Rates[b].tick_volume / class="num">1000.0f; state[b * class="num">12 + class="num">4] = (class="type">class="kw">float)sTime.hour; state[b * class="num">12 + class="num">5] = (class="type">class="kw">float)sTime.day_of_week; state[b * class="num">12 + class="num">6] = (class="type">class="kw">float)sTime.mon; state[b * class="num">12 + class="num">7] = rsi; state[b * class="num">12 + class="num">8] = cci; state[b * class="num">12 + class="num">9] = atr; state[b * class="num">12 + class="num">10] = macd; state[b * class="num">12 + class="num">11] = sign; } class=class="str">"cmt">//---
把账户与持仓塞进状态数组再让智能体下单
这段逻辑把交易环境的实时快照写进一个偏移量为 240 的浮点数组,给后续的强化学习智能体当观测特征。前 5 个槽位依次存了余额、净值、可用保证金、保证金水平百分比和浮动盈亏,都是 AccountInfoDouble 直接拉取的账户级数据。 接着用 PositionsTotal 遍历当前所有持仓,只筛本品种 Symb.Name() 的仓位,把多空 volume 与 profit 分别累加进 state[245]~state[248]。如果你在 MT5 里同时挂了黄金和欧美,这段不会把跨品种手数混进来,回测时这点能避免特征污染。 action 由 SampleAction(4) 采样得到,0 买、1 卖、2 平掉本品种全部持仓,下单量统一用 Symb.LotsMin() 最小手数——外汇和贵金属杠杆高,最小手数也足以让回测曲线剧烈波动,实盘前务必在策略测试器里跑一遍。 最后把本次 action 追加进 actions 数组,并通过 ArrayCopy 把历史动作链拷给新 cell 的 Base 结构,让决策带一点记忆。下面这段是原文核心代码,逐行拆解见注释。
state[class="num">240] = (class="type">class="kw">float)AccountInfoDouble(ACCOUNT_BALANCE); state[class="num">240 + class="num">1] = (class="type">class="kw">float)AccountInfoDouble(ACCOUNT_EQUITY); state[class="num">240 + class="num">2] = (class="type">class="kw">float)AccountInfoDouble(ACCOUNT_MARGIN_FREE); state[class="num">240 + class="num">3] = (class="type">class="kw">float)AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); state[class="num">240 + class="num">4] = (class="type">class="kw">float)AccountInfoDouble(ACCOUNT_PROFIT); class=class="str">"cmt">//--- class="type">class="kw">double buy_value = class="num">0, sell_value = class="num">0, buy_profit = class="num">0, sell_profit = class="num">0; class="type">int total = PositionsTotal(); for(class="type">int i = class="num">0; i < total; i++) { if(PositionGetSymbol(i) != Symb.Name()) class="kw">continue; class="kw">switch((class="type">int)PositionGetInteger(POSITION_TYPE)) { case POSITION_TYPE_BUY: buy_value += PositionGetDouble(POSITION_VOLUME); buy_profit += PositionGetDouble(POSITION_PROFIT); class="kw">break; case POSITION_TYPE_SELL: sell_value += PositionGetDouble(POSITION_VOLUME); sell_profit += PositionGetDouble(POSITION_PROFIT); class="kw">break; } } state[class="num">240 + class="num">5] = (class="type">class="kw">float)buy_value; state[class="num">240 + class="num">6] = (class="type">class="kw">float)sell_value; state[class="num">240 + class="num">7] = (class="type">class="kw">float)buy_profit; state[class="num">240 + class="num">8] = (class="type">class="kw">float)sell_profit; class=class="str">"cmt">//--- class="type">int act = SampleAction(class="num">4); class="kw">switch(act) { case class="num">0: Trade.Buy(Symb.LotsMin(), Symb.Name()); class="kw">break; case class="num">1: Trade.Sell(Symb.LotsMin(), Symb.Name()); class="kw">break; case class="num">2: for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) if(PositionGetSymbol(i) == Symb.Name()) Trade.PositionClose(PositionGetInteger(POSITION_IDENTIFIER)); class="kw">break; } class=class="str">"cmt">//--- copy cell actions[action_count] = act; Base[action_count].total_actions = action_count+StartCell.total_actions; if(action_count > class="num">0) { ArrayCopy(Base[action_count].actions, actions, class="num">0, class="num">0, Base[action_count].total_actions+class="num">1);
◍ 回测框架里怎么把动作序列捞出来
在 MT5 多品种、多参数优化里,光看最终盈利不够,得把每一组参数跑出来的动作序列存下来。上面这段把 OnTester 到 OnTesterDeinit 的钩子串起来了:盈利大于 0 的 pass 才用 FrameAdd 把 Base 数组(含 241 号状态差值等)推给框架,亏损 pass 直接丢弃。 OnTesterInit 里先 LoadTotalBase() 把历史底库读进内存;OnTesterPass 用 FrameNext 遍历所有 frame,只认程序名匹配且 id>0 的,用 ArrayResize(Total, total+(int)id, 10000) 以 1 万为步长扩容,再 ArrayCopy 拼进 Total。这意味着单次优化最多可累积数万条动作记录而不必频繁重分配。 OnTesterDeinit 打印 total 条数并进入 do-while 冒泡排序,flag 初始 false,每轮若有交换才置 true 继续。外汇与贵金属市场高杠杆、滑点诡异,这套序列排序后只是给你做样本筛选,不表示任何未来收益倾向。开 MT5 把 STAT_PROFIT 阈值改成你关心的回撤比,能立刻看到保留的 pass 数量变化。
Base[action_count - class="num">1].value = Base[action_count - class="num">1].state[class="num">241] - state[class="num">241]; } ArrayCopy(Base[action_count].state, state, class="num">0, class="num">0); class=class="str">"cmt">//--- action_count++; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Tester function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester() { class=class="str">"cmt">//--- class="type">class="kw">double ret = class="num">0.0; class=class="str">"cmt">//--- class="type">class="kw">double profit = TesterStatistics(STAT_PROFIT); action_count--; if(profit > class="num">0) FrameAdd(MQLInfoString(MQL_PROGRAM_NAME), action_count, profit, Base); class=class="str">"cmt">//--- class="kw">return(ret); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterInit function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterInit() { class=class="str">"cmt">//--- LoadTotalBase(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterPass function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterPass() { class=class="str">"cmt">//--- class="type">class="kw">ulong pass; class="type">class="kw">string name; class="type">long id; class="type">class="kw">double value; Cell array[]; class="kw">while(FrameNext(pass, name, id, value, array)) { class="type">int total = ArraySize(Total); if(name != MQLInfoString(MQL_PROGRAM_NAME)) class="kw">continue; if(id <= class="num">0) class="kw">continue; if(ArrayResize(Total, total + (class="type">int)id, class="num">10000) < class="num">0) class="kw">return; ArrayCopy(Total, array, total, class="num">0, (class="type">int)id); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterDeinit function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterDeinit() { class=class="str">"cmt">//--- class="type">bool flag = class="kw">false; class="type">int total = ArraySize(Total); printf("total %d", total); Cell temp; Print("Start sorting..."); do { flag = class="kw">false;
「排序落盘与神经网络冷启动」
这段逻辑先把 Total 数组按 value 做冒泡降序:外层 do-while 靠 flag 控制是否发生交换,内层 for 从 0 跑到 total-2,只要 Total[i].value 小于后一项就互换并置 flag=true。排序完 Print("Saving...") 后调 SaveTotalBase() 写盘,再 Print("Saved") 收尾,整个动作在 EA 初始化或周期收尾时跑一次即可。 初始化里先 LoadTotalBase() 读底座数据,失败直接 INIT_FAILED;接着 StudyNet.Load 尝试加载 FileName+".nnw",若文件不存在就 new CArrayObj 调 CreateDescriptions 搭网络描述,再 StudyNet.Create 建模型,任何一步出错都删 model 并返回失败。 input int Iterations = 100000 是训练迭代上限,StudyNet.TrainMode(true) 打开训练态,失败同样 INIT_FAILED。外汇与贵金属波动剧烈、杠杆风险高,这套 RL 底座在实盘前务必用 MT5 策略测试器以最小手数验证收敛与过拟合倾向。 让小布替你跑这套:把 Cell.mqh 与 ..\RL\FQF.mqh 路径确认无误后,改 Iterations 到 1000 先跑通 OnInit 不报 INIT_FAILED,再逐步加压看 dError 变化。
for(class="type">int i = class="num">0; i < (total - class="num">1); i++) if(Total[i].value < Total[i + class="num">1].value) { temp = Total[i]; Total[i] = Total[i + class="num">1]; Total[i + class="num">1] = temp; flag = true; } } class="kw">while(flag); Print("Saving..."); SaveTotalBase(); Print("Saved"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Includes | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Cell.mqh" class="macro">#include "..\RL\FQF.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Input parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">int Iterations = class="num">100000; CNet StudyNet; class=class="str">"cmt">//--- class="type">class="kw">float dError; class="type">class="kw">datetime dtStudied; class="type">bool bEventStudy; class=class="str">"cmt">//--- CBufferFloat State1; CBufferFloat *Rewards; Cell Base[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- if(!LoadTotalBase()) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- if(!StudyNet.Load(FileName + ".nnw", dError, dError, dError, dtStudied, true)) { CArrayObj *model = new CArrayObj(); if(!CreateDescriptions(model)) { class="kw">delete model; class="kw">return INIT_FAILED; } if(!StudyNet.Create(model)) { class="kw">delete model; class="kw">return INIT_FAILED; } class="kw">delete model; } if(!StudyNet.TrainMode(true)) class="kw">return INIT_FAILED; class=class="str">"cmt">//---
训练循环怎样被图表事件触发
EA 初始化末尾用 EventChartCustom 往当前图表推了一条自定义事件,字符串参数填 "Init",返回值赋给 bEventStudy 仅作标记;随后 return(INIT_SUCCEEDED) 让初始化干净退出,不阻塞主线程。 真正的训练入口躲在 OnChartEvent 里:当 id == 1001(自定义事件 ID)时直接调 Train()。也就是说,你拖 EA 上图后,若没手动或代码触发 1001 事件,Train() 永远不会跑,这是很多新手在 MT5 里发现「网络不学习」的首因。 Train() 内部先用 ArraySize(Base) 拿到样本总数,再用 GetTickCount() 记起点。循环条件是 iter < Iterations 且 !IsStopped(),每轮先用两次 MathRand() 平方再除以 32767² 做非线性抽样选样本下标 i——这种写法让小下标被抽中的概率略高,和纯均匀随机不同,调参时可留意偏差。 抽样后把 Base[i].state 灌进 State1,若中途 IsStopped() 为真就打印函数行号并 ExpertRemove() 自退。feedForward 隐藏层节点写死 12,backProp 用 -Base[i].value 当奖励更新信号,说明把原本的「值」取负作为误差方向。 循环里每过 500 毫秒(GetTickCount()-ticks > 500)会切一次节奏,避免单线程训练卡死图表交互;外汇与贵金属杠杆高,这类自学习 EA 过拟合历史样本后实盘失效概率偏大,上 MT5 验证前先缩小 Iterations 到千级看耗时。
bEventStudy = EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init"); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- if(id == class="num">1001) Train(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Train function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Train(class="type">void) { class="type">int total = ArraySize(Base); class="type">uint ticks = GetTickCount(); for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped()); iter ++) { class="type">int i = class="num">0; class="type">int count = class="num">0; class="type">int total_max = class="num">0; i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (total - class="num">1)); State1.AssignArray(Base[i].state); if(IsStopped()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); class="kw">return; } if(!StudyNet.feedForward(GetPointer(State1), class="num">12, true)) class="kw">return; class="type">int action = Base[i].total_actions; if(action < class="num">0) { iter--; class="kw">continue; } action = Base[i].actions[action]; if(action < class="num">0 || action > class="num">3) action = class="num">3; StudyNet.getResults(Rewards); if(!Rewards.Update(action, -Base[i].value)) class="kw">return; if(!StudyNet.backProp(GetPointer(Rewards))) class="kw">return; if(GetTickCount() - ticks > class="num">500) {