拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C++将CSV/TXT转为MT4 HST文件:二进制格式解析与实现

简介针对MT4平台历史数据导入需求csv2hst提供了一套C源码实现可将外部CSV格式交易数据转换为MT4专用的HST二进制格式解决第三方数据无法直接用于MT4回测与分析的问题。该工具面向量化开发者、数据工程师及需要整合多源历史数据的MT4用户完整演示了CSV解析、数据结构组织、HST文件头与记录写入、时间戳格式转换及错误处理等关键环节。整个压缩包共22个文件约720KB核心包括一个C源文件、Visual Studio解决方案与工程配置.sln/.vcxproj并附带已编译的exe、pdb调试信息、obj中间文件及tlog生成日志便于代码对照、运行调试与二次开发。已有379人学习下载学习者可结合完整工程与可执行文件直接运行验证也可从源码中学习MT4数据存储机制、C二进制文件操作及性能优化思路是研究与实现交易数据转换的实用参考。1. 用 C 把 CSV/TXT 转成 MT4 的 HST 文件为什么值得自己写MT4 客户端自带的历史数据导入功能只认特定结构的 CSV而且对时间戳格式、跳空处理、柱体合并规则都做了内部假设。csv2hst这个场景要解决的是你手里有外部数据源比如券商导出的 tick 级 CSV、自建量化系统的成交记录甚至是多年份的日线数据想灌进 MT4 做回测或复盘但常规的“文件-导入”路径要么丢精度、要么报错、要么速度慢到无法接受。用 C 写转换器而不是 Python 脚本不是因为 C 更“高级”而是 HST 是纯二进制定长结构C 的struct可以直接对映磁盘布局内存映射文件处理 GB 级数据也不会有 GIL 和字符串编码层面的额外开销。这篇实战用的就是这个思路把 HST 格式拆开、把 CSV 解析要处理的脏数据问题讲清楚、最后给出一个可编译可运行的最小实现。适合的人群是已经能跑通 MT4 回测、但受限于数据导入这一环的人以及想理解 MT4 本地存储结构的读者。目标很简单你拿到一份带日期的价格序列能在一分钟内把它变成 MT4 里历史数据中心能正常加载的 HST 文件。2. 拆解 MT4 的 HST 文件格式从头部到柱体的字节布局2.1 HST 头部结构体版本、符号、时间与 13 个保留字段HST 是 MT4 用来存储周期 K 线M1 到 MN1的二进制容器文件扩展名.hst实际内容是一段固定长度的头后面紧跟等长的价格柱记录。先看头部MT4 的history头定义在标准 SDK 里能翻到实际布局如下#pragma pack(push, 1) struct HSTHeader { uint32_t version; // 当前版本MT4 Build 600 常写 400 char copyright[64]; // 版权字段可写空串 char symbol[16]; // 货币对或品种名如 EURUSD uint32_t period; // 周期1M1, 5M5, 15M15, 30M30, 60H1, 240H4, 1440D1 uint32_t digits; // 小数位数EURUSD 为 5 或 4 uint32_t timesign; // 时间戳标记一般写 0 uint32_t last_sync; // 最后一次同步时间Unix 时间戳 uint32_t unused[13]; // 13 个保留 4 字节共 52 字节 }; #pragma pack(pop)对齐是关键如果不加#pragma pack(push, 1)编译器会在char copyright[64]之后按 4 字节对齐插入填充头部长度的计算就错了。整个头部的字节数是 4 64 16 4*5 52 132 字节。网上有些老代码把头部写 148 字节那是因为unused数组多写了 4 个元素或者copyright用了 80 字节——这类不兼容的结构体写出的文件MT4 能读但会认为版本异常甚至直接拒绝加载。参数上需要注意两点。symbol必须与 MT4 的市场报价窗口里的品种名完全一致大小写都算period是枚举值不是分钟数比如周线写 10080 是错的应写 16385digits决定了 MT4 图表上显示几位小数但不会影响内部浮点精度写错的表现是报价数字少了尾数。last_sync可以填 0MT4 会用文件修改时间代替。2.2 价格柱记录为什么必须用 int64 存时间戳头部之后是连续的柱体记录每根柱 44 字节结构如下struct HSTBar { int64_t ctm; // 打开时间Unix 秒 double open; double low; double high; double close; double volume; // 成交量双精度存储 uint32_t spread; };时间戳是 8 字节的int64_t很多老的教程写 4 字节的uint32_t这在 2038 年问题之外还有另一个坑MT4 的 D1 及以上周期部分平台会用微秒级时间戳做内部排序但 HST 文件里存的还是秒高位补零即可。volume是double不是int这点和 CSV 里常见的tick_volume不同——如果你从 CSV 读出来的是整数直接赋值给double没问题但不要用int去对映这个字段否则后面 4 字节会错位spread读到的一定是垃圾值。柱体必须按时间正序排列MT4 在加载时会用二分查找定位数据如果相邻两根柱时间戳差值小于周期秒数比如 M1 周期两柱相差 30 秒加载会报“历史数据错误”。这一点在合并 tick 数据时最容易踩。另一个需要避免的是跳空处理MT4 的 HST 结构里没有独立的“跳空标记”跳空体现在high与下一根open之间存在缺口转换时无需填充但如果你在 CSV 里用了 0 值代表无成交写进 HST 后图表上会画出从 0 到前收盘的竖线所以转换前要过滤掉 open/high/low/close 任一字段为 0 的行。2.3 写入模式追加、覆盖与安全落盘MT4 的历史数据文件由客户端独占管理如果你的 MT4 正在运行写入会被系统锁定或造成加载异常。常见做法是先关闭 MT4写完文件再启动。但如果你写的是一个守护进程需要轮询检测文件是否被占用std::FILE* f std::fopen(path, rb); if (!f) { // 文件不存在用 wb 新建 f std::fopen(path, wb); } else { // 文件存在且可打开说明 MT4 未锁定 }常见做法是转换时写临时文件写完后rename替换正式文件这样即使转换中途崩溃也不会留下半个 HST。写入时必须用fflush后再fclose不能依赖程序退出时的隐式刷新——曾有案例是程序异常退出头部已落盘但柱体不完整MT4 打开后显示“文件损坏”。3. CSV/TXT 解析层字符集、分隔符与脏数据过滤3.1 字符集判断UTF-8 与 ANSI 的兼容处理CSV/TXT 的来源决定字符集Windows 记事本默认 ANSIGBK从 Linux 服务器导出的多为 UTF-8MT4 自带的转换工具只认 ANSI。你用 C 处理时最稳妥的办法是统一转成 UTF-8 再解析还是直接按 ANSI 读我一般建议先读文件前 3 个字节判断 BOM。有EF BB BF是 UTF-8有FF FE是 UTF-16 LE都没有则按 ANSI 处理。具体实现std::string detectAndSkipBOM(FILE* f) { unsigned char b[3] {0}; size_t r fread(b, 1, 3, f); if (r 3 b[0]0xEF b[1]0xBB b[2]0xBF) return utf8; // 跳过 BOM后续用 UTF-8 解码 if (r 2 b[0]0xFF b[1]0xFE) { fseek(f, 2, SEEK_SET); return utf16le; // 返回后走宽字符读取分支 } fseek(f, 0, SEEK_SET); // 没有 BOM回退到开头按 ANSI 读 return ansi; }注意按 ANSI 读取时如果 CSV 里包含欧元符号、中文备注你会得到乱码字节。这里有个取舍——HST 文件本身只存数值CSV 里的非数字列都该忽略所以 ANSI 读入后直接strtod解析数值遇到解析失败的行跳过即可不需要做码制转换。只有 CSV 第一行的表头如Date,Open,High,Low,Close,Volume可能是非 ASCII 字符跳过表头的逻辑用行首字母判断就行。3.2 分隔符自识别逗号、制表符还是空格CSV 不一定是逗号。MT4 导出的历史数据有CSV和PRN两种格式后者是制表符分隔。外部数据源更常见的是空格或不定长空白分隔。写解析器时应该做自适应检测而不是让用户传参指定。检测逻辑很简单读取前 5 行统计每行中逗号、制表符、单空格的出现次数取最稳定的那个作为分隔符。注意“稳定”指的是每行数量相同——如果某行逗号数量突然变少多半是该行里有字符串字段包含了逗号比如备注列统计时要排除被引号包裹的部分。char detectDelimiter(const std::vectorstd::string sampleLines) { int commas0, tabs0, spaces0; for (auto ln : sampleLines) { bool inQuote false; for (char c : ln) { if (c ) inQuote !inQuote; if (inQuote) continue; if (c ,) commas; else if (c \t) tabs; else if (c ) spaces; } } if (commas tabs commas spaces) return ,; if (tabs spaces) return \t; return ; // 空格分隔的最后一搏 }这段逻辑里inQuote的处理是必要的RFC 4180 允许字段内含逗号但必须用双引号包裹。如果你不做引号状态跟踪带注释列的 CSV 会把分隔符误判成逗号因为引号内还有逗号导致整个解析偏移。3.3 时间格式与浮点精度LL 回测对不上账的元凶CSV 里的时间字段常见三种写法2023.01.15 12:30、2023-01-15 12:30:00、15.01.2023 12:30。MT4 的导入向导只认第一种但自己写转换器就不必受这个限制——你需要做的是把所有格式统一解析成 Unix 秒。time_t parseDateTime(const std::string s) { struct tm t {0}; int y, mo, d, h, mi, sec; // 支持 2023.01.15 12:30 和 2023-01-15 12:30:00 if (sscanf(s.c_str(), %d.%d.%d %d:%d, y, mo, d, h, mi) 5) { // 点分日期 } else if (sscanf(s.c_str(), %d-%d-%d %d:%d:%d, y, mo, d, h, mi, sec) 5) { // 横线日期 } else { return -1; // 解析失败 } t.tm_year y - 1900; t.tm_mon mo - 1; t.tm_mday d; t.tm_hour h; t.tm_min mi; t.tm_sec sec; return timegm(t); // 使用 UTC不做本地时区修正 }这里的关键决策是timegm而不是mktime。mktime使用系统本地时区如果你在 UTC8 的机器上把 UTC 时间的 CSV 用mktime转换写进 HST 的时间戳会少 8 小时图表上的 K 线整体向左偏移回测结果看着“很接近”但始终对不上逐笔。MT4 内部所有 K 线时间都以服务器时区为准建议你转换前先确认 CSV 的时间戳到底是 UTC 还是服务器本地时间——如果是 MT4 导出的通常已是服务器本地时间用timegm处理会写错。一个保险做法让转换器接受一个--utc-offset参数默认 0用户根据数据源时区自己调。4. 核心转换流程从 CSV 行到 HST 柱体的完整 C 实现4.1 按周期聚合 tick 数据合柱规则与边界条件CSV 数据可能是 tick 级每毫秒一条也可能已经是分钟级。如果源数据是 tick直接转 HST 行不行可以但 MT4 的 HST 文件至少是按 M1 组织写 tick 进去会导致同一秒出现多根柱加载异常。所以必须做聚合。聚合的规则是按时间窗口切分窗口内第一笔 tick 的价格作为 open最后一笔作为 close窗口内最高价作为 high最低价作为 low成交量累加。边界上要注意tick 的时间戳恰好落在窗口边界时应归入后一个窗口而不是前一个。struct PriceBar { time_t t; double open, high, low, close, vol; }; std::vectorHSTBar aggregateToPeriod( const std::vectorTickData ticks, int periodSeconds) { std::vectorHSTBar out; if (ticks.empty()) return out; PriceBar cur; cur.t ticks[0].t / periodSeconds * periodSeconds; // 窗口起点取整 cur.open cur.high cur.low cur.close ticks[0].price; cur.vol ticks[0].volume; for (size_t i 1; i ticks.size(); i) { time_t wstart ticks[i].t / periodSeconds * periodSeconds; if (wstart ! cur.t) { // 新窗口开始 // 收尾旧窗口 HSTBar b; b.ctm cur.t; b.open cur.open; b.high cur.high; b.low cur.low; b.close cur.close; b.volume cur.vol; b.spread 0; out.push_back(b); // 初始化新窗口 cur.t wstart; cur.open cur.high cur.low cur.close ticks[i].price; cur.vol ticks[i].volume; } else { cur.close ticks[i].price; cur.high std::max(cur.high, ticks[i].price); cur.low std::min(cur.low, ticks[i].price); cur.vol ticks[i].volume; } } // 别忘了最后一个窗口 // 代码与上面相同略 return out; }这段代码里有几个容易写错的地方。窗口起点用ticks[i].t / periodSeconds * periodSeconds取整处理的是跳空时段——如果 12:00 到 12:05 之间没有任何 tick那么 12:05 的第一根 tick 会直接落进 12:05 窗口生成一根只包含该 tick 的柱体这是正确的行为。cur.vol是累计值但如果你后续发现 MT4 图表的成交量比实际大很多检查 CSV 里的 volume 字段是不是已经做过乘数换算有些平台把 volume 存成 1/100 手。4.2 写入文件并生成 .hst 的完整函数有了聚合好的柱体数组写入文件就非常简单了bool writeHST(const std::string filename, const std::string symbol, uint32_t period, uint32_t digits, const std::vectorHSTBar bars) { // 如果文件已存在先删除再重建避免旧数据残留 std::remove(filename.c_str()); std::FILE* f std::fopen(filename.c_str(), wb); if (!f) return false; HSTHeader hdr; memset(hdr, 0, sizeof(hdr)); hdr.version 400; hdr.period period; hdr.digits digits; hdr.last_sync static_castuint32_t(time(nullptr)); strncpy(hdr.symbol, symbol.c_str(), sizeof(hdr.symbol) - 1); // copyright 留空无需填充内容 fwrite(hdr, sizeof(hdr), 1, f); for (auto bar : bars) { fwrite(bar, sizeof(HSTBar), 1, f); } fflush(f); fclose(f); return true; }调用时要注意三个参数的一致性period是枚举值而非秒数如果你从用户输入里读“60”想表示 H1需要做一层映射60 不转成 60 直接用而要映射为周期枚举值 60——因为 MT4 的枚举值 60 恰好等于 H1 的分钟数所以此处可以直接用但 D1 就必须映射了symbol必须与 MT4 平台里的品种名一致如果文件名和 symbol 不匹配MT4 加载时会用文件名为主symbol 字段写错会导致图表显示正确但“打开离线历史数据”时找不到文件digits在你做跨平台复用时要注意——如果你同时在 MT5 里用同样的数据MT5 的 digits 是独立放在符号属性里的HST 文件里这个字段不会被读取。4.3 周期映射表与文件名命名规则MT4 会自动把EURUSD1.hst识别为 EURUSD 的 M1 数据EURUSD240.hst识别为 H4。文件名的规则是“品种名 周期数字 .hst”周期数字与头部结构体里的period必须一致。下面这个映射表是我会写进转换器注释里的周期名称枚举值文件名后缀M11EURUSD1M55EURUSD5M1515EURUSD15M3030EURUSD30H160EURUSD60H4240EURUSD240D11440EURUSD1440W110080实际用 16385MN143200实际用 32769注意最后两行W1 和 MN1 在 MT4 里的枚举值不是分钟数。W1 的内部值 16385MN1 是 32769如果你按分钟数写 10080 和 43200生成的文件后缀名虽然没问题但 MT4 加载时会把它识别为未知周期。这是我最开始写转换器时踩过的坑之一网上不少老帖子也错在这里。你生成文件后放到 MT4 数据目录的正确结构是这样的MQL4\Files\ 不是这里实际位置是 MT4 客户端目录下的history\服务器名\子目录。如果你用的是模拟账户路径类似C:\Users\...\AppData\Roaming\MetaQuotes\Terminal\实例ID\history\MetaQuotes-Demo\。找不准位置时在 MT4 里打开“文件-打开数据文件夹”上一层目录里找history。5. HST 转换的 3 个验证技巧不启动 MT4 也能确认文件正确5.1 用 44 字节步长校验柱体边界写完文件后先别急着启动 MT4。用下面这段代码从头到尾扫描文件确认尺寸和数据完整性python3 -c import struct, sys path rC:\...\EURUSD1.hst raw open(path, rb).read() print(file size:, len(raw)) print(header size: 132, bars:, (len(raw)-132)/44) assert (len(raw)-132) % 44 0, 柱体边界错误 # 解析最后一根柱时间戳应递增 off len(raw) - 44 ctm, o, l, h, c, v, s struct.unpack(qdddddi, raw[off:off44]) print(last bar ctm:, ctm, close:, c) 这个 Python 读取并不影响 MT4只是做个快速校验。qdddddi对应小端 int64、四个 double、int32正好 485448 字节——等等这里你发现一个问题HSTBar 的结构体里ctm是 int64 占 8 字节open/low/high/close/volume是 5 个 double 占 40 字节spread是 uint32 占 4 字节合计 52 字节不是 44。这里要纠正MT4 的 HST 柱体实际是 44 字节volume是doublespread也是 4 字节但结构体存在__declspec(align(8))对齐实际有 4 字节填充。最好在 C 里打印sizeof(HSTBar)来确认——我给你的结构体定义其实是 52 字节这就是为什么要求在结构体前面加#pragma pack(push, 1)把它压回 44 字节。压缩后qdddddi的 48 字节也不对应为 8854 52。这个差异值得说清楚提示MT4 对 HST 柱体不要求结构体对齐到 8 字节但如果你用编译器默认对齐sizeof(HSTBar)会是 56 字节ctm8 40 spread4 填充 4。只有强pack(push,1)后才是 44 字节。MT4 读取时按 44 字节定长读取如果你的文件按 56 写入MT4 会解析错乱。5.2 用 Excel 或文本编辑器打开 .hst 看二进制尾巴这个验证技巧花 30 秒就能做用 VS Code 的十六进制查看器打开生成的 .hst拉到文件末尾。最后的 44 个字节应该是整齐的 ASCII 数字序列——等等不是 ASCII是二进制。更实际的做法是看文件大小如果你有 1000 根柱体文件大小应该等于 132 100044 44132 字节。如果大小差了 121000 字节几乎可以肯定是结构体对齐问题。这种尺寸检查是最快的回归测试。5.3 在 MT4 里用“离线图表”确认周期映射启动 MT4点击“文件-打开离线历史数据”找到你的品种名双击打开。观察两点其一图表右侧多出来的柱体时间戳是否连续、是否按周期对齐其二右键“属性”里显示的“历史数据”数量是否与你的 CSV 行数匹配不完全一致是正常的因为 MT4 会移除部分柱体头部的情况在这个操作里不会发生数据量应该几乎相等。如果打开后图表空白优先检查文件名和 symbol 是否完全匹配。如果柱体时间对不上检查你的parseDateTime是否用了timegm导致 UTC 偏差。如果文件能打开但 MT4 提示“历史数据错误”可以尝试把文件移到别的数据目录看是文件本身损坏还是路径不对——这个技巧帮我定位过第三方平台 HST 文件“能查不能画”的怪问题。进阶用法写一个小批量脚本遍历history\服务器名下所有*.hst逐个检查(size-132) % 44 0不满足的文件用转换器重构。我通常在每个月底跑一次这个巡检配合 MT4 的归档功能把旧数据导成 CSV 再做增量转换省的每次重导全量数据。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门