480 lines
16 KiB
MQL5
480 lines
16 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Main.mqh |
|
|
//| Copyright 2026, Niquel Mendoza. |
|
|
//| https://www.mql5.com/ |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2026, Niquel Mendoza."
|
|
#property link "https://www.mql5.com/"
|
|
#property strict
|
|
|
|
#ifndef LLMREGISTERYBYLEO_SRC_MAIN_MQH
|
|
#define LLMREGISTERYBYLEO_SRC_MAIN_MQH
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
#include "Def.mqh"
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
#resource "Init.sql" as const string g_sql_init
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
namespace TSN
|
|
{
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
class CLlmRegistery : public CLoggerBase
|
|
{
|
|
private:
|
|
//---
|
|
int m_sql_handle;
|
|
CJsonParser m_json_parser;
|
|
CJsonBuilder m_json_builder;
|
|
string m_file_name_json;
|
|
bool m_common;
|
|
bool m_check_revised; // ahora
|
|
int m_segundos_timer; // fijo siempre..
|
|
int m_tiemout_ms_req;
|
|
|
|
//
|
|
int m_intentos;
|
|
int m_max_intentos;
|
|
int m_segundos_to_retry;
|
|
|
|
//---
|
|
void CheckInitialNextTime();
|
|
bool TraerEventos();
|
|
bool WriteAllEvents();
|
|
void SaveExito();
|
|
void Try();
|
|
|
|
public:
|
|
CLlmRegistery(void);
|
|
~CLlmRegistery(void);
|
|
|
|
//---
|
|
bool Init(const string &folder_name, bool in_comon, int horas_update, int max_intenos,
|
|
int segundos_try, int timeout_ms);
|
|
|
|
//---
|
|
void OnTimerEvent();
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
CLlmRegistery::CLlmRegistery()
|
|
: m_sql_handle(INVALID_HANDLE), m_file_name_json(NULL), m_common(true), m_check_revised(false), m_segundos_timer(0),
|
|
m_intentos(0), m_max_intentos(5), m_segundos_to_retry(0)
|
|
{
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
CLlmRegistery::~CLlmRegistery(void)
|
|
{
|
|
if(m_sql_handle != INVALID_HANDLE)
|
|
{
|
|
DatabaseClose(m_sql_handle);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CLlmRegistery::Init(const string &folder_name, bool in_comon, int horas_update, int max_intenos,
|
|
int segundos_try, int timeout_ms)
|
|
{
|
|
//---
|
|
m_common = in_comon;
|
|
m_max_intentos = max_intenos;
|
|
m_segundos_to_retry = segundos_try;
|
|
m_segundos_timer = horas_update * 3600;
|
|
m_tiemout_ms_req = timeout_ms;
|
|
|
|
//---
|
|
m_file_name_json = folder_name + LLMREG_FILENAME_CONFIRM_JSON;
|
|
|
|
//---
|
|
const string sqlname = folder_name + LLMREG_FILENAME_SQL;
|
|
::ResetLastError();
|
|
m_sql_handle = DatabaseOpen(sqlname, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | (in_comon ? DATABASE_OPEN_COMMON : 0));
|
|
if(m_sql_handle == INVALID_HANDLE)
|
|
{
|
|
LogCriticalError(StringFormat("Fallo al abrir\\crear la base de dato = %s, ultimo err = %d", sqlname,
|
|
::GetLastError()), FUNCION_ACTUAL);
|
|
return false;
|
|
}
|
|
|
|
//--- Aqui creamos las tablas en caso no existan....
|
|
if(!DatabaseExecute(m_sql_handle, g_sql_init))
|
|
{
|
|
LogCriticalError(StringFormat("Fallo al ejeuctar el comando incial sql, ultimo err = %d", sqlname, ::GetLastError()),
|
|
FUNCION_ACTUAL);
|
|
return false;
|
|
}
|
|
|
|
//---
|
|
CheckInitialNextTime();
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CLlmRegistery::CheckInitialNextTime(void)
|
|
{
|
|
//--- Vericamos si no existe
|
|
// En ese caso creamos todo de 0
|
|
// En caso exista solo configuramos timer para ello..
|
|
|
|
//--- Existe el archivo
|
|
::ResetLastError();
|
|
|
|
const int ext = m_json_parser.AssingFile(m_file_name_json, m_common);
|
|
if(m_json_parser.AutoParseByFileExt(ext))
|
|
{
|
|
const datetime t = StringToTime(m_json_parser.GetRoot()["last_date_revised"].ToString("0"));
|
|
const int diff = int(TimeLocal() - t);
|
|
if(t > 0 && diff > 0) // solo si el valor es valido y hay difiencia positiva (hay tiempo aun)
|
|
{
|
|
LogInfo("Existe arhicov base.. oncigunado siguiente acutlziacoin", FUNCION_ACTUAL);
|
|
EventSetTimer(diff); // Esos segundos..
|
|
return; // Salimos..
|
|
}
|
|
// Es invalido digmoas menor a 0.... asi qeu acutlizamos ahora mismo..
|
|
}
|
|
else
|
|
{
|
|
// Fallo al abrir o fallo al parsear...
|
|
LogError(StringFormat("No se pudo leer json en el archivo = %s, json info: ", m_file_name_json), FUNCION_ACTUAL);
|
|
m_json_parser.ErrorInfo();
|
|
}
|
|
|
|
|
|
//--- Actulizacion ahora mismo...
|
|
LogWarning("Configurando timer ahora mismo, no existe el archivo base o no se pudo abrir..", FUNCION_ACTUAL);
|
|
m_check_revised = true; // Seteamos..
|
|
OnTimerEvent(); // Primer calling...
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CLlmRegistery::Try()
|
|
{
|
|
//---
|
|
if(m_intentos == 0)
|
|
{
|
|
// En caos esta en true digmoas uqe vino con desfase pero falla
|
|
// Entonces quitamos la revision daod qeu aqui ya la haremos..
|
|
m_check_revised = false;
|
|
EventKillTimer();
|
|
LogWarning(StringFormat("Fallo al traer eventos o escribir valores en db.. Reintnado entro de %d segundo",
|
|
m_segundos_to_retry), FUNCION_ACTUAL);
|
|
}
|
|
|
|
//---
|
|
m_intentos++;
|
|
if(m_intentos > m_max_intentos)
|
|
{
|
|
EventKillTimer();
|
|
m_intentos = 0; // Reinicamos...
|
|
LogError("No se pudo concretar la acutlizacion luego de varios intentos, esperando al sigueinte turno",
|
|
FUNCION_ACTUAL);
|
|
EventSetTimer(m_segundos_timer);
|
|
}
|
|
else
|
|
{
|
|
// Esperamos los segundos de try
|
|
EventSetTimer(m_segundos_to_retry);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CLlmRegistery::OnTimerEvent(void)
|
|
{
|
|
// Solo se llama cuando hayqeu volver a chekear
|
|
if(!TraerEventos())
|
|
{
|
|
Try(); // vemos..
|
|
return;
|
|
}
|
|
if(!WriteAllEvents()) // Acutlizamos la base de datos
|
|
{
|
|
Try(); // vemos..
|
|
return;
|
|
}
|
|
if(m_check_revised) // Por revisar.. ahora mismo usando timpeo custom
|
|
{
|
|
EventKillTimer();
|
|
LogInfo("Timer configurado denuevo con seg base, dadao que m_check_revised es true", FUNCION_ACTUAL);
|
|
EventSetTimer(m_segundos_timer);
|
|
|
|
//--- reinicio previo...
|
|
m_check_revised = false;
|
|
m_intentos = 0;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CLlmRegistery::TraerEventos(void)
|
|
{
|
|
//---
|
|
uchar body[];
|
|
::ResetLastError();
|
|
static string res_headers = "";
|
|
const int status = WebRequest(
|
|
"GET", // metodo
|
|
"https://models.dev/api.json", // endpoint
|
|
"", // headers custom
|
|
m_tiemout_ms_req, // timeout ms
|
|
body, // body
|
|
m_json_parser.m_raw, // respuesta
|
|
res_headers // headers de respuesta
|
|
);
|
|
|
|
//---
|
|
if(status == -1)
|
|
{
|
|
LogError("Posible WebRequest no habilitado, habilitado en config, para la url:\n'https://models.dev/api.json'", FUNCION_ACTUAL);
|
|
return false;
|
|
}
|
|
|
|
//---
|
|
if(status != 200)
|
|
{
|
|
LogError(StringFormat("Status no es igual a 200 [es = %d], primeros 20 bytes del json: ", status),
|
|
FUNCION_ACTUAL);
|
|
Print(CharArrayToString(m_json_parser.m_raw, 0, 20, CP_UTF8));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CLlmRegistery::SaveExito()
|
|
{
|
|
//---
|
|
m_json_builder.Clear();
|
|
m_json_builder.Obj();
|
|
m_json_builder.KeyWV("last_date_revised").ValSWV(TimeToString(TimeLocal(), TIME_DATE | TIME_MINUTES | TIME_SECONDS));
|
|
m_json_builder.KeyWV("chart_id").Val(ChartID());
|
|
m_json_builder.EndObj();
|
|
|
|
//---
|
|
m_json_builder.Truncate();
|
|
|
|
//---
|
|
::ResetLastError();
|
|
if(!FileSave(m_file_name_json,
|
|
m_json_builder.m_buf, (m_common ? FILE_COMMON : 0)))
|
|
{
|
|
LogError(StringFormat("No se pudo gaurdar json en el archivo = %s, ultimo err = %d",
|
|
m_file_name_json, ::GetLastError()), FUNCION_ACTUAL);
|
|
}
|
|
|
|
|
|
//--- Preparmeos req
|
|
::ResetLastError();
|
|
const int req = DatabasePrepare(m_sql_handle, "SELECT * FROM charts_id");
|
|
if(req == INVALID_HANDLE)
|
|
{
|
|
LogError(StringFormat("Fallo al prearar req: %d", ::GetLastError()), FUNCION_ACTUAL);
|
|
return;
|
|
}
|
|
|
|
//--- Iteramos por toda la base y mandamos
|
|
while(DatabaseRead(req))
|
|
{
|
|
long chart_id;
|
|
DatabaseColumnLong(req, 0, chart_id);
|
|
::ResetLastError();
|
|
if(!::EventChartCustom(chart_id, LLMREG_ON_UPDATE, 0, 0.0, ""))
|
|
{
|
|
LogError(StringFormat("Fallo al enviar evento a chart id = %I64d, last err = %d", chart_id, ::GetLastError())
|
|
, FUNCION_ACTUAL);
|
|
}
|
|
}
|
|
|
|
// Finalizamos
|
|
DatabaseFinalize(req);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CLlmRegistery::WriteAllEvents(void)
|
|
{
|
|
//---
|
|
m_json_parser.CalcLen();
|
|
m_json_parser.CorrectPadding();
|
|
const ulong a = GetMicrosecondCount();
|
|
if(!m_json_parser.Parse())
|
|
{
|
|
LogError("Fallo al parsear json: ", FUNCION_ACTUAL);
|
|
m_json_parser.ErrorInfo();
|
|
return false;
|
|
}
|
|
const ulong b = GetMicrosecondCount();
|
|
LogInfo(StringFormat("Json api.json parseado en = %I64u microsegundos", (b - a)), FUNCION_ACTUAL);
|
|
|
|
//---
|
|
CJsonNode root = m_json_parser.GetRoot();
|
|
if(root.Size() < 1)
|
|
{
|
|
LogError("No hay modelos en el json", FUNCION_ACTUAL);
|
|
return false;
|
|
}
|
|
|
|
//--- Iniciamos
|
|
::ResetLastError();
|
|
if(!DatabaseTransactionBegin(m_sql_handle))
|
|
{
|
|
LogError(StringFormat("Fallo al inicar transacion datos al m_sql_handle: ", ::GetLastError()), FUNCION_ACTUAL);
|
|
return false;
|
|
}
|
|
|
|
//---
|
|
::ResetLastError();
|
|
const int stmt_org = DatabasePrepare(m_sql_handle,
|
|
"INSERT OR REPLACE INTO llm_orgs (id, name, doc) VALUES(?,?,?)");
|
|
if(stmt_org == INVALID_HANDLE)
|
|
{
|
|
LogError(StringFormat("Fallo al ejeuctar un preprare de la base de datos, ultimo err = %d",
|
|
::GetLastError()), FUNCION_ACTUAL);
|
|
return false;
|
|
}
|
|
|
|
const int stmt_models = DatabasePrepare(m_sql_handle,
|
|
"INSERT OR REPLACE INTO llm_models "
|
|
"(id, org_id, name, description, cost_in, cost_out, release_date, last_update_date, "
|
|
"flags, in_modals, out_modals, context_window, context_output, context_input) "
|
|
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
|
if(stmt_models == INVALID_HANDLE)
|
|
{
|
|
LogError(StringFormat("Fallo al ejeuctar un preprare de la base de datos, ultimo err = %d",
|
|
::GetLastError()), FUNCION_ACTUAL);
|
|
return false;
|
|
}
|
|
|
|
//---
|
|
CJsonIteratorObj it = root.BeginObj();
|
|
|
|
//--- Es valido
|
|
while(it.IsValid())
|
|
{
|
|
//---
|
|
CJsonNode val = it.Val();
|
|
const string org_id = val["id"].ToString();
|
|
DatabaseReset(stmt_org);
|
|
DatabaseBind(stmt_org, 0, org_id);
|
|
DatabaseBind(stmt_org, 1, val["name"].ToString());
|
|
DatabaseBind(stmt_org, 2, val["doc"].ToString());
|
|
DatabaseRead(stmt_org);
|
|
|
|
//---
|
|
CJsonNode models = val["models"];
|
|
const int size = models.Size();
|
|
if(size > 0)
|
|
{
|
|
CJsonIteratorObj it_models = models.BeginObj();
|
|
while(it_models.IsValid())
|
|
{
|
|
CJsonNode model = it_models.Val();
|
|
|
|
//---
|
|
DatabaseReset(stmt_models);
|
|
|
|
//--- base
|
|
DatabaseBind(stmt_models, 0, model["id"].ToString());
|
|
DatabaseBind(stmt_models, 1, org_id);
|
|
DatabaseBind(stmt_models, 2, model["name"].ToString());
|
|
DatabaseBind(stmt_models, 3, model["description"].ToString());
|
|
DatabaseBind(stmt_models, 4, model["cost"]["input"].ToDouble(0.00));
|
|
DatabaseBind(stmt_models, 5, model["cost"]["output"].ToDouble(0.00));
|
|
DatabaseBind(stmt_models, 6, StringToTime(model["release_date"].ToString()));
|
|
DatabaseBind(stmt_models, 7, StringToTime(model["last_update_date"].ToString()));
|
|
|
|
//-- flags
|
|
uint f = 0;
|
|
f |= model["tool_call"].ToBool(false) ? LLMDEF_FLAG_TOOLS : 0;
|
|
f |= model["reasoning"].ToBool(false) ? LLMDEF_FLAG_REASONING : 0;
|
|
f |= model["temperature"].ToBool(false) ? LLMDEF_FLAG_TEMPERATURE : 0;
|
|
f |= model["open_weights"].ToBool(false) ? LLMDEF_FLAG_OPEN_WEIGHTS : 0;
|
|
f |= model["structured_output"].ToBool(false) ? LLMDEF_FLAG_STRUCTURED_OUTPUT : 0;
|
|
DatabaseBind(stmt_models, 8, f);
|
|
|
|
//--- complementiosn
|
|
CJsonNode modalities = model["modalities"];
|
|
f = 0;
|
|
if(modalities["input"].IsArray())
|
|
{
|
|
CJsonIteratorArray it_in = modalities["input"].BeginArr();
|
|
while(it_in.IsValid())
|
|
{
|
|
f |= CEnumRegLLmReg::GetValNoRef<uint>(it_in.Val().ToString(""), 0);
|
|
it_in.Next();
|
|
}
|
|
}
|
|
DatabaseBind(stmt_models, 9, f); // in modlas
|
|
|
|
//---
|
|
f = 0;
|
|
if(modalities["output"].IsArray())
|
|
{
|
|
CJsonIteratorArray it_out = modalities["output"].BeginArr();
|
|
while(it_out.IsValid())
|
|
{
|
|
f |= CEnumRegLLmReg::GetValNoRef<uint>(it_out.Val().ToString(""), 0);
|
|
it_out.Next();
|
|
}
|
|
}
|
|
DatabaseBind(stmt_models, 10, f); // out modlas
|
|
|
|
//--- contexts
|
|
CJsonNode limit = model["limit"];
|
|
DatabaseBind(stmt_models, 11, (int)limit["context"].ToInt(0));
|
|
DatabaseBind(stmt_models, 12, (int)limit["output"].ToInt(0));
|
|
DatabaseBind(stmt_models, 13, (int)limit["input"].ToInt(0));
|
|
DatabaseRead(stmt_models);
|
|
|
|
//---
|
|
it_models.Next();
|
|
}
|
|
}
|
|
it.Next();
|
|
}
|
|
|
|
//---
|
|
::ResetLastError();
|
|
if(!DatabaseTransactionCommit(m_sql_handle))
|
|
{
|
|
LogError(StringFormat("Fallo al enviar datos al m_sql_handle: ", ::GetLastError()), FUNCION_ACTUAL);
|
|
return false;
|
|
}
|
|
DatabaseFinalize(stmt_models);
|
|
DatabaseFinalize(stmt_org);
|
|
|
|
|
|
//---
|
|
SaveExito();
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
#endif // LLMREGISTERYBYLEO_SRC_MAIN_MQH
|