财经建模中合成数据的生成式对抗网络(GAN)(第 2 部分):创建测试合成品种(基础篇)
📘

财经建模中合成数据的生成式对抗网络(GAN)(第 2 部分):创建测试合成品种(基础篇)

第 1/2 篇

◍ 用 GAN 给 MT5 造一个没人交易过的合成品种

想在 MT5 里验证价格行为策略,但历史样本太少、实盘干扰太多?可以用生成式对抗网络(GAN)造一段「测试合成品种」——它不对应任何真实外汇或贵金属报价,纯粹用来压测你的指标逻辑。 生成器负责输出假 K 线序列,判别器负责判断这段序列像不像真实行情;两者对抗迭代后,生成器产出的数据在统计特征上会逼近训练集分布。注意:合成品种仅用于算法验证,外汇与贵金属实盘仍属高风险,任何回测优势都不等于实盘概率。 在 MQL5 中,可先以 CSV 形式导入 GAN 离线生成的序列,再用自定义品种接口注册。下面这段演示如何创建一个空白自定义品种,方便后续灌入合成数据。

MQL5 / C++
class="type">int sym_handle = CustomSymbolCreate("SynthGAN_EURX", "", Symbol() ); class=class="str">"cmt">// 创建名为 SynthGAN_EURX 的自定义品种,基类用当前品种
if(sym_handle == INVALID_HANDLE) Print("自定义品种创建失败,错误码:", GetLastError()); class=class="str">"cmt">// 失败则打印错误
else Print("自定义品种句柄:", sym_handle); class=class="str">"cmt">// 成功则输出句柄

「为什么历史数据撑不起策略检验」

做财经建模最头疼的不是没算法,而是样本太少。很多时段里你手里就一小撮 K 线,崩盘、极端波动、长期横盘这些关键态根本没被采到。直接拿这种残缺集去训策略,泛化能力天然瘸腿,放到真实盘里容易一头撞墙。 地缘博弈和政策转向还会改市场结构,历史回测再漂亮也追不上没见过的盘整。靠回测记录保底可行,但面对新形态依旧会失灵。 补数据的务实路子是用人工合成样本。GAN 这类生成式对抗网络能把真实财经序列的分布在数学上重画一遍,吐出包含非对称行情的仿真数据,拿来压力测试比纯历史集宽得多。 本文用 Shapiro-Wilk、学生 t 检验、Levene 这三把尺子卡合成数据与真值的相似度。统计上过不了显著性阈值的样本直接弃用,过了你才敢往 MT5 策略里塞。外汇与贵金属杠杆高,合成数据只是补盲区,不等于免死金牌。

用 GAN 补上稀缺的极端行情样本

财经历史数据受监管与可得性限制,像 EURUSD 的崩盘、瞬间拉升这类极端片段在真实样本里占比极低,直接拿去训模型容易在非常态市况下失效。GAN 用生成器与鉴别器对抗:前者从随机噪声里造出仿真序列,后者逼着它越来越像真数据,几轮下来能补齐那些训练集里缺得的稀有事件。 落地时先把 EURUSD 历史汇率做清理,去掉缺失点并把数值归一化到 -1 到 1 之间,再抽时间序列特征。生成器吃随机向量输出合成价格,鉴别器给真伪反馈,两边用不同损失函数对着跑,学习率要手调以防不收敛。 造出来的合成品种能模拟大幅下跌和异常波动,给策略提供未见过的压力场景,降低对单调历史趋势的过拟合倾向。外汇与贵金属属高风险品种,用合成数据测过的算法也只在概率上更稳,实盘前仍须在 MT5 用真实 tick 复核。

◍ 把 GAN 造的假 EURUSD 灌进 MT5

用 GAN 跑出来的合成行情,最终要落到 MT5 里才能当回测样本。思路不复杂:GAN 负责生成贴近真实形态的价格序列,导出成标准 CSV,再用 MQL5 脚本以自定义品种方式写进平台。CSV 的分隔与字段顺序必须对齐 MT5 读取习惯,否则 CustomRatesUpdate 投喂时会直接丢数据。 实操时在「市场报价」里建一个品种,比如命名 SYNTH_EURUSD,用 CustomSymbolCreate 拉起容器,再把 CSV 逐行解析后塞进去。下面这段代码就是干这事的核心:先读文件、按分号切字段、转成 MqlRates 结构,再在 OnStart 里建符号并设好小数位和点值。

