交易中的神经网络:具有层化记忆的智代(终篇)·进阶篇
「给强化学习模型挂上 ADAM 与 Rho」
在 MT5 的自定义智能交易系统里,给 actor 网络描述符注入优化器与概率度量,是训练前必须落地的两步。上面这段片段把 descr 的 optimization 设成 ADAM,probability 设成 Rho,再尝试 Add 进 actor;若插入失败就释放内存并返回 false,成功才返回 true。 从工程角度看,ADAM 作为自适应矩估计优化器,在策略梯度类任务里收敛稳定性通常优于朴素 SGD;Rho 在这里作为概率相关字段,可能影响探索与利用的权衡。外汇与贵金属市场高杠杆、高波动,这类参数组合仅代表一种技术配置,实盘表现倾向受样本外数据分布偏移影响。 开 MT5 后,把这段代码嵌进你的 CExpert 或自写训练壳里,先跑 2020—2023 年 XAUUSD 的 M15 历史数据做离线校验,观察 actor.Add 返回 false 的频率——若高于 5%,大概率是你 descr 内存管理或字段赋值顺序有问题。
descr.optimization = ADAM; descr.probability = Rho; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; }
FinMem 训练循环在 MT5 里的落地细节
训练方法 Train 的起点很传统:从经验回放缓冲区按历史运行盈利性生成轨迹选择概率向量,再声明局部变量。重复模型对输入顺序敏感,所以必须用外环抽轨迹、内环沿轨迹走状态的嵌套循环,迭代次数和批次规模由外部参数定。 每换一条新轨迹前必须清掉模型记忆,否则存储数据会和当前环境错位。内环里先取环境状态描述并构造账户状态向量——不是直接搬运缓冲区,而是让模型学习智代前一步动作对财务结果的影响,这步无法简单转送。 回报计算做了极度简化:不考虑止损止盈触发、佣金,假设前持仓在最后一次操作前已平仓。回报 = 最后一根柱线价格变化(收减开,阳线为正)乘以买卖交易量差值,两个带符号值相乘即得带符号结果。这只是粗略性能评估,实盘前务必补齐市场细节,外汇和贵金属杠杆高、滑点跳空频繁,简化假设会严重失真。 目标值用「展望未来」方式构造:从缓冲区取计划时段数据排成时间序矩阵,第一列累计求和得各步总体价变(忽略间隙,实盘不可接受)。若有持仓就找离场点,无持仓则判盛行趋势按每 100 美元余额最低手数开仓。反向传播把预测动作和近乎理想目标间的偏差最小化。 下面这段是 Train 方法开头的核心代码,可见概率采样与局部变量声明: void Train(void) { //--- vector<float> probability = GetProbTrajectories(Buffer, 0.9); //--- vector<float> result, target, state; matrix<float> fstate = matrix<float>::Zeros(1, NForecast * BarDescr); bool Stop = false; for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch) { int tr = SampleTrajectory(probability); int start = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast - Batch)); if(start <= 0) { iter -= Batch; continue; } if(!Actor.Clear()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } for(int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++) {
| if(!state.Assign(Buffer[tr].States[i].state) |
|---|
| MathAbs(state).Sum() == 0 |
!bState.AssignArray(state)) { iter -= Batch + start - i; break; } bTime.Clear(); 外层循环里 SampleTrajectory 按 0.9 衰减概率抽轨迹,start 用两次 MathRand 平方归一防聚簇;Actor.Clear() 失败直接打断并置 Stop。内环遇到状态向量全零或赋值失败就回退迭代计数跳出,保证残缺数据不进前馈。开 MT5 把这段贴进 Experts\FinMem\Study.mq5 改 Iterations 和 Batch,能直接看训练进度注释栏输出。
class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">class="kw">float> probability = GetProbTrajectories(Buffer, class="num">0.9); class=class="str">"cmt">//--- vector<class="type">class="kw">float> result, target, state; matrix<class="type">class="kw">float> fstate = matrix<class="type">class="kw">float>::Zeros(class="num">1, NForecast * BarDescr); class="type">bool Stop = false; for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch) { class="type">int tr = SampleTrajectory(probability); class="type">int start = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 - NForecast - Batch)); if(start <= class="num">0) { iter -= Batch; class="kw">continue; } if(!Actor.Clear()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } for(class="type">int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++) { if(!state.Assign(Buffer[tr].States[i].state) || MathAbs(state).Sum() == class="num">0 || !bState.AssignArray(state)) { iter -= Batch + start - i; class="kw">break; } bTime.Clear();
◍ 把账户时间轴喂给神经层的写法
这段逻辑在做一件事:把时间戳转成周期相关的正弦/余弦分量,再拼进账户特征向量里送进前向传播。外汇与贵金属市场高杠杆、跳空频繁,这类特征工程只是给模型提供周期背景,不预示任何方向。 先取账户状态里的第 7 号字段当作时间值,用 2023.01.01 到 2024.01.01 的秒数做归一,算出年周期 sin 分量;随后分别除以月线、周线、日线的 PeriodSeconds,得到月/周/日周期的 cos 或 sin 分量,一共 4 个时间特征压进 bTime。 账户侧先抓上一根柱的余额与净值(索引用 MathMax(i-1,0) 防越界),算出权益相对余额的偏离、加利润后的权益比、利润占权益比,以及多头/空头最大浮盈这类 0 截断量,最后把 bTime 整个数组挂到 bAccount 后面。 feedForward 若返回失败就打印函数名加行号、置 Stop 并 break——实盘跑 EA 时建议把这条 PrintFormat 接上邮件或推送,不然静默断流你很难察觉。
class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7]; class="type">class="kw">double x = time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;); bTime.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1); bTime.Add((class="type">class="kw">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1); bTime.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1); bTime.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); if(bTime.GetIndex() >= class="num">0) bTime.BufferWrite(); class=class="str">"cmt">//--- Previous Action Actor.getResults(result); class="type">class="kw">float profit = class="type">class="kw">float(bState[class="num">0] / (_Point * class="num">10) * (result[class="num">0] - result[class="num">3])); class=class="str">"cmt">//--- Account class="type">class="kw">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0]; class="type">class="kw">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1]; bAccount.Clear(); bAccount.Add((PrevEquity - PrevBalance) / PrevBalance); bAccount.Add((PrevEquity + profit) / PrevEquity); bAccount.Add(profit / PrevEquity); bAccount.Add(MathMax(result[class="num">0] - result[class="num">3], class="num">0)); bAccount.Add(MathMax(result[class="num">3] - result[class="num">0], class="num">0)); bAccount.Add((bAccount[class="num">3]>class="num">0 ? profit / PrevBalance : class="num">0)); bAccount.Add((bAccount[class="num">4]>class="num">0 ? profit / PrevBalance : class="num">0)); bAccount.Add(class="num">0); bAccount.AddArray(GetPointer(bTime)); if(bAccount.GetIndex() >= class="num">0) bAccount.BufferWrite(); class=class="str">"cmt">//--- Feed Forward if(!Actor.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, GetPointer(bAccount))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } class=class="str">"cmt">//--- Look for target
「训练样本里状态归零就直接跳批」
在强化学习训练循环里,每取一条样本都要先把动作向量清零:target 用 Zeros(NActions) 初始化,再 AssignArray 给 bActions,避免上一轮的残值污染本次梯度。 如果待预测状态 state 取不到、Resize 到 NForecast*BarDescr 失败、或绝对值求和等于 0(即全零状态),代码会回退 iter 并 break 跳出,相当于这条样本废掉、不计入 Batch。外汇与贵金属行情里全零状态多出现在休市或数据断层段,高杠杆下用脏样本训练可能放大过拟合风险。 下面这段把 state 重排成 fstate 矩阵并做行翻转,再取第 0 列累加得到 target;若 result[0] 大于 result[3],进入多单分支,按 MaxSL*Point() 算动态止损线 cur_sl,遍历 NForecast 步找 tp 与 sl。 [CODE] 逐行拆解: target = vector<float>::Zeros(NActions); // 建长度 NActions 的全零浮点向量 bActions.AssignArray(target); // 把全零向量写进动作缓冲
| if(!state.Assign(Buffer[tr].States[i + NForecast].state) | // 取未来状态失败 |
|---|---|
| !state.Resize(NForecast * BarDescr) | // 拉成 NForecast*BarDescr 失败 |
MathAbs(state).Sum() == 0) // 状态全零 { iter -= Batch + start - i; break; } // 回退 iter 并跳出
| if(!fstate.Resize(1, NForecast * BarDescr) | // 建单行矩阵失败 |
|---|---|
| !fstate.Row(state, 0) | // 把 state 塞进第 0 行失败 |
!fstate.Reshape(NForecast, BarDescr)) // 重塑成 NForecast 行失败 { iter -= Batch + start - i; break; } // 同样回退跳出 for(int i = 0; i < NForecast / 2; i++) // 前半行与对称行交换 { if(!fstate.SwapRows(i, NForecast - i - 1)) // 交换失败 { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); // 打印函数与行号 Stop = true; break; } } // 置停止标记并跳出 target = fstate.Col(0).CumSum(); // 第 0 列累加赋给 target if(result[0] > result[3]) // 多单条件 { float tp = 0; float sl = 0; // 初始化止盈止损 float cur_sl = float(-(result[2] > 0 ? result[2] : 1) * MaxSL * Point()); // 动态止损价 int pos = 0; for(int i = 0; i < NForecast; i++) // 逐步扫描 { tp = MathMax(tp, target[i] + fstate[i,1] - fstate[i,0]); // 更新最大 tp pos = i; if(cur_sl >= target[i] + fstate[i,2] - fstate[i,0]) break; // 触止损线退出 sl = MathMin(sl, target[i] + fstate[i,2] - fstate[i,0]); } // 更新最小 sl if(tp > 0) // 有正止盈才归一化 { sl = float(MathMin(MathAbs(sl) / (MaxSL * Point()), 1)); // sl 归一 tp = float(MathMin(tp / (MaxTP * Point()), 1)); } } // tp 归一
target = vector<class="type">class="kw">float>::Zeros(NActions); bActions.AssignArray(target); if(!state.Assign(Buffer[tr].States[i + NForecast].state) || !state.Resize(NForecast * BarDescr) || MathAbs(state).Sum() == class="num">0) { iter -= Batch + start - i; class="kw">break; } if(!fstate.Resize(class="num">1, NForecast * BarDescr) || !fstate.Row(state, class="num">0) || !fstate.Reshape(NForecast, BarDescr)) { iter -= Batch + start - i; class="kw">break; } for(class="type">int i = class="num">0; i < NForecast / class="num">2; i++) { if(!fstate.SwapRows(i, NForecast - i - class="num">1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } } target = fstate.Col(class="num">0).CumSum(); if(result[class="num">0] > result[class="num">3]) { class="type">class="kw">float tp = class="num">0; class="type">class="kw">float sl = class="num">0; class="type">class="kw">float cur_sl = class="type">class="kw">float(-(result[class="num">2] > class="num">0 ? result[class="num">2] : class="num">1) * MaxSL * Point()); class="type">int pos = class="num">0; for(class="type">int i = class="num">0; i < NForecast; i++) { tp = MathMax(tp, target[i] + fstate[i, class="num">1] - fstate[i, class="num">0]); pos = i; if(cur_sl >= target[i] + fstate[i, class="num">2] - fstate[i, class="num">0]) class="kw">break; sl = MathMin(sl, target[i] + fstate[i, class="num">2] - fstate[i, class="num">0]); } if(tp > class="num">0) { sl = class="type">class="kw">float(MathMin(MathAbs(sl) / (MaxSL * Point()), class="num">1)); tp = class="type">class="kw">float(MathMin(tp / (MaxTP * Point()), class="num">1));
空头情境下的止损止盈反推逻辑
当预测方向偏弱、result[0] 小于 result[3] 时,引擎进入空头分支,重新计算反向的 tp 与 sl。 cur_sl 以 MaxSL * Point() 为上限,若 result[5] 大于 0 则取其值,否则取 1,作为当前允许的最大止损距离。 循环遍历 NForecast 根预测线:tp 取各根 target[i]+fstate[i,2]-fstate[i,0] 的最小值;只要 cur_sl 不超过 target[i]+fstate[i,1]-fstate[i,0] 就 break,否则 sl 刷新为这些值的最大值。 若最终 tp < 0,说明空头空间成立:sl 归一化为 MathMin(MathAbs(sl)/(MaxSL*Point()),1),tp 归一化为 MathMin(-tp/(MaxTP*Point()),1),result[3] 取 MathMax(result[3]-result[0],0.01f) 防止归零,前三个动作位清 0 后写回 bActions。外汇与贵金属杠杆高,该分支仅描述算法行为,实盘触发概率随品种波动而变。
result[class="num">0] = MathMax(result[class="num">0] - result[class="num">3], class="num">0.01f); result[class="num">1] = tp; result[class="num">2] = sl; for(class="type">int i = class="num">3; i < NActions; i++) result[i] = class="num">0; bActions.AssignArray(result); } } else { if(result[class="num">0] < result[class="num">3]) { class="type">class="kw">float tp = class="num">0; class="type">class="kw">float sl = class="num">0; class="type">class="kw">float cur_sl = class="type">class="kw">float((result[class="num">5] > class="num">0 ? result[class="num">5] : class="num">1) * MaxSL * Point()); class="type">int pos = class="num">0; for(class="type">int i = class="num">0; i < NForecast; i++) { tp = MathMin(tp, target[i] + fstate[i, class="num">2] - fstate[i, class="num">0]); pos = i; if(cur_sl <= target[i] + fstate[i, class="num">1] - fstate[i, class="num">0]) class="kw">break; sl = MathMax(sl, target[i] + fstate[i, class="num">1] - fstate[i, class="num">0]); } if(tp < class="num">0) { sl = class="type">class="kw">float(MathMin(MathAbs(sl) / (MaxSL * Point()), class="num">1)); tp = class="type">class="kw">float(MathMin(-tp / (MaxTP * Point()), class="num">1)); result[class="num">3] = MathMax(result[class="num">3] - result[class="num">0], class="num">0.01f); result[class="num">4] = tp; result[class="num">5] = sl; for(class="type">int i = class="num">0; i < class="num">3; i++) result[i] = class="num">0; bActions.AssignArray(result); } }
◍ 截断尾部噪声后落袋盈亏比
上面那段逻辑干的事,是先砍掉 target 数组里最极端的峰谷干扰,再在残留序列里找可成交的 TP / SL 边界。 代码逐行看:argmin 与 argmax 取出当前 target 数组的最小、最大下标;while 循环里只要极值下标都大于 0,就判断最大正值是否压过最小负值的绝对值,若满足方向条件直接 break,否则把数组截到较小极值处重算,等于一层层剥掉尾部异常摆动。 进入 argmin==0 或 argmax<argmin 的分支后,cur_sl 以 MaxSL*Point() 取负作为硬下限,for 循环遍历到 argmax 之前,用 fstate 的 [1] 减 [0] 累加潜在 TP、用 [2] 减 [0] 收紧 SL;一旦 cur_sl 已优于当前回撤边界就 break 锁仓。 最后若 tp>0,sl 和 tp 各自除以 MaxSL / MaxTP 的 Point 值并 clamp 到 1 以内,result 塞入仓位比例(account[0]/100*0.01)、归一化 tp 与 sl。外汇与贵金属杠杆高,这套归一化只解决数值尺度,不预示胜率,开 MT5 把 MaxSL、MaxTP 按品种波动重设后再跑才看得清边界。
class="type">ulong argmin = target.ArgMin(); class="type">ulong argmax = target.ArgMax(); while(argmax > class="num">0 && argmin > class="num">0) { if(argmax < argmin && target[argmax] > MathAbs(target[argmin])) class="kw">break; if(argmax > argmin && target[argmax] < MathAbs(target[argmin])) class="kw">break; target.Resize(MathMin(argmax, argmin)); argmin = target.ArgMin(); argmax = target.ArgMax(); } if(argmin == class="num">0 || argmax < argmin) { class="type">class="kw">float tp = class="num">0; class="type">class="kw">float sl = class="num">0; class="type">class="kw">float cur_sl = - class="type">class="kw">float(MaxSL * Point()); class="type">ulong pos = class="num">0; for(class="type">ulong i = class="num">0; i < argmax; i++) { tp = MathMax(tp, target[i] + fstate[i, class="num">1] - fstate[i, class="num">0]); pos = i; if(cur_sl >= target[i] + fstate[i, class="num">2] - fstate[i, class="num">0]) class="kw">break; sl = MathMin(sl, target[i] + fstate[i, class="num">2] - fstate[i, class="num">0]); } if(tp > class="num">0) { sl = (class="type">class="kw">float)MathMin(MathAbs(sl) / (MaxSL * Point()), class="num">1); tp = (class="type">class="kw">float)MathMin(tp / (MaxTP * Point()), class="num">1); result[class="num">0] = class="type">class="kw">float(Buffer[tr].States[i].account[class="num">0] / class="num">100 * class="num">0.01); result[class="num">1] = tp; result[class="num">2] = sl;