将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(二)-LoRA-调优·综合运用
📘

将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(二)-LoRA-调优·综合运用

第 3/3 篇

对齐真实与生成序列再算误差

用 LLM 生成价格序列后,第一步不是直接看涨跌,而是先把真实序列和模型输出截成等长。trim_lists 取两者最短长度做切片,避免后续算 MSE 时因维度不一致报错。 误差评估用三组指标:MSE 是均方误差,rmse 取其平方根回到价格量纲,nrmse 再除以真实值极差做归一。若 true_prices 极差接近 0,nrmse 会爆大,这种样本直接剔除别进回测。 生成阶段用 model.generate 跑 200 token,do_sample=True 代表带随机性,同一 prompt 每次结果可能不同。把输出按换行切出首行、转 float 列表,再截到 len(true_prices) 长度,这一步决定了你后面画图时两根曲线的可比性。 plot_ 里只在 title=='prediction' 才画真实值,其余情况只叠加 fine_tuning 与 lora_tuning 两条线。跑完建议把 png 存下来,肉眼比对 X 与方块标记偏离度,比单看 RMSE 数字更直觉。外汇与贵金属波动剧烈,此类生成结果仅作概率参考,实盘前务必在 MT5 历史数据上验证。

MQL5 / C++
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"Input data: {input_data}")
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}")

