kronos-mql5/Include/Kronos/KronosInference.mqh
2026-07-05 05:03:49 +00:00

278 lines
11 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <Kronos\KronosTokenizerMath.mqh>
#include <Kronos\KronosTransformerCore.mqh>
#include <Kronos\KronosSampling.mqh>
#include <Kronos\KronosEncoder.mqh>
#include <Kronos\KronosDecoder.mqh>
#include <Kronos\KronosPredictorS1.mqh>
#include <Kronos\KronosPredictorS2.mqh>
//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+