神经网络变得简单(第 67 部分):按照过去的经验解决新任务(基础篇)
📘

神经网络变得简单(第 67 部分):按照过去的经验解决新任务(基础篇)

第 1/3 篇

用历史样本映射新行情的神经网络思路

在 MT5 里做策略迁移时,一个实用假设是:当前走势若与历史某段样本足够相似,网络就可借用那段样本的训练结果去处理新任务。这比从零训练更省算力,也更适合外汇与贵金属这种高波动、高杠杆品种——稍有不慎止损就会被扫,先验证再上实盘是底线。 具体做法倾向先把历史片段做成特征向量,再计算新数据与旧样本的余弦距离或欧氏距离,距离越小越可能属于同一类形态。2024 年 6 月发布的这套思路在社群里获得 1004 次查看、6 条讨论,说明手动验证的人不少,但没人给出稳定收益证明,概率上仍属探索阶段。 你可以开 MT5 新建 EA,把一段 EURUSD 的 H1 数据切成 20 根 K 线的窗口,跑一次相似度检索,看网络是否把当下窗口匹配到预期的历史启动段。

◍ 离线强化学习为何适合交易场景

强化学习的核心逻辑是智能体在与环境交互中追逐累计奖励最大化,但实盘里频繁试错往往意味着真金白银的损耗。对外汇与贵金属这类高杠杆、高波动品种而言,盲目探索带来的负面回报可能直接转化为账户回撤,这是多数交易者无法承受的。 离线强化学习(Offline RL)给出了一条折中路径:它只利用历史交互中已收集的有限轨迹存档来训练策略,不要求持续在线试探环境。在 MQL5 相关的自动化研究里,这意味着你可以把 MT5 实盘或回测导出的历史成交、状态序列当作静态数据集,先离线训好模型再上线。 代价也很明确。训练样本一旦有限,模型就很难覆盖市场所有的随机多变状态,复杂行情下泛化能力会明显走弱。好在大多数优化任务并非从零开始——我们手头几乎总能找到过往类似策略与环境互动的日志,这些真实轨迹若能在动作和状态空间上做一定覆盖,离线框架(如 Real-ORL)就有落地价值。 需要提醒,外汇与贵金属交易属高风险活动,离线训出的策略仅代表历史样本上的概率倾向,实盘仍需小资金验证。

「真实世界里的离线强化学习到底卡在哪」

离线强化学习(ORL)把环境当成马尔可夫决策过程来建模,前提是手里已有一份由某种行为策略采来的轨迹数据集,目标是训出一个接近最优的策略 π,而不是理论上的 π*。现实里因为探索不足、数据有限,几乎不可能触达最优,只能基于手头数据尽量逼近。 大多数 ORL 算法都带正则化或保守主义,比如政策梯度正则、近似动态规划、或借环境模型学习。真实-ORL 框架没造新训练算法,而是把已有代表性 ORL 算法搬上物理机器人做实测。原文指出,很多基准测试偏爱理想、独立同分布的数据集,但真实随机世界有动作延迟,模拟结论未必能直接迁移。 为了客观,作者用四个经典操纵任务做压力测试,每个都建成带独立奖励函数的 MDP,所有算法在同条件下跑完。训练数据来自专家监督下的启发式手工策略,只收成功轨迹——他们认为真实机器人上收次优或扭曲轨迹不安全,也伤设备。 实验规模不小:3000+ 训练轨迹、3500+ 估测轨迹、270+ 人工时。结论有几个硬点:区域内任务上 ORL 能泛化到数据稀缺和动态变化;异构数据带来的性能波动随智能体、任务、数据特征而变;部分与任务无关的轨迹反而提供重叠支撑,提升学习效果。即便在区域外数据模式,ORL 对真实世界仍是更现实有效的路线,但外汇与贵金属这类高波动市场若套用类似离线范式,需警惕分布偏移导致策略失效的高风险。

用信号历史当离线强化学习的环境经验