MQL5 / C++
class="macro">#define SYNTHETIC_CSV_FILE_NAME "EURUSD_3_years_synthetic.csv"
class=class="str">"cmt">// Function to read synthetic data from a CSV file
class="type">bool ReadSyntheticDataFromCSV(const class="type">class="kw">string fileName, class="type">MqlRates &rates[])
{
   class="type">int fileHandle = FileOpen(fileName, FILE_CSV | FILE_READ | FILE_ANSI);
   if (fileHandle == INVALID_HANDLE)
   {
      Print("Error opening file: ", GetLastError());
      class="kw">return class="kw">false;
   }
   ArrayResize(rates, class="num">0);
   class="kw">while (!FileIsEnding(fileHandle))
   {
      class="type">class="kw">string line = FileReadString(fileHandle);
      StringReplace(line, ",", ".");
      class="type">class="kw">string fields[];
      class="type">int fieldCount = StringSplit(line, &class="macro">#x27;;&class="macro">#x27;, fields);
      if (fieldCount >= class="num">6)
      {
         class="type">MqlRates rate;
         rate.time = (class="type">class="kw">datetime)StringToTime(fields[class="num">0]);
         rate.open = StringToDouble(fields[class="num">1]);
         rate.high = StringToDouble(fields[class="num">2]);
         rate.low = StringToDouble(fields[class="num">3]);
         rate.close = StringToDouble(fields[class="num">4]);
         rate.tick_volume = (class="type">long)StringToInteger(fields[class="num">5]);
         rate.spread = class="num">0;
         rate.real_volume = class="num">0;
         class="type">int currentSize = ArraySize(rates);
         ArrayResize(rates, currentSize + class="num">1);
         rates[currentSize] = rate;
      }
   }
   FileClose(fileHandle);
   Print("Synthetic data successfully read from CSV.");
   class="kw">return true;
}
class="type">void OnStart(){
   class="type">class="kw">string syntheticSymbol = "SYNTH_EURUSD";
   class=class="str">"cmt">// Step class="num">1: Create or Reset the Synthetic Symbol
   if (!CustomSymbolCreate(syntheticSymbol))
   {
      Print("Error creating synthetic symbol: ", GetLastError());
      class="kw">return;
   }
   class=class="str">"cmt">// Step class="num">2: Configure the Symbol Properties
   CustomSymbolSetInteger(syntheticSymbol, SYMBOL_DIGITS, class="num">5);
   CustomSymbolSetDouble(syntheticSymbol, SYMBOL_POINT, class="num">0.00001);
逐行拆一下:宏定义写死文件名 EURUSD_3_years_synthetic.csv,意味着这套流程默认喂的是 3 年合成数据;ReadSyntheticDataFromCSV 里 FileOpen 用 CSV+ANSI 读,逗号被替换成点号是修欧式小数格式;StringSplit 按分号切,fieldCount>=6 才认作有效行,对应时间、开高低收、成交量;OnStart 中 CustomSymbolCreate 建符号,SYMBOL_DIGITS 设 5、SYMBOL_POINT 设 0.00001,贴合 EURUSD 五位数报价。 品种建完别急着跑策略。拿统计检验比一下合成序列和真实 EURUSD 的均值、波动聚类,确认形态不漂移。外汇与贵金属属高风险品种,合成数据仅用于策略预筛,实盘前仍须在真实tick上重验。

MQL5 / C++
class="macro">#define SYNTHETIC_CSV_FILE_NAME "EURUSD_3_years_synthetic.csv"
class=class="str">"cmt">// Function to read synthetic data from a CSV file
class="type">bool ReadSyntheticDataFromCSV(const class="type">class="kw">string fileName, class="type">MqlRates &rates[])
{
   class="type">int fileHandle = FileOpen(fileName, FILE_CSV | FILE_READ | FILE_ANSI);
   if (fileHandle == INVALID_HANDLE)
   {
      Print("Error opening file: ", GetLastError());
      class="kw">return class="kw">false;
   }
   ArrayResize(rates, class="num">0);
   class="kw">while (!FileIsEnding(fileHandle))
   {
      class="type">class="kw">string line = FileReadString(fileHandle);
      StringReplace(line, ",", ".");
      class="type">class="kw">string fields[];
      class="type">int fieldCount = StringSplit(line, &class="macro">#x27;;&class="macro">#x27;, fields);
      if (fieldCount >= class="num">6)
      {
         class="type">MqlRates rate;
         rate.time = (class="type">class="kw">datetime)StringToTime(fields[class="num">0]);
         rate.open = StringToDouble(fields[class="num">1]);
         rate.high = StringToDouble(fields[class="num">2]);
         rate.low = StringToDouble(fields[class="num">3]);
         rate.close = StringToDouble(fields[class="num">4]);
         rate.tick_volume = (class="type">long)StringToInteger(fields[class="num">5]);
         rate.spread = class="num">0;
         rate.real_volume = class="num">0;
         class="type">int currentSize = ArraySize(rates);
         ArrayResize(rates, currentSize + class="num">1);
         rates[currentSize] = rate;
      }
   }
   FileClose(fileHandle);
   Print("Synthetic data successfully read from CSV.");
   class="kw">return true;
}
class="type">void OnStart(){
   class="type">class="kw">string syntheticSymbol = "SYNTH_EURUSD";
   class=class="str">"cmt">// Step class="num">1: Create or Reset the Synthetic Symbol
   if (!CustomSymbolCreate(syntheticSymbol))
   {
      Print("Error creating synthetic symbol: ", GetLastError());
      class="kw">return;
   }
   class=class="str">"cmt">// Step class="num">2: Configure the Symbol Properties
   CustomSymbolSetInteger(syntheticSymbol, SYMBOL_DIGITS, class="num">5);
   CustomSymbolSetDouble(syntheticSymbol, SYMBOL_POINT, class="num">0.00001);

常见问题

用 GAN 学习真实品种的分布后生成极端行情合成数据,补上稀缺样本再灌进平台回测,注意这只是概率增强不是保证。
可训练 GAN 生成没人交易过的合成 EURUSD 序列,扩充数据量,但外汇高风险,合成结果仅作参考。
小布可对接合成品种页面做 AIGC 诊断,提示分布偏移与极端段异常,你专注判断是否采用。
有可能,建议只用合成数据补极端段、保留真实数据为主,并交叉验证多组生成种子。
可用现成轻量脚本生成基础合成序列,重点先验证极端行情覆盖度,再逐步迭代模型。