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