离线强化学习(Offline RL)的核心约束是:用来训练智能体的数据集,必须来自你要分析的环境本身的交互结果。放到外汇上,这就意味着我们不该随便拿模拟盘或别的品种历史去喂模型,而该用真实账户在 EURUSD 这类标的上跑出来的成交轨迹。 我挑了 EURUSD 2023 年前 7 个月作为训练区间,按“这段历史里信号有没有实际成交”来筛信号,付费免费都行。付费信号常隐藏最近成交,但开仓历史大多还在,而我们要的正是开仓点。在 MT5 信号页的“账户”和“统计”选项卡交叉核对后,挑出 20 个信号,把各自交易历史 csv 存到 Terminal\Common\Files\Signals\Signal X .csv(X 为序号)。 真实-ORL 框架只把选中轨迹当“与环境交互的经验”,不承诺克隆轨迹,所以筛的时候不用管成交和指标的相关性,也别指望训出来的模型会完美复述最赚的那笔。下一步是把 csv 成交映射成每根柱线开盘时的历史价和指标读数。 下面这段 CDeal 类只留公开变量、省略品种名(默认单品种),point 存点数大小,因为模型动作按相对单位生成,而成交里止损止盈是价格。 [CODE] class CDeal : public CObject { public: datetime OpenTime; datetime CloseTime; ENUM_POSITION_TYPE Type; double Volume; double OpenPrice; double StopLos; double TakeProfit; double point; //--- [/CODE] 逐行拆解:class CDeal 继承 CObject 供动态数组管理;OpenTime/CloseTime 记开平时间;Type 区分多空;Volume 为交易量;OpenPrice 开仓价;StopLos/TakeProfit 以价格形式存,后面要除 point 转相对偏移;point 就是品种点值,转换用。 CDeals 类负责从一个 csv 加载全部成交,并聚合成动作向量。LoadDeals 先校验文件在 Common 文件夹存在、品种名能对应(不对就掉用图表品种),再逐行读,遇品种不符跳过;csv 里没有显式 SL/TP,只给平仓价,于是假设盈利平仓即 TP、亏损即 SL,把平仓价填进对应字段。Action 方法遍历成交,若历史信号时间不在柱线开口,则把开仓硬调成柱线开盘,并返回动作向量:多头写索引 0 量、1 TP、2 SL,空头偏移 3 位。多仓并发时只加交易量、SL/TP 取最大偏差。 EA ResearchRealORL.mq5 在初始化时靠宏替换 SignalFile(Agent) 读对应 Signal X.csv,策略测试器里优化 Agent 参数就能并行跑多信号收轨迹。OnTick 里不跑模型,只在新柱开盘从信号史取动作向量、补齐账户与持仓状态、记余额净值变化当奖励。外汇和贵金属杠杆高、滑点跳空频繁,这种无显式止损的映射只是训练数据预备,实盘风险极大,下游务必重训时补风控。

MQL5 / C++
class CDeal        :  class="kw">public CObject
  {
class="kw">public:
   class="type">class="kw">datetime              OpenTime;
   class="type">class="kw">datetime              CloseTime;
   class="type">ENUM_POSITION_TYPE    Type;
   class="type">class="kw">double                Volume;
   class="type">class="kw">double                OpenPrice;
   class="type">class="kw">double                StopLos;
   class="type">class="kw">double                TakeProfit;
   class="type">class="kw">double                point;
   class=class="str">"cmt">//---

◍ 把持仓状态压成强化学习特征向量

下面这段 CDeal 类把一笔已开仓单的静态属性(开平时间、多空类型、成交量、挂单价)封装起来,核心在 Action() 方法:它不在每根 K 线无脑输出,而是先用 `(OpenTime - period_seconds) > currentCloseTime <= current` 判断当前时间戳是否落在持仓生命周期内,不在就直接返回全零向量,避免用失效样本污染训练。

进入多空分支后,买仓把 Volume 写进 result[0],若 TP/SL 大于 0,则分别用 (TakeProfit - ask)/(MaxTP*point)(ask - StopLos)/(MaxSL*point) 把绝对点数距离归一化到 [0,1] 附近区间;卖仓对称地填 result[3~5],分母同样是 MaxTP/MaxSL 乘 point。这里 point 构造函数里写死 1e-5,做黄金或日元对时若直接套用会算错比例,必须按品种重设。 CDeals 类则用 CArrayObj 管理多笔 CDeal 实例,提供 LoadDeals(file_name, symbol, point) 接口从文件按品种和点值批量装载。实盘前建议先打印某笔 EURUSD 持仓的 Action() 返回值,确认 result[1] 在 TP 未触及时落在 0~1 之间,否则归一化系数要调。外汇与贵金属杠杆高,特征算错会直接带偏模型决策,验证通过再接训练。

MQL5 / C++
CDeal(class="type">void);
 ~CDeal(class="type">void) {};
 class=class="str">"cmt">//---
 vector<class="type">float>       Action(class="type">class="kw">datetime current, class="type">class="kw">double ask, class="type">class="kw">double bid, class="type">int period_seconds);
 };
class="type">void CDeal::CDeal(class="type">void) :  OpenTime(class="num">0),
                     CloseTime(class="num">0),
                     Type(POSITION_TYPE_BUY),
                     Volume(class="num">0),
                     OpenPrice(class="num">0),
                     StopLos(class="num">0),
                     TakeProfit(class="num">0),
                     point(class="num">1e-5)
 {
 }
vector<class="type">float> CDeal::Action(class="type">class="kw">datetime current, class="type">class="kw">double ask, class="type">class="kw">double bid, class="type">int period_seconds)
 {
  vector<class="type">float> result = vector<class="type">float>::Zeros(NActions);
  if((OpenTime - period_seconds) > current || CloseTime <= current)
    class="kw">return result;
  class="kw">switch(Type)
    {
     case POSITION_TYPE_BUY:
       result[class="num">0] = class="type">float(Volume);
       if(TakeProfit > class="num">0)
         result[class="num">1] = class="type">float((TakeProfit - ask) / (MaxTP * point));
       if(StopLos > class="num">0)
         result[class="num">2] = class="type">float((ask - StopLos) / (MaxSL * point));
       break;
     case POSITION_TYPE_SELL:
       result[class="num">3] = class="type">float(Volume);
       if(TakeProfit > class="num">0)
         result[class="num">4] = class="type">float((bid - TakeProfit) / (MaxTP * point));
       if(StopLos > class="num">0)
         result[class="num">5] = class="type">float((StopLos - bid) / (MaxSL * point));
       break;
    }
class=class="str">"cmt">//---
  class="kw">return result;
 }
class CDeals
 {
class="kw">protected:
  CArrayObj        Deals;
class="kw">public:
                     CDeals(class="type">void) { Deals.Clear(); }
                    ~CDeals(class="type">void) { Deals.Clear(); }
  class=class="str">"cmt">//---
  class="type">bool            LoadDeals(class="type">class="kw">string file_name, class="type">class="kw">string symbol, class="type">class="kw">double point);

「从 CSV 回放历史成交的加载细节」

把历史成交喂给 EA 做复盘,第一步是把 CSV 读进内存。下面这段代码示范了用 FILE_COMMON 共享目录读取、按分号切列、过滤对应品种的实现路径,直接拷进 MT5 能跑。 文件不存在就直接退出,避免后续空指针。若调用时没传 symbol,则自动回退到当前图表品种 _Symbol 和点值 _Point,这样同一段逻辑既能复盘指定品种,也能就地验证当前盘面。 核心循环里每次 FileReadString 按顺序取一列:开仓时间、类型、成交量、品种名、开仓价、最大成交量、平仓时间、平仓价,后面三列原样读掉不解析。用 StringFind 做品种模糊匹配,不匹配就 continue 跳过,匹配成功才 new 一个 CDeal 对象并赋值开平时间。 外汇与贵金属属高风险品种,复盘结果只代表历史概率,不预示未来收益。把这段加载函数接上你自己的 CDeal 容器,改一个品种名就能在策略测试器外独立校验成交序列。

MQL5 / C++
class="type">bool CDeals::LoadDeals(class="type">class="kw">string file_name, class="type">class="kw">string symbol, class="type">class="kw">double point)
  {
   if(file_name == NULL || !FileIsExist(file_name, FILE_COMMON))
     {
       PrintFormat("File %s not exist", file_name);
       class="kw">return false;
     }
   if(symbol == NULL)
     {
       symbol = _Symbol;
       point = _Point;
     }
   ResetLastError();
   class="type">int handle = FileOpen(file_name, FILE_READ | FILE_ANSI | FILE_CSV | FILE_COMMON, class="type">short(&class="macro">#x27;;&class="macro">#x27;), CP_ACP);
   if(handle == INVALID_HANDLE)
     {
       PrintFormat("Error of open file %s: %d", file_name, GetLastError());
       class="kw">return false;
     }
   FileSeek(handle, class="num">0, SEEK_SET);
   while(!FileIsEnding(handle))
     {
       class="type">class="kw">string s = FileReadString(handle);
       class="type">class="kw">datetime open_time = StringToTime(s);
       class="type">class="kw">string type = FileReadString(handle);
       class="type">class="kw">double volume = StringToDouble(FileReadString(handle));
       class="type">class="kw">string deal_symbol = FileReadString(handle);
       class="type">class="kw">double open_price = StringToDouble(FileReadString(handle));
       volume = MathMin(volume, StringToDouble(FileReadString(handle)));
       class="type">class="kw">datetime close_time = StringToTime(FileReadString(handle));
       class="type">class="kw">double close_price = StringToDouble(FileReadString(handle));
       s = FileReadString(handle);
       s = FileReadString(handle);
       s = FileReadString(handle);
       if(StringFind(deal_symbol, symbol, class="num">0) < class="num">0)
         class="kw">continue;
       ResetLastError();
       CDeal *deal = new CDeal();
       if(!deal)
         {
           PrintFormat("Error of create new deal object: %d", GetLastError());
           class="kw">return false;
         }
       deal.OpenTime = open_time;
       deal.CloseTime = close_time;

常见问题

把历史中相似的价格走势和对应持仓结果存成样本库,新行情进来先匹配最近邻样本,再参考其历史应对方式,概率上更稳。
在线训练要在真实账户试错,成本高且风险大;离线强化学习直接用历史成交数据学策略,不干扰实盘,适合高杠杆品种。
可以,小布盯盘的 AIGC 会读取品种页里的历史信号,自动标注与当前形态接近的过往案例和当时结果,你只需看结论。
至少含当前方向、浮盈浮亏、持仓时长、近期波动幅度;压缩后作为输入,模型才知你处在哪种局面,避免乱出信号。
多是时间列格式不对或分隔符混用,统一成 YYYY-MM-DD HH:MM 和英文逗号,并确认首行是表头再读,基本能解决。