将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLMs 开发和测试交易策略(一)- 微调·进阶篇
「把 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 的朋友先确认网络能通,不然实例化直接报错。
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 出来能看到模型续写出了同分布的行情序列,说明微调生效。外汇与贵金属数据高频跳变,微调模型仅作辅助参考,实盘仍属高风险。
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,否则直接报设备错误。外汇与贵金属波动受事件驱动,模型续写仅反映历史统计倾向,高风险品种勿直接当作下单依据。
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 跑之前建议先改这句再验证。
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,先在小布盯盘里手测一批误判样本再决定。