将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLMs 开发和测试交易策略(一)- 微调·进阶篇
📘

将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLMs 开发和测试交易策略(一)- 微调·进阶篇

第 2/2 篇

「把 M5 报价拼成模型能读的句子」

做 LLM 微调第一步,是先有一份干净的训练文本。之前那版 llm_data.csv 有 2442 行、64 列,全是 M5 周期货币对报价;现在改成每行 60 个收盘价,并转成纯文本,方便语言模型按固定序列长度去学。 核心思路是:给模型 20 个价,让它续写剩下 40 个,而不是乱出数。所以要把一行里散着的“0.6119”“0.61197”…压成“0.6119 0.61197 0.61201…0.61196”这样一整句。 切数据时有俩细节:df.iloc[:-10,1:] 里“:-10”是留最后 10 行不进训练集、拿去测;“1:”是丢掉每行第一列的 csv 索引,模型不需要它。拼完所有句子写进 train.txt,以后直接读这个文件,不用反复跑 csv。 下面这段就是把 csv 转 txt、再挂上 GPT2 tokenizer 和语言模型数据包装的完整代码。第一次用 GPT2 会从 Huggingface 拉预训练文件,用 docker 或 wsl 的朋友先确认网络能通,不然实例化直接报错。

MQL5 / C++
class="kw">import pandas as="as" pd
from transformers class="kw">import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config
from transformers class="kw">import TextDataset, DataCollatorForLanguageModeling
from transformers class="kw">import Trainer, TrainingArguments
class="kw">import torch
df = pd.read_csv(&class="macro">#x27;llm_data.csv&class="macro">#x27;)
sentences = [&class="macro">#x27; &class="macro">#x27;.join(map(str, prices)) for prices in df.iloc[:-class="num">10,class="num">1:].values]
with open(&class="macro">#x27;train.txt&class="macro">#x27;, &class="macro">#x27;w&class="macro">#x27;) as f:
    for sentence in sentences:
        f.write(sentence + &class="macro">#x27;
&class="macro">#x27;)
tokenizer = GPT2Tokenizer.from_pretrained(&class="macro">#x27;gpt2&class="macro">#x27;)
train_dataset = TextDataset(tokenizer=tokenizer,
                            file_path="train.txt",
                            block_size=class="num">60)
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

把 GPT-2 压成专属行情语言模型

加载预训练 GPT-2 后,真正决定微调质量的不是模型本身,而是 TrainingArguments 里那几个被忽视的开关。下面这段实例化只设了 7 个参数,但每一个都直接牵动显存与过拟合边界。 output_dir 指向 ./gpt2_stock,overwrite_output_dir=True 让重复跑实验不被旧检查点干扰;num_train_epochs=3 在中等规模行情文本上通常够用,再多容易记噪声。per_device_train_batch_size=32 取 2 的幂,配合消费级显卡更稳。 save_steps=10_000 意味着每跑一万步落一个检查点,save_total_limit=2 只留两个,避免磁盘被撑爆;load_best_model_at_end=True 保证最后用的是验证集最优权重,而不是末尾那步可能抖动的参数。 Trainer 类把 model、training_args、data_collator、train_dataset 一股脑塞进去就能 train(),但回调(callback)没设就是隐患——提前停止、TensorBoard 日志全默认关了,保守示例能跑通,真做贵金属波段预测建议把早停加上。 推断时取数据集最后一行转字符串,tokenizer.encode(..., return_tensors='pt') 投给 CUDA,do_sample=True 且 max_length=200,跑完 print 出来能看到模型续写出了同分布的行情序列,说明微调生效。外汇与贵金属数据高频跳变,微调模型仅作辅助参考,实盘仍属高风险。

MQL5 / C++
model = GPT2LMHeadModel.from_pretrained(&class="macro">#x27;gpt2&class="macro">#x27;)
training_args = TrainingArguments(output_dir="./gpt2_stock",
                                overwrite_output_dir=True,
                                num_train_epochs=class="num">3,
                                per_device_train_batch_size=class="num">32,
                                save_steps=10_000,
                                save_total_limit=class="num">2,
                                load_best_model_at_end=True,
                                )
trainer = Trainer(model=model,
                  args=training_args,
                  data_collator=data_collator,
                  train_dataset=train_dataset,)
trainer.train()
trainer.save_model("./gpt2_stock")
prompt = &class="macro">#x27; &class="macro">#x27;.join(map(str, df.iloc[:,class="num">1:class="num">20].values[-class="num">1]))
generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors=&class="macro">#x27;pt&class="macro">#x27;).to("cuda"),
do_sample=True,
max_length=class="num">200)[class="num">0],
skip_special_tokens=True)
print(f"test the model:{generated}")
class="kw">import pandas as pd
from transformers class="kw">import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config
from transformers class="kw">import TextDataset, DataCollatorForLanguageModeling
from transformers class="kw">import Trainer, TrainingArguments
class="kw">import torch
dvc=&class="macro">#x27;cuda&class="macro">#x27; if torch.cuda.is_available() else &class="macro">#x27;cpu&class="macro">#x27;
print(dvc)
df = pd.read_csv(&class="macro">#x27;llm_data.csv&class="macro">#x27;)
sentences = [&class="macro">#x27; &class="macro">#x27;.join(map(str, prices)) for prices in df.iloc[:-class="num">10,class="num">1:].values]
with open(&class="macro">#x27;train.txt&class="macro">#x27;, &class="macro">#x27;w&class="macro">#x27;) as f:
    for sentence in sentences:
        f.write(sentence + &class="macro">#x27;\n&class="macro">#x27;)
tokenizer = GPT2Tokenizer.from_pretrained(&class="macro">#x27;gpt2&class="macro">#x27;)
train_dataset = TextDataset(tokenizer=tokenizer,
                            file_path="train.txt",

◍ 用 GPT2 微调跑通行情续写

这段 Python 片段演示了如何把一个已处理好的行情 DataFrame 接进 GPT2 做语言模型微调,而不是做分类。block_size 设成 60,意味着单次喂入的上下文窗口是 60 个 token,对日内贵金属分时序列来说偏短,容易丢掉跨段结构。 训练参数里 num_train_epochs=3、per_device_train_batch_size=32,在单卡上跑小规模序列通常 1~2 小时可收敛,save_steps=10000 对几万行数据其实过于稀疏,可能整轮都没存几次。 推理部分取 df 最后一行第 1~19 列拼成 prompt,max_length=200 做 do_sample 续写。实盘前你应当把 dvc 变量改成本机可用的 cuda 或 cpu,否则直接报设备错误。外汇与贵金属波动受事件驱动,模型续写仅反映历史统计倾向,高风险品种勿直接当作下单依据。

MQL5 / C++
block_size=class="num">60)
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
model = GPT2LMHeadModel.from_pretrained(&class="macro">#x27;gpt2&class="macro">#x27;)
training_args = TrainingArguments(output_dir="./gpt2_stock",
                overwrite_output_dir=True,
                num_train_epochs=class="num">3,
                per_device_train_batch_size=class="num">32,
                save_steps=10_000,
                save_total_limit=class="num">2,
                load_best_model_at_end=True,
                )
trainer = Trainer(model=model,
                args=training_args,
                data_collator=data_collator,
                train_dataset=train_dataset,)
trainer.train()
trainer.save_model("./gpt2_stock")
prompt = &class="macro">#x27; &class="macro">#x27;.join(map(str, df.iloc[:,class="num">1:class="num">20].values[-class="num">1]))
generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors=&class="macro">#x27;pt&class="macro">#x27;).to(dvc), do_sample=True, max_length=class="num">200)[class="num">0], skip_special_tokens=True)
print(f"test the model:{generated}")

「小误差背后藏着大偏差」

模型微调跑完不代表能直接用,得拿真实值和预测值比一比。最直观的指标是均方误差(MSE),但光看它容易误判——当原始价格数值本身极小,MSE 再小也可能只是量纲带来的假象。 我们用数据集最后一行后 40 个收盘价当真值,让微调后的 GPT2 生成同长度序列,再用 trim_lists 按最短列表裁齐两端。打印出来能看到:true_prices 在 0.6119~0.61242 之间波动,generated_prices 在 0.61162~0.61231 之间,肉眼看贴得不算远。 直接算 MSE 得到 2.19e-07,绝对值极小。可一旦换算 RMSE(0.000468)和 NRMSE,后者跑到 0.585——意思是预测误差约占观测值范围的 58.5%。外汇与贵金属价格序列本就窄幅高频,这种相对偏差放在实盘里足以让信号反转,属于高风险误用场景。 想压住这个偏差,几条路都可行:微调迭代次数加量、喂更多历史 K 线、调学习率等微调参数,或直接换更大尺度模型。代码里 NRMSE 分母写的是 np.max(true_prices)-np.min(generated_prices),严格说该用真值极差,开 MT5 跑之前建议先改这句再验证。

MQL5 / C++
class="kw">import pandas as pd
from transformers class="kw">import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config
from sklearn.metrics class="kw">import mean_squared_error
class="kw">import torch
class="kw">import numpy as np
df = pd.read_csv(&class="macro">#x27;llm_data.csv&class="macro">#x27;)
dvc=&class="macro">#x27;cuda&class="macro">#x27; if torch.cuda.is_available() else &class="macro">#x27;cpu&class="macro">#x27;
model = GPT2LMHeadModel.from_pretrained(&class="macro">#x27;./gpt2_stock&class="macro">#x27;)
tokenizer = GPT2Tokenizer.from_pretrained(&class="macro">#x27;gpt2&class="macro">#x27;)
prompt = &class="macro">#x27; &class="macro">#x27;.join(map(str, df.iloc[:,class="num">1:class="num">20].values[-class="num">1]))
generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors=&class="macro">#x27;pt&class="macro">#x27;), do_sample=True, max_length=class="num">200)[class="num">0], skip_special_tokens=True)
true_prices= df.iloc[-class="num">1:,class="num">21:].values.tolist()[class="num">0]
generated_prices=generated.split(&class="macro">#x27;
&class="macro">#x27;)[class="num">0]
generated_prices=list(map(class="type">float,generated_prices.split()))
generated_prices=generated_prices[class="num">0:len(true_prices)]
def trim_lists(a, b):
    min_len = min(len(a), len(b))
    class="kw">return a[:min_len], b[:min_len]
true_prices,generated_prices=trim_lists(true_prices,generated_prices)
print(f"true_prices:{true_prices}")
print(f"generated_prices:{generated_prices}")
mse = mean_squared_error(true_prices, generated_prices)
print(&class="macro">#x27;MSE:&class="macro">#x27;, mse)
rmse=np.sqrt(mse)
nrmse=rmse/(np.max(true_prices)-np.min(generated_prices))
print(f"RMSE:{rmse},NRMSE:{nrmse}")

后续文章会补哪些代码

这一节把前面没铺开的坑点了个名:全参数微调 GPT2 的示例已经给了,但那个数据集并不适配文中提到的所有微调方法,只是先跑通流程。 后面系列会挑有代表性的方法补两套东西——可运行的 MQL5 代码,以及与之匹配的 EA 示例,数据集构造方式也会在对应文章里单独讲。 RAG 和 Agent 这类只提了一嘴的技术,不会烂尾,会有专文给详细讨论和代码实现。 外汇与贵金属交易本身杠杆高风险大,用 LLM 辅助出的策略未经严格回测前,实盘只建议极小仓位验证。

◍ 别急着下结论

把微调好的小模型接进 MT5 之前,先看清这套流程的真实边界。Yuqiang Pan 在评论区点明:示例中用的预训练 GPT2 权重与行情序列毫无关联,不微调就直接喂数据,模型根本识别不了;而经过 LoRA 式微调后,才能按需求吐出对应输出。 微调相对从头训练最实在的三点——收敛快、算力省、数据少。作者原话提到,因示例模型偏小,省资源的效果「不是很明显」,但数据量不足时,拿同样数据做微调远好于直接训。外汇与贵金属波动剧烈、杠杆高风险极大,任何 LLM 辅助策略都只是概率倾向,别把回测里的 1121 KB train.txt 当成实盘护身符。 想验证就自己下那几个附件:Fine-tuning.py 仅 1.73 KB、test.py 1.18 KB,跑通后再谈集成。模型轻量不等于信号管用,真要上 EA,先在小布盯盘里手测一批误判样本再决定。

常见问题

按时间顺序把开盘、最高、最低、收盘和成交量用空格或分隔符串成单行文本,避免换行和冗余字段,模型才能稳定续写。
单步预测偏差看似很小,但多步滚动生成会放大成方向性偏离,回测里可能整段走势都跑偏,别拿续写结果当确定信号。
小布可以替你加载品种页把续写输出和真实报价叠图对比,标出偏差扩大的区段,你只看结论不用自己跑脚本。
通常会补数据清洗脚本、训练循环封装和滚动预测评估函数,方便你直接套到自己品种上做压力测试。
不行。先在多品种多时段回看续写吻合度,确认误差不系统性偏移再考虑,外汇贵金属高风险别急着下结论。