将您自己的 LLM 集成到 EA 中(第 3 部分):使用 CPU 训练自己的 LLM·进阶篇
🧠

将您自己的 LLM 集成到 EA 中(第 3 部分):使用 CPU 训练自己的 LLM·进阶篇

(2/3)· 没有独显也能跑通训练:从金融数据集构造到 CPU 上微调轻量语言模型的实操分支

含代码示例偏理论 第 2/3 篇
很多人以为本地训练 LLM 必须堆显卡,结果连第一步数据集都没建就放弃了。其实用 CPU 也能完成特定简单任务的微调,关键在语料质量和分词管线是否走通。本篇接着环境篇,把训练链路从零铺开。

用 MT5 批量抓 micro 品种收盘价切片

这段 Python 脚本通过 MetaTrader5 模块直连终端,把带 *micro* 组的交易品种 M5 收盘价按 60 根一组切出来,存成二维表供后续训练或统计。实盘前先确认终端已登录且「允许 DLL 导入」和「允许算法交易」已开,否则 initialize() 会直接返回 False。 脚本里 mt_data_len 写死 2500、sr_len 写死 60,意味着每个品种最多取出 2500-60+1=2441 个不重叠滑动窗口;外层 for j in sbs 配 ct>0 即 break,实际只处理符号列表里第一个 micro 品种,想扩样本得把 ct 限制去掉。 copy_rates_from_pos 从 0 偏移抓最新 2500 根,转 DataFrame 后只留 close 列;while k+1 循环里每轮取 cl_d[k:k+60],首行建表、后续用 xy.loc[len(xy)]=cl_ds 追加。跑完 mt.shutdown() 断连、xy.to_csv 落盘,外汇与贵金属 micro 品种点差大、跳空多,切片统计结论只反映历史形态,实盘有高风险。

MQL5 / C++
mt_data_len=class="num">2500
sr_len=class="num">60
if not mt.initialize():
    print("mt initialize failed!")
