63 行
2.3 KiB
Python
63 行
2.3 KiB
Python
"""
| |||
MT5 `.hcc` history decoder (broker M1 bars), rebuilt 2026-09-27.
| |||
| |||
Layout (measured on FivePercentOnline-Real SP500 2024):
| |||
* 228-byte file header (UTF-16 copyright string)
| |||
* index from byte 228: 18-byte records (u32 idx, u32 update time, u16 ?,
| |||
u32 chunk size, u32 ABSOLUTE chunk offset), one per day, newest first
| |||
* each chunk: 129-byte header (u16 = 129, UTF-16 symbol name, ...), then
| |||
N x 60-byte MqlRates (i64 time, 4 x f64 OHLC, i64 tick_volume, i32 spread,
| |||
i64 real_volume)
| |||
* the first record of a chunk is often a DAILY SUMMARY (time 00:00, tick
| |||
volume = the day's total) - dropped when its tick volume is >= 90% of the
| |||
rest of the chunk, or when it is not strictly before the next bar.
| |||
| |||
Times are the broker clock. Real volume is 0 on CFDs; spread is in points.
| |||
"""
| |||
| |||
from __future__ import annotations
| |||
| |||
import glob
| |||
import os
| |||
import struct
| |||
| |||
import numpy as np
| |||
| |||
REC = np.dtype([("t", "<i8"), ("o", "<f8"), ("h", "<f8"), ("l", "<f8"), ("c", "<f8"),
| |||
("tv", "<i8"), ("sp", "<i4"), ("rv", "<i8")])
| |||
assert REC.itemsize == 60
| |||
| |||
| |||
def read_hcc(path: str) -> np.ndarray:
| |||
b = open(path, "rb").read()
| |||
chunks = []
| |||
k = 0
| |||
while 228 + 18 * (k + 1) <= len(b):
| |||
_, _, _, size, off = struct.unpack_from("<IIHII", b, 228 + 18 * k)
| |||
if off < 228 or off + size > len(b) or size < 129:
| |||
break
| |||
hdr = struct.unpack_from("<H", b, off)[0]
| |||
body = size - hdr
| |||
if hdr == 129 and body > 0 and body % 60 == 0:
| |||
a = np.frombuffer(b, REC, body // 60, off + hdr).copy()
| |||
if len(a) > 1 and (a["tv"][0] >= 0.9 * a["tv"][1:].sum() or a["t"][0] >= a["t"][1]):
| |||
a = a[1:]
| |||
chunks.append(a)
| |||
k += 1
| |||
if not chunks:
| |||
return np.zeros(0, REC)
| |||
a = np.concatenate(chunks)
| |||
a = a[np.argsort(a["t"], kind="stable")]
| |||
keep = np.concatenate([[True], np.diff(a["t"]) > 0])
| |||
return a[keep]
| |||
| |||
| |||
def load_m1(folder: str, years=None) -> np.ndarray:
| |||
files = sorted(glob.glob(os.path.join(folder, "*.hcc")))
| |||
if years is not None:
| |||
files = [f for f in files if int(os.path.basename(f)[:4]) in years]
| |||
a = np.concatenate([read_hcc(f) for f in files])
| |||
a = a[np.argsort(a["t"], kind="stable")]
| |||
a = a[np.concatenate([[True], np.diff(a["t"]) > 0])]
| |||
step = np.diff(a["t"])
| |||
return a, step
|