278 lines
11 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+
|