# generater 节选
generated = tokenizer.decode(
    model.generate(token, do_sample=True, max_length=class="num">200)[class="num">0],
    skip_special_tokens=True
)
generated_prices = generated.split(&class="macro">#x27;\n&class="macro">#x27;)[class="num">0]
generated_prices = list(map(class="type">float, generated_prices.split()))
generated_prices = generated_prices[:len(true_prices)]

# plot_ 节选
plt.figure(figsize=(class="num">10, class="num">6))
if title == &class="macro">#x27;prediction&class="macro">#x27;:
    plt.plot(true_prices, label=&class="macro">#x27;True Values&class="macro">#x27;, marker=&class="macro">#x27;o&class="macro">#x27;)
plt.plot(a, label=&class="macro">#x27;fine_tuning&class="macro">#x27;, marker=&class="macro">#x27;x&class="macro">#x27;)
plt.plot(b, label=&class="macro">#x27;lora_tuning&class="macro">#x27;, marker=&class="macro">#x27;s&class="macro">#x27;)
plt.title(title)
plt.xlabel(&class="macro">#x27;Index&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;Value&class="macro">#x27;)
plt.legend()
plt.savefig(f"{title}.png")

「两种微调路线的对比图怎么画」

把全量微调(fine-tuning)和 LoRA 微调(lora-tuning)的预测表现并排看,最直观的办法是用 matplotlib 画分组柱状图。下面这段 Python 脚本先给两组模型灌入最近一条样本(df.iloc[:,1:20].values[-1]),再各自生成 200 token 以内的价格序列,最后用 plt.bar 分左右两簇展示。 柱宽设成 0.2,r1 为模型 a 的基准位置,r2 在 r1 基础上右移一个 bar_width,这样两组柱子不会重叠。y 轴切到对数刻度(plt.yscale('log'))是因为两类指标量纲差太大,线性轴会把小值压成一条线。 实测里模型 a 的首项指标读到 101.7946、模型 b 为 69.5605,差距接近 1.46 倍;若直接线性轴看图,b 组后几项(0.877、4.10)基本看不见。图存成 Comparison.png,不弹窗,方便你批量跑完再翻。 外汇与贵金属价格受宏观事件扰动大,这类生成结果只反映历史样本上的统计倾向,实盘使用前请在 MT5 用真实 tick 复核,杠杆品种高风险。

MQL5 / C++
class="kw">import time
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
from peft class="kw">import PeftModel
class="kw">import matplotlib.pyplot as plt
# Load the dataset
df = pd.read_csv(&class="macro">#x27;llm_data.csv&class="macro">#x27;)
# Define the device(GPU if available)
dvc = &class="macro">#x27;cuda&class="macro">#x27; if torch.cuda.is_available() else &class="macro">#x27;cpu&class="macro">#x27;
# Model paths and base settings
base_model = &class="macro">#x27;gpt2&class="macro">#x27;
fine_tuning_path = &class="macro">#x27;./gpt2_stock&class="macro">#x27;
lora_tuning_path = &class="macro">#x27;./gpt2_LORA_None&class="macro">#x27;
# Load the tokenizer and models
tokenizer = GPT2Tokenizer.from_pretrained(base_model)
model_fine_tuning = GPT2LMHeadModel.from_pretrained(fine_tuning_path).to(dvc)
model_lora_tuning = GPT2LMHeadModel.from_pretrained(base_model)
model_lora_tuning = PeftModel.from_pretrained(model_lora_tuning, lora_tuning_path).to(dvc)
# Extract the input data and true prices from the dataset
input_data = df.iloc[:, class="num">1:class="num">20].values[-class="num">1]
true_prices = df.iloc[-class="num">1:, class="num">21:].values.tolist()[class="num">0]
prompt = &class="macro">#x27; &class="macro">#x27;.join(map(str, input_data))
# Function to generate predictions
def generater(model):
    global true_prices
    model.eval()
    # Tokenization and text generation
    token = tokenizer.encode(prompt, return_tensors=&class="macro">#x27;pt&class="macro">#x27;).to(dvc)
    start_ = time.time()
    generated = tokenizer.decode(
        model.generate(token, do_sample=True, max_length=class="num">200)[class="num">0],
        skip_special_tokens=True
    )
    end_ = time.time()
    print(f&class="macro">#x27;Generate time: {end_ - start_}&class="macro">#x27;)
    # Processing generated prices
    generated_prices = generated.split(&class="macro">#x27;\n&class="macro">#x27;)[class="num">0]
    generated_prices = list(map(class="type">float, generated_prices.split()))
    generated_prices = generated_prices[:len(true_prices)]
    # Function to trim lists to the same length
    def trim_lists(a, b):
        pass

plt.figure(figsize=(class="num">10, class="num">6))
# Update values for model a and b
a = [class="num">101.7946, class="num">1.243, class="num">5.67, a[class="num">1], a[class="num">2], a[class="num">3]]
b = [class="num">69.5605, class="num">0.877, class="num">4.10, b[class="num">1], b[class="num">2], b[class="num">3]]
# Bar width for each group of bars
bar_width = class="num">0.2
# Set the positions of the bars
r1 = np.arange(len(metrics))  # Positions for model a
r2 = [x + bar_width for x in r1]  # Positions for model b
# Plot bars for both models
plt.bar(r1, a, class="type">class="kw">color=&class="macro">#x27;r&class="macro">#x27;, width=bar_width, edgecolor=&class="macro">#x27;grey&class="macro">#x27;, label=models[class="num">0])
plt.bar(r2, b, class="type">class="kw">color=&class="macro">#x27;b&class="macro">#x27;, width=bar_width, edgecolor=&class="macro">#x27;grey&class="macro">#x27;, label=models[class="num">1])
# Set log scale for y-axis
plt.yscale(&class="macro">#x27;log&class="macro">#x27;)
# Set labels and title
plt.xlabel(&class="macro">#x27;Metrics&class="macro">#x27;, fontweight=&class="macro">#x27;bold&class="macro">#x27;)
plt.xticks([r + bar_width / class="num">2 for r in range(len(metrics))], metrics)  # Center the x-axis ticks
plt.ylabel(&class="macro">#x27;Values(log scale)&class="macro">#x27;, fontweight=&class="macro">#x27;bold&class="macro">#x27;)
plt.title(&class="macro">#x27;Model Comparison&class="macro">#x27;)
# Display legend and save the plot
plt.legend()
# plt.show()  # Uncomment to display the plot
plt.savefig(&class="macro">#x27;Comparison.png&class="macro">#x27;)
fine_tuning_result = generater(model_fine_tuning)
lora_tuning_result = generater(model_lora_tuning)
plot_(fine_tuning_result[class="num">0],lora_tuning_result[class="num">0],title=&class="macro">#x27;predication&class="macro">#x27;)
groups_chart(fine_tuning_result,lora_tuning_result,models=[&class="macro">#x27;fine-tuning&class="macro">#x27;,&class="macro">#x27;lora-tuning&class="macro">#x27;])

◍ 对齐价格序列并量化两种微调的偏差

把真实行情和模型生成的价格放在一起比之前,得先截到同样长度。trim_lists 用 min(len(a), len(b)) 取短边,再对两个列表做切片,避免后面算 MSE 时维度对不上。 误差计算走的是均方误差路线:mse = mean_squared_error(true_prices, generated_prices),rmse 开根号,nrmse 用 rmse 除以真实价最大值与生成价最小值之差做归一。外汇与贵金属价格序列波动大,NRMSE 若超过 0.1 往往意味着生成轨迹已严重偏离实盘,仅作模型筛选参考,实盘高风险依旧。 groups_chart 里两组模型的硬指标已经跑出:全量微调训练 101.7946 秒、推理 1.243 秒、显存 5.67 GB;LoRA 微调对应 69.5605 秒、0.877 秒、4.10 GB。柱状图用对数纵轴,差距一眼可辨。 直接在 MT5 外接的 Python 环境里复跑 generater 拿到四个返回值,再调 plot_ 和 groups_chart,就能把 fine-tuning 与 lora-tuning 的预测线和指标柱图存成 png,肉眼核对哪条线更贴 true_prices。

MQL5 / C++
    min_len = min(len(a), len(b))
    class="kw">return a[:min_len], b[:min_len]

    # Trim the true prices and generated prices
    true_prices, generated_prices = trim_lists(true_prices, generated_prices)

    # Output metrics
    print(f"Input data: {input_data}")
    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}")

    class="kw">return generated_prices, mse, rmse, nrmse
# Function to plot the comparison between true prices and predictions
def plot_(a, b, title):
    plt.figure(figsize=(class="num">10, class="num">6))

    if title == &class="macro">#x27;prediction&class="macro">#x27;:
        plt.plot(true_prices, label=&class="macro">#x27;True Values&class="macro">#x27;, marker=&class="macro">#x27;o&class="macro">#x27;)

    plt.plot(a, label=&class="macro">#x27;fine_tuning&class="macro">#x27;, marker=&class="macro">#x27;x&class="macro">#x27;)
    plt.plot(b, label=&class="macro">#x27;lora_tuning&class="macro">#x27;, marker=&class="macro">#x27;s&class="macro">#x27;)

    plt.title(title)
    plt.xlabel(&class="macro">#x27;Index&class="macro">#x27;)
    plt.ylabel(&class="macro">#x27;Value&class="macro">#x27;)
    plt.legend()
    plt.savefig(f"{title}.png")
# Function to generate a bar chart comparing different metrics between models
def groups_chart(a, b, models):
    metrics = [&class="macro">#x27;Train Time(s)&class="macro">#x27;, &class="macro">#x27;Inference Time(s)&class="macro">#x27;, &class="macro">#x27;Memory Usage(GB)&class="macro">#x27;, &class="macro">#x27;MSE&class="macro">#x27;, &class="macro">#x27;RMSE&class="macro">#x27;, &class="macro">#x27;NRMSE&class="macro">#x27;]
    plt.figure(figsize=(class="num">10, class="num">6))

    # Data for the metrics
    a = [class="num">101.7946, class="num">1.243, class="num">5.67, a[class="num">1], a[class="num">2], a[class="num">3]]
    b = [class="num">69.5605, class="num">0.877, class="num">4.10, b[class="num">1], b[class="num">2], b[class="num">3]]

    bar_width = class="num">0.2
    r1 = np.arange(len(metrics))
    r2 = [x + bar_width for x in r1]

    # Plotting bars for both models
    plt.bar(r1, a, class="type">class="kw">color=&class="macro">#x27;r&class="macro">#x27;, width=bar_width, edgecolor=&class="macro">#x27;grey&class="macro">#x27;, label=models[class="num">0])
    plt.bar(r2, b, class="type">class="kw">color=&class="macro">#x27;b&class="macro">#x27;, width=bar_width, edgecolor=&class="macro">#x27;grey&class="macro">#x27;, label=models[class="num">1])

    # Set y-axis to log scale for better visibility of differences
    plt.yscale(&class="macro">#x27;log&class="macro">#x27;)

    plt.xlabel(&class="macro">#x27;Metrics&class="macro">#x27;, fontweight=&class="macro">#x27;bold&class="macro">#x27;)
    plt.xticks([r + bar_width for r in range(len(metrics))], metrics)
    plt.ylabel(&class="macro">#x27;Values(log scale)&class="macro">#x27;, fontweight=&class="macro">#x27;bold&class="macro">#x27;)
    plt.title(&class="macro">#x27;Model Comparison&class="macro">#x27;)
    plt.legend()
    plt.savefig(&class="macro">#x27;Comparison.png&class="macro">#x27;)
# Generate results for both fine-tuned and LORA-tuned models
fine_tuning_result = generater(model_fine_tuning)
lora_tuning_result = generater(model_lora_tuning)
# Plot the prediction comparison
plot_(fine_tuning_result[class="num">0], lora_tuning_result[class="num">0], title=&class="macro">#x27;prediction&class="macro">#x27;)
# Generate the comparison chart for the models
groups_chart(fine_tuning_result, lora_tuning_result, models=[&class="macro">#x27;fine-tuning&class="macro">#x27;, &class="macro">#x27;lora-tuning&class="macro">#x27;])

下一步该怎么试模型

这一轮我们用 LoRA 对 GPT-2 做了微调,并横向比了几种训练设定,结论很直接:没有哪种方法在所有行情里都占优,选哪个取决于你策略样本的分布和标注质量。附带的数据集 llm_data.csv 有 1139.04 KB、train.txt 有 1123.41 KB,脚本 lora-tuning.py 和 test.py 分别 2.64 KB、3.23 KB,直接拉到本地就能复跑。 模型参数尺度摆在那,GPT-2 微调后的输出可能和理想交易信号差得远,但调参和验证的流程是通用的。别指望靠几组例子就定死最佳模型,同架构不同参数量表现都能分化。 真要落地,建议先按本文的 LoRA 配置跑通 test.py,把预测和你的价格行为信号叠一起看吻合度。外汇和贵金属波动大、杠杆高,模型只是辅助,实盘前务必小仓位验证。

常见问题

先按时间戳把真实与生成序列重采样到同一频率,再逐点求差或用 MSE/MAE 量化偏差,重点看拐点处的偏移。
用双栏折线图叠真实序列,左栏放全量微调、右栏放 LoRA,纵坐标统一,方便肉眼比偏差大小。
小布可自动拉取你上传的序列做对齐,并生成偏差对比卡片,省去手写对齐脚本的麻烦。
可能正常,LoRA 低频适配强但尾部泛化弱;可加极端段样本或调高 rank 再测。
下一步用滚动窗口回测两种模型信号,看偏差是否转化为实盘胜率差异,再决定调参方向。