kronos-mql5/Kronos_Python/kronos_reference_capture.py
2026-07-05 05:07:42 +00:00

275 lines
12 KiB
Python

#!/usr/bin/env python3
"""
kronos_reference_capture.py
===========================
First build artifact for the "Kronos-small inference in native MQL5" project.
What it does
------------
1. Loads Kronos-small + Kronos-Tokenizer-base and PINS their exact resolved
architecture to JSON (so the tokenizer config gets captured automatically).
2. Freezes ONE fully reproducible input window and saves it as the contract the
MQL5 side will read verbatim -- MQL5 never re-synthesizes the input, it reads
these bytes, so the data generation here does not need to be portable.
3. Replicates KronosPredictor's preprocessing EXACTLY (per-window z-score with
eps=1e-5, population std, clip +/-5) and saves every intermediate.
4. Captures a DETERMINISTIC verification ladder -- no sampling anywhere -- at each
model boundary, so each MQL5 component can be checked against ground truth as
it is built:
x_norm -> (s1_ids, s2_ids) -> s1_logits -> s2_logits -> reconstruction
5. (Optional) one SEEDED end-to-end forecast as a coarse full-pipeline check.
This one is stochastic; it is documented as such and is not a bit-exact target.
Every array is written twice:
- <name>.npy for Python-side inspection
- <name>.bin + manifest.json little-endian, row-major, for MQL5 FileReadArray
Usage
-----
Run from inside the cloned Kronos repo (so `from model import ...` resolves), or
point --kronos_repo at it:
python kronos_reference_capture.py --kronos_repo /path/to/Kronos --out ./kronos_refs
Notes
-----
- CPU + float32 are forced for reproducibility. Do not run this on GPU.
- np.std defaults to population std (ddof=0); KronosPredictor relies on that, so
MQL5 must also use population std (divide by N, not N-1).
"""
import argparse
import json
import os
import sys
import numpy as np
def to_py(v):
"""Make a config value JSON-serializable."""
try:
import torch
if isinstance(v, torch.Tensor):
return v.tolist()
except Exception:
pass
if isinstance(v, (np.integer,)):
return int(v)
if isinstance(v, (np.floating,)):
return float(v)
if isinstance(v, (np.ndarray,)):
return v.tolist()
return v
def dump_config(obj, curated_keys):
"""Best-effort capture of a HF-mixin module's resolved config."""
cfg = {}
# 1) try the stored hub-mixin config dict
for attr in ("config", "_hub_mixin_config", "_config"):
c = getattr(obj, attr, None)
if isinstance(c, dict):
cfg.update({k: to_py(v) for k, v in c.items()})
# 2) scrape curated attributes directly off the instantiated module
for k in curated_keys:
if hasattr(obj, k):
cfg[k] = to_py(getattr(obj, k))
return cfg
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--kronos_repo", default=".",
help="Path to cloned Kronos repo (the dir containing model/)")
ap.add_argument("--out", default="./kronos_refs")
ap.add_argument("--tokenizer_id", default="NeoQuasar/Kronos-Tokenizer-base")
ap.add_argument("--model_id", default="NeoQuasar/Kronos-small")
ap.add_argument("--lookback", type=int, default=256, help="context length, <=512")
ap.add_argument("--pred_len", type=int, default=16)
ap.add_argument("--clip", type=float, default=5.0)
ap.add_argument("--eps", type=float, default=1e-5)
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--skip_forecast", action="store_true",
help="Skip the seeded end-to-end forecast (stochastic) step")
args = ap.parse_args()
sys.path.insert(0, os.path.abspath(args.kronos_repo))
import torch
import pandas as pd
from model import Kronos, KronosTokenizer
torch.manual_seed(args.seed)
np.random.seed(args.seed)
torch.set_grad_enabled(False)
device = "cpu" # reproducible references only
os.makedirs(args.out, exist_ok=True)
manifest = {"arrays": {}, "scalars": {}, "notes": []}
def save(name, arr, dtype):
a = np.ascontiguousarray(np.asarray(arr).astype(dtype))
np.save(os.path.join(args.out, name + ".npy"), a)
a.tofile(os.path.join(args.out, name + ".bin"))
manifest["arrays"][name] = {
"file": name + ".bin",
"dtype": np.dtype(dtype).name,
"shape": list(a.shape),
"order": "little-endian, row-major (C)",
}
print(f" saved {name:24s} shape={tuple(a.shape)} dtype={np.dtype(dtype).name}")
# ------------------------------------------------------------------ load
print("Loading models (CPU, float32) ...")
tok = KronosTokenizer.from_pretrained(args.tokenizer_id).to(device).float().eval()
mdl = Kronos.from_pretrained(args.model_id).to(device).float().eval()
tok_cfg = dump_config(tok, [
"d_in", "d_model", "n_heads", "ff_dim", "enc_layers", "dec_layers",
"s1_bits", "s2_bits", "codebook_dim",
])
mdl_cfg = dump_config(mdl, [
"d_model", "n_heads", "ff_dim", "n_layers", "s1_bits", "s2_bits",
"s1_vocab_size", "s2_vocab_size", "learn_te",
])
with open(os.path.join(args.out, "config_tokenizer.json"), "w") as f:
json.dump(tok_cfg, f, indent=2)
with open(os.path.join(args.out, "config_predictor.json"), "w") as f:
json.dump(mdl_cfg, f, indent=2)
print("Pinned tokenizer config:", json.dumps(tok_cfg))
print("Pinned predictor config:", json.dumps(mdl_cfg))
# ------------------------------------------------- freeze an input window
L, P = args.lookback, args.pred_len
price_cols = ["open", "high", "low", "close"]
feat_cols = price_cols + ["volume", "amount"]
stamp_cols = ["minute", "hour", "weekday", "day", "month"]
idx = pd.date_range("2023-01-02 00:00:00", periods=L + P, freq="h")
rw = np.cumsum(np.random.randn(L + P).astype(np.float64)) * 0.5 + 100.0
close = rw
open_ = np.concatenate([[close[0]], close[:-1]])
high = np.maximum(open_, close) + np.abs(np.random.randn(L + P)) * 0.2
low = np.minimum(open_, close) - np.abs(np.random.randn(L + P)) * 0.2
volume = np.abs(np.random.randn(L + P)) * 1000.0 + 500.0
amount = volume * (open_ + high + low + close) / 4.0
df_all = pd.DataFrame(
{"open": open_, "high": high, "low": low, "close": close,
"volume": volume, "amount": amount}, index=idx)
hist_df = df_all.iloc[:L].copy()
x_ts = pd.Series(df_all.index[:L])
y_ts = pd.Series(df_all.index[L:L + P])
def stamps(ts):
out = np.zeros((len(ts), 5), dtype=np.float32)
out[:, 0] = ts.dt.minute
out[:, 1] = ts.dt.hour
out[:, 2] = ts.dt.weekday
out[:, 3] = ts.dt.day
out[:, 4] = ts.dt.month
return out
x_raw = hist_df[feat_cols].values.astype(np.float32) # (L, 6)
x_stamp = stamps(x_ts) # (L, 5)
y_stamp = stamps(y_ts) # (P, 5)
# --------------------------------------------- preprocessing (exact copy)
x_mean = x_raw.mean(axis=0) # (6,)
x_std = x_raw.std(axis=0) # (6,) population std, ddof=0
x_norm = (x_raw - x_mean) / (x_std + args.eps)
x_norm = np.clip(x_norm, -args.clip, args.clip).astype(np.float32)
print("\nStage 0 preprocessing")
save("x_raw", x_raw, np.float32)
save("x_norm", x_norm, np.float32)
save("x_mean", x_mean, np.float32)
save("x_std", x_std, np.float32)
save("x_stamp", x_stamp, np.float32)
save("y_stamp", y_stamp, np.float32)
xt = torch.from_numpy(x_norm)[None].to(device) # (1, L, 6)
xs = torch.from_numpy(x_stamp)[None].to(device) # (1, L, 5)
# ----------------------------------------------- Stage 1: tokenizer.encode
print("\nStage 1 tokenizer.encode (half=True -> [s1_ids, s2_ids])")
z_idx = tok.encode(xt, half=True)
s1_ids = z_idx[0] if isinstance(z_idx, (list, tuple)) else z_idx
s2_ids = z_idx[1] if isinstance(z_idx, (list, tuple)) else None
save("s1_ids", s1_ids.cpu().numpy(), np.int32)
if s2_ids is not None:
save("s2_ids", s2_ids.cpu().numpy(), np.int32)
# --------------------------------- Stage 2: predictor decode_s1 / decode_s2
print("\nStage 2 predictor logits (deterministic, teacher-forced)")
try:
s1_logits, context = mdl.decode_s1(s1_ids, s2_ids, xs)
save("s1_logits_full", s1_logits.cpu().numpy(), np.float32)
save("s1_logits_last", s1_logits[0, -1].cpu().numpy(), np.float32)
# deterministic condition for s2: argmax of the last-step s1 logits
s1_pick = s1_logits[:, -1:].argmax(dim=-1) # (1, 1)
save("s1_pick_last", s1_pick.cpu().numpy(), np.int32)
try:
s2_logits = mdl.decode_s2(context, s1_pick)
save("s2_logits_last", s2_logits.reshape(-1).cpu().numpy(), np.float32)
except Exception as e:
manifest["notes"].append(f"decode_s2 capture failed: {e!r} "
f"(check arg convention vs auto_regressive_inference)")
print(" [warn] decode_s2 failed:", repr(e))
except Exception as e:
manifest["notes"].append(f"decode_s1 capture failed: {e!r}")
print(" [warn] decode_s1 failed:", repr(e))
# ------------------------------------ Stage 3: tokenizer.decode round-trip
print("\nStage 3 tokenizer.decode (reconstruction of the context window)")
try:
recon = tok.decode([s1_ids, s2_ids], half=True) # (1, L, 6) normalized
recon_np = recon[0].cpu().numpy()
save("recon_norm", recon_np, np.float32)
save("recon_denorm", recon_np * (x_std + args.eps) + x_mean, np.float32)
except Exception as e:
manifest["notes"].append(f"tokenizer.decode capture failed: {e!r}")
print(" [warn] tokenizer.decode failed:", repr(e))
# ------------------------------- Stage 4: seeded end-to-end forecast (opt.)
if not args.skip_forecast:
print("\nStage 4 seeded end-to-end forecast (STOCHASTIC, seed-pinned)")
try:
from model import KronosPredictor
torch.manual_seed(args.seed)
predictor = KronosPredictor(mdl, tok, device=device,
max_context=512, clip=args.clip)
pred_df = predictor.predict(
df=hist_df, x_timestamp=x_ts, y_timestamp=y_ts,
pred_len=P, T=1.0, top_k=0, top_p=0.9,
sample_count=1, verbose=False)
save("forecast_seeded", pred_df[feat_cols].values, np.float32)
manifest["notes"].append(
"forecast_seeded uses torch.manual_seed(seed), sample_count=1, "
"T=1.0, top_p=0.9 -- reproducible in PyTorch but NOT a bit-exact "
"MQL5 target; use the logits-level refs for exact verification.")
except Exception as e:
manifest["notes"].append(f"seeded forecast failed: {e!r}")
print(" [warn] forecast failed:", repr(e))
# ----------------------------------------------------------- write manifest
manifest["scalars"] = {
"lookback": L, "pred_len": P, "clip": args.clip, "eps": args.eps,
"seed": args.seed, "feature_order": feat_cols, "stamp_order": stamp_cols,
"std": "population (ddof=0)",
"tokenizer_config": tok_cfg, "predictor_config": mdl_cfg,
}
with open(os.path.join(args.out, "manifest.json"), "w") as f:
json.dump(manifest, f, indent=2)
print(f"\nDone. {len(manifest['arrays'])} arrays written to {args.out}")
if manifest["notes"]:
print("Notes / warnings:")
for n in manifest["notes"]:
print(" -", n)
if __name__ == "__main__":
main()