else:
    sbs=mt.symbols_get(group=&class="macro">#x27;*micro*&class="macro">#x27;)
    if sbs is not None:
        for i in [mt.TIMEFRAME_M5,]:
            xy=None
            ct=class="num">0
            for j in sbs:
                if ct>class="num">0:
                    class="kw">break
                print(j.name)
                d_=mt.copy_rates_from_pos(j.name,i,class="num">0,mt_data_len)
                df_d=pd.DataFrame(d_)
                cl_d=df_d[&class="macro">#x27;close&class="macro">#x27;]
                k=class="num">0
                while k+class="num">1:
                    if mt_data_len-k>=sr_len:
                        cl_ds=cl_d[k:k+sr_len].tolist()
                        if xy is None:
                            xy=pd.DataFrame([cl_ds])
                        else:
                            xy.loc[len(xy)]=cl_ds
                        k+=class="num">1
                    else:
                        class="kw">break
                ct+=class="num">1
    mt.shutdown()
xy.to_csv(data_file)

「循环收尾与数据落盘的细节」

上面这段是 Python 侧采集脚本的收口逻辑,和 MQL5 终端的联动靠 mt.shutdown() 切断会话。注意 ct+=1 放在 break 之后、shutdown 之前,意味着只有正常走完分支才会计数,异常跳出不会污染样本量。 xy.to_csv(data_file) 把拼好的特征表直接落盘,注释里留了 to_json 的写法但没启用,说明当前流水线优先喂 CSV 给后续训练。外汇与贵金属行情采样本就高波动,这类脚本跑出来的样本若含跳空缺口,需在读表阶段自行过滤,否则模型容易学到噪声。 验证方式很直接:把 data_file 路径指到 MT5 导出的 tick 目录,跑一遍看 xy 行数是否等于你设定的 ct 上限,差一行就说明 break 提前触发了。

MQL5 / C++
else:
                class="kw">break
            ct+=class="num">1
    mt.shutdown()
    
    # print(len(xy),"  ",len(xy_list))
    xy.to_csv(data_file)
    # xy.to_json(f&class="macro">#x27;llm_data.json&class="macro">#x27;)
    class="kw">return xy

◍ 把 MT5 行情序列喂进 GPT-2 分词器

做本地小模型训练时,第一步是把 MT5 拉到的数字序列变成大模型能读的整数流。示例里直接调用 get_data() 拿返回值,而不去读 csv——后者会在存读过程中多出一列和一行表头,得手动 iloc[1:,1:] 切掉,能省一步就省一步。

分词器选的是 tiktoken 的 gpt2 预训练编码,特殊标记用 <endoftext> 同时当序列头尾。验证集切分靠 val_cut=len(data)//10,也就是总数据的前 10% 进 val_tokens,剩下 90% 进 train_tokens;想调比例就改那个 10。

写 bin 文件时 header[0]=20240520 这个魔数别手滑。后续加载训练数据时框架会校验它,对不上直接报错,排查起来很隐蔽。 原数据本就是数字,为什么还要过一遍分词器?取决于你给模型定的任务规划——如果模型最终要吃类 GPT 的 token 流,统一编码通道比直接塞数组更稳。示例数据集每条长度固定且总量小,没做 padding 和 mask;真要搞复杂数据集,清洗和填充这一步极大影响模型质量。 第一次跑 tiktoken 会联网去 huggingface 下模型文件,MT5 终端机若不通外网要提前备好本地 encoder,否则卡在编码阶段。

MQL5 / C++
data=get_data()
# data=pd.read_csv(data_file)
# data=data.iloc[class="num">1:,class="num">1:]
    enc = tiktoken.get_encoding("gpt2")
    encode = lambda s: enc.encode_ordinary(s)
    eot = enc._special_tokens[&class="macro">#x27;<|endoftext|>&class="macro">#x27;]
    train_tokens=[]
    val_tokens=[]
    val_cut=len(data)class=class="str">"cmt">//class="num">10
def data_to_file(path, tks):
    header = np.zeros(class="num">256, dtype=np.int32)
    header[class="num">0] = class="num">20240520
    header[class="num">1] = class="num">1
    header[class="num">2] = len(tks)
    toks_np = np.array(tks, dtype=np.uint16)
    with open(path, "wb") as f:
        f.write(header.tobytes())
        f.write(toks_np.tobytes())
for i,r in data.iterrows():
ser=r.tolist()
ser= &class="macro">#x27;&class="macro">#x27;.join(str(elem) for elem in ser)
tokens = encode(ser)
if i< val_cut:
    val_tokens.append(eot)
    val_tokens.extend(tokens)
    enc_f = os.path.join(DATA_DIR, "val_data.bin")
    data_to_file(enc_f, val_tokens)
else:
    train_tokens.append(eot)
    train_tokens.extend(tokens)
    enc_f = os.path.join(DATA_DIR, "train_data.bin")
    data_to_file(enc_f, train_tokens)
class="kw">import MetaTrader5 as mt
class="kw">import pandas as pd
class="kw">import numpy as np
class="kw">import os
class="kw">import tiktoken
DATA_DIR = os.path.dirname(__file__)
data_file = os.path.join(DATA_DIR, "llm_data.csv")
def get_data():
    mt_data_len=class="num">2500
    sr_len=class="num">60
    if not mt.initialize():
        print("mt initialize failed!")
    else:
        sbs=mt.symbols_get(group=&class="macro">#x27;*micro*&class="macro">#x27;)
        if sbs is not None:
            # for i in [mt.TIMEFRAME_M5,mt.TIMEFRAME_M15,mt.TIMEFRAME_H1,mt.TIMEFRAME_D1]:
            for i in [mt.TIMEFRAME_M5,]:
                xy=None
                # xy_list=[]
                ct=class="num">0
                for j in sbs:
                    if ct>class="num">0:
                        class="kw">break
                    print(j.name)

把K线切片喂给小模型的落盘写法

这段脚本干的事很直接:从 MT5 拉完指定品种和周期的收盘价序列后,按 sr_len 窗口长度逐根滑动,把每段收盘价子序列堆进 DataFrame,最后整表写 csv 供后续微调或推理用。 核心循环里 while k+1 实际是靠 mt_data_len-k>=sr_len 判断剩余长度,不够就 break,因此当 mt_data_len=1000、sr_len=64 时,大约能切出 937 个样本行,样本数随数据长度线性递减。 data_to_file 函数则走另一条路:用 256 个 int32 的头块(首字段写死 20240520 作版本标记,次字段置 1,第三字段存品种数),后面紧接 uint16 的 token 流,直接 wb 二进制落盘,比 csv 省解析开销。 主程序里按 len(data)//10 做验证集切分,前 10% 行编码后塞 val_tokens 并补 eot 终止符;tiktoken 用 gpt2 普通编码,不处理特殊 token,意味着价格数字串会被拆成子词,训练前最好先确认词表覆盖度。外汇与贵金属波动剧烈,这类序列用于模型训练仅作概率性辅助,实盘仍需人工把关。

MQL5 / C++
d_=mt.copy_rates_from_pos(j.name,i,class="num">0,mt_data_len)
df_d=pd.DataFrame(d_)
cl_d=df_d[&class="macro">#x27;close&class="macro">#x27;]
k=class="num">0
while k+class="num">1:
	if mt_data_len-k>=sr_len:
		cl_ds=cl_d[k:k+sr_len].tolist()
		if xy is None:
			xy=pd.DataFrame([cl_ds])
			# xy_list=[cl_ds]
		else:
			xy.loc[len(xy)]=cl_ds
			# xy_list.append(cl_ds)
		k+=class="num">1
	else:
		class="kw">break
ct+=class="num">1
mt.shutdown()

# print(len(xy),"  ",len(xy_list))
xy.to_csv(data_file)
# xy.to_json(f&class="macro">#x27;llm_data.json&class="macro">#x27;)
class="kw">return xy
def data_to_file(path, tks):
	header = np.zeros(class="num">256, dtype=np.int32)
	header[class="num">0] = class="num">20240520
	header[class="num">1] = class="num">1
	header[class="num">2] = len(tks)
	toks_np = np.array(tks, dtype=np.uint16)
	with open(path, "wb") as f:
		f.write(header.tobytes())
		f.write(toks_np.tobytes())
if __name__=="__main__":
	data=get_data()
	# data=pd.read_csv(data_file)
	# data=data.iloc[class="num">1:,class="num">1:]
	enc = tiktoken.get_encoding("gpt2")
	encode = lambda s: enc.encode_ordinary(s)
	eot = enc._special_tokens[&class="macro">#x27;<|endoftext|>&class="macro">#x27;]
	train_tokens=[]
	val_tokens=[]
	val_cut=len(data)class=class="str">"cmt">//class="num">10
	for i,r in data.iterrows():
		ser=r.tolist()
		ser=&class="macro">#x27;&class="macro">#x27;.join(str(elem) for elem in ser)
		# ser = ser.strip()
		tokens = encode(ser)
		if i< val_cut:
			val_tokens.append(eot)
把语料巡检交给小布
小布盯盘已内置 AIGC 诊断,可帮你核对金融文本的分词异常与标签分布,打开对应品种页即可看到,你只管调训练超参。

常见问题

倾向只能处理相对简单的特定任务,例如基于历史注释做情绪标签归类或短句分类,复杂推理仍受算力限制。
不可以。两者分词空间与特殊 token 定义不同,混用会导致向量错位,训练前须固定一种预训练分词器。
目前小布内置的是云端 AIGC 诊断,本地私有模型需经导出规范接入,具体格式见知识库工程页,外汇贵金属波动高风险,接入后也先离线回测。
建议保留并单独打标,否则模型可能把非连续行情当成序列依赖,CPU 训练本就样本少,噪声会更放大。