//+------------------------------------------------------------------+ //| KronosInference.mqh | //| MMQ — Muhammad Minhas Qamar | //| www.mql5.com | //+------------------------------------------------------------------+ #property copyright "MMQ — Muhammad Minhas Qamar" #property link "https://www.mql5.com" #property version "1.00" #ifndef KRONOS_INFERENCE_MQH #define KRONOS_INFERENCE_MQH #include #include #include #include #include #include #include //+------------------------------------------------------------------+ //| Full Kronos model: tokenizer (encode + decode) and predictor | //| (decode_s1 + decode_s2). | //+------------------------------------------------------------------+ class CKronosModel { private: CKronosEncoder m_enc; CKronosDecoder m_dec; CKronosPredictorS1 m_p1; CKronosPredictorS2 m_p2; int m_max_context; //+---------------------------------------------------------------+ //| Slice rows [r0, r0+L) of an (N,5) stamp matrix into (L,5). | //+---------------------------------------------------------------+ void StampWindow(const matrix &full_stamp, int r0, int L, matrix &out) { out = matrix::Zeros((ulong)L, 5); for(int i = 0; i < L; i++) for(int j = 0; j < 5; j++) out[i][j] = full_stamp[r0 + i][j]; } public: //+---------------------------------------------------------------+ //| Load every component. tok_dir / pred_dir are weight folders | //| under MQL5/Files/ (with a trailing backslash). | //| Tokenizer: d_model=256, n_heads=4, enc/dec_layers=4, ff=512 | //| Predictor: d_model=512, n_heads=8, n_layers=8, ff=1024 | //+---------------------------------------------------------------+ bool Init(const string tok_dir, int tok_enc_layers, int tok_dec_layers, ulong tok_dm, int tok_heads, ulong tok_ff, const string pred_dir, int pred_layers, ulong pred_dm, int pred_heads, ulong pred_ff, int max_context = 512) { m_max_context = max_context; bool ok = true; ok &= m_enc.Init(tok_dir, tok_enc_layers, tok_dm, tok_heads, tok_ff); ok &= m_dec.Init(tok_dir, tok_dec_layers, tok_dm, tok_heads, tok_ff); ok &= m_p1.Init(pred_dir, pred_layers, pred_dm, pred_heads, pred_ff); ok &= m_p2.Init(pred_dir, pred_dm); if(!ok) Print("CKronosModel::Init: a component failed to load"); return ok; } //+---------------------------------------------------------------+ //| One autoregressive path (single sample). Returns pred_len rows| //| of normalized OHLCVA (the caller denormalizes) and fills the | //| generated s1/s2 token sequences. | //+---------------------------------------------------------------+ bool GeneratePathNorm(const matrix &x_norm, const matrix &full_stamp, int pred_len, double T, int top_k, double top_p, bool greedy, matrix &pred_norm, int &out_gen_s1[], int &out_gen_s2[]) { int L0 = (int)x_norm.Rows(); // initial context length //--- encode the context window -> base tokens int base_s1[], base_s2[]; if(!m_enc.Encode(x_norm, base_s1, base_s2)) return false; //--- token ring buffers grow up to max_context int pre[], post[]; ArrayCopy(pre, base_s1); ArrayCopy(post, base_s2); int gen_s1[], gen_s2[]; ArrayResize(gen_s1, pred_len); ArrayResize(gen_s2, pred_len); //--- Prime the decode_s1 KV-cache over the initial context window. While the //--- sequence stays in the grow phase (current_seq_len < max_context) each //--- step extends the cache by one row (O(T) per step) instead of re-running //--- the full 8-block transformer over the whole window (O(T^2)). The cache //--- is exact only while positions never shift, so once the window starts to //--- slide we fall back to the full DecodeS1 and stop using the cache. matrix prime_logits, prime_ctx; int win0_s1[], win0_s2[]; ArrayResize(win0_s1, L0); ArrayResize(win0_s2, L0); for(int t = 0; t < L0; t++) { win0_s1[t] = base_s1[t]; win0_s2[t] = base_s2[t]; } matrix stamp0; StampWindow(full_stamp, 0, L0, stamp0); bool cache_ok = m_p1.PrimeCache(win0_s1, win0_s2, stamp0, prime_logits, prime_ctx); for(int i = 0; i < pred_len; i++) { int current_seq_len = L0 + i; int window_len = (int)MathMin(current_seq_len, m_max_context); int ctx_end = current_seq_len; int ctx_start = (int)MathMax(0, ctx_end - m_max_context); //--- decode_s1: cached grow-phase step, or full-window fallback matrix s1_logits, context; int last; bool use_cache = (cache_ok && current_seq_len < m_max_context); if(use_cache) { if(i == 0) { //--- step 0 reuses the primed last row (== full DecodeS1 of base) s1_logits = prime_logits; m_p1.GetContext(context); last = (int)s1_logits.Rows() - 1; } else { //--- feed the previously generated token (position L0+i-1) through //--- the cache; its stamp is full_stamp[L0+i-1]. vector srow = vector::Zeros(5); for(int j = 0; j < 5; j++) srow[j] = full_stamp[L0 + i - 1][j]; matrix step_logits, step_ctx; if(!m_p1.DecodeS1Step(gen_s1[i - 1], gen_s2[i - 1], srow, step_logits, step_ctx)) return false; s1_logits = step_logits; // (1, vocab) -> last row is row 0 m_p1.GetContext(context); // full running context for decode_s2 last = 0; // s1 logits have a single row } } else { //--- slide phase (or priming failed): full window recompute int win_s1[], win_s2[]; ArrayResize(win_s1, window_len); ArrayResize(win_s2, window_len); int off = ArraySize(pre) - window_len; for(int t = 0; t < window_len; t++) { win_s1[t] = pre[off + t]; win_s2[t] = post[off + t]; } matrix stamp_win; StampWindow(full_stamp, ctx_start, window_len, stamp_win); if(!m_p1.DecodeS1(win_s1, win_s2, stamp_win, s1_logits, context)) return false; last = window_len - 1; } double l1[]; ArrayResize(l1, (int)s1_logits.Cols()); for(int j = 0; j < (int)s1_logits.Cols(); j++) l1[j] = s1_logits[last][j]; int s1_pick = SampleFromLogits(l1, T, top_k, top_p, greedy); //--- decode_s2(context, [s1_pick]) -> sample last-step s2. The cross-attn //--- query (the single s1 pick) attends over the FULL context, so we pass //--- the whole context and read its last row. int pick_arr[]; ArrayResize(pick_arr, 1); pick_arr[0] = s1_pick; matrix s2_logits; if(!m_p2.DecodeS2(context, pick_arr, s2_logits)) return false; int s2_last = (int)s2_logits.Rows() - 1; double l2[]; ArrayResize(l2, (int)s2_logits.Cols()); for(int j = 0; j < (int)s2_logits.Cols(); j++) l2[j] = s2_logits[s2_last][j]; int s2_pick = SampleFromLogits(l2, T, top_k, top_p, greedy); gen_s1[i] = s1_pick; gen_s2[i] = s2_pick; //--- append, sliding the buffer to max_context int n = ArraySize(pre); if(n < m_max_context) { ArrayResize(pre, n + 1); pre[n] = s1_pick; ArrayResize(post, n + 1); post[n] = s2_pick; } else { for(int t = 0; t < n - 1; t++) { pre[t] = pre[t + 1]; post[t] = post[t + 1]; } pre[n - 1] = s1_pick; post[n - 1] = s2_pick; } } //--- final decode: full window = context tokens + generated tokens, last <=512 int total = L0 + pred_len; int dec_start = (int)MathMax(0, total - m_max_context); int dec_len = total - dec_start; int full_s1[], full_s2[]; ArrayResize(full_s1, dec_len); ArrayResize(full_s2, dec_len); for(int t = 0; t < dec_len; t++) { int gi = dec_start + t; // global index 0..total-1 if(gi < L0) { full_s1[t] = base_s1[gi]; full_s2[t] = base_s2[gi]; } else { full_s1[t] = gen_s1[gi - L0]; full_s2[t] = gen_s2[gi - L0]; } } matrix recon; if(!m_dec.Decode(full_s1, full_s2, recon)) return false; // (dec_len, 6) normalized //--- keep the last pred_len rows pred_norm = matrix::Zeros((ulong)pred_len, KR_NFEAT); int base = dec_len - pred_len; for(int t = 0; t < pred_len; t++) for(int j = 0; j < KR_NFEAT; j++) pred_norm[t][j] = recon[base + t][j]; ArrayCopy(out_gen_s1, gen_s1); ArrayCopy(out_gen_s2, gen_s2); return true; } //+---------------------------------------------------------------+ //| Full predict: a raw OHLCVA window (L,6) plus stamps becomes a | //| forecast (pred_len,6) in raw units. full_stamp is | //| (L+pred_len, 5) covering context and horizon, weekday in the | //| pandas convention. Averages sample_count paths in normalized | //| space (as the reference does), then denormalizes once. | //+---------------------------------------------------------------+ bool Predict(const matrix &raw, const matrix &full_stamp, int pred_len, double T, int top_k, double top_p, int sample_count, bool greedy, matrix &forecast) { matrix x_norm; vector mean, stdv; KronosNormalize(raw, x_norm, mean, stdv); matrix avg = matrix::Zeros((ulong)pred_len, KR_NFEAT); int got = 0; for(int s = 0; s < sample_count; s++) { matrix p; int g1[], g2[]; if(!GeneratePathNorm(x_norm, full_stamp, pred_len, T, top_k, top_p, greedy, p, g1, g2)) return false; avg += p; got++; } if(got == 0) return false; avg *= (1.0 / (double)got); //--- denormalize with the context window's per-feature stats KronosDenormalize(avg, mean, stdv, forecast); return true; } }; #endif // KRONOS_INFERENCE_MQH //+------------------------------------------------------------------+