FastNLExamples/Src/CalendarFormat/parser.js
133 líneas
6,2 KiB
JavaScript
Nique_372
2026-11-04 12:54:16 -05:00
//+------------------------------------------------------------------+
//| parser.js |
//| Parser del .bin de un evento del calendario (CEcoEvents::Save). |
//| Formato (little-endian), ver CalendarByLeo\Events\EcoEvent.mqh: |
//| 3 strings : int32 largo + bytes ANSI (event_code, name, url) |
//| int64 id, int64 country_id |
//| 8 x int32 : importance, type, sector, frequency, time_mode, |
//| multiplier, unit, digits |
//| int32 n : cantidad de registros |
//| n x 56 bytes (struct pack(8) EconomicEventData): |
//| f64 actual, f64 previous, f64 revised_previous, f64 forecast |
//| u64 id, i64 time, u32 extra_meta, i32 impact |
//| Se lee solo la cabecera; cada registro se lee on-demand por |
//| indice (tamano fijo => acceso directo sin recorrer el archivo). |
//+------------------------------------------------------------------+
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory();
else root.CalFmt = factory();
})(this, function () {
'use strict';
const RECORD_SIZE = 56;
const ERROR_VARIABLE = -777999999912891; // "sin valor" (ver FastNL\Src\Prev\Def.mqh)
const NUM_INT_FIELDS = 8;
// Enums de MT5 (ENUM_CALENDAR_EVENT_*)
const IMPORTANCE = ['Ninguna', 'Baja', 'Media', 'Alta'];
const TYPE = ['Evento', 'Indicador', 'Feriado'];
const SECTOR = ['Ninguno', 'Mercado', 'PIB', 'Empleo', 'Precios', 'Dinero', 'Comercio', 'Gobierno',
'Negocios', 'Consumo', 'Vivienda', 'Impuestos', 'Feriados'];
const FREQUENCY = ['Ninguna', 'Semanal', 'Mensual', 'Trimestral', 'Anual', 'Diaria'];
const TIMEMODE = ['Fecha y hora', 'Solo fecha', 'Sin hora', 'Tentativo'];
const MULTIPLIER = ['', 'K', 'M', 'B', 'T'];
const MULT_NAME = ['Ninguno', 'Miles', 'Millones', 'Billones', 'Trillones'];
const UNIT = ['Ninguna', 'Porcentaje', 'Moneda', 'Horas', 'Empleos', 'Plataformas', 'USD', 'Personas',
'Hipotecas', 'Votos', 'Barriles', 'Pies cubicos', 'Posiciones', 'Tiendas'];
const IMPACT = ['N/A', 'Positivo', 'Negativo'];
// country_id de MQL5 = codigo numerico ISO 3166 (999 = Union Europea)
const COUNTRY = {
36: 'Australia', 76: 'Brasil', 124: 'Canada', 156: 'China', 250: 'Francia', 276: 'Alemania', 344: 'Hong Kong',
356: 'India', 380: 'Italia', 392: 'Japon', 410: 'Corea del Sur', 484: 'Mexico', 554: 'Nueva Zelanda',
578: 'Noruega', 710: 'Sudafrica', 724: 'Espana', 752: 'Suecia', 756: 'Suiza', 792: 'Turquia',
826: 'Reino Unido', 840: 'Estados Unidos', 999: 'Union Europea'
};
function label(arr, i) { return i >= 0 && i < arr.length ? arr[i] : 'Desconocido (' + i + ')'; }
// Cabecera. Lanza Error con mensaje legible si el archivo no cuadra con el formato.
function parseHeader(buf) {
const view = new DataView(buf);
const dec = new TextDecoder('windows-1252'); // FILE_ANSI
let o = 0;
function need(n, what) {
if (o + n > buf.byteLength) throw new Error('Archivo truncado leyendo ' + what + ' (offset ' + o + ')');
}
function readString(what) {
need(4, 'largo de ' + what);
const len = view.getInt32(o, true); o += 4;
if (len < 0 || len > 65536) throw new Error('Largo invalido en ' + what + ' (' + len + '): no parece un archivo de evento');
need(len, what);
const s = dec.decode(new Uint8Array(buf, o, len)); o += len;
return s;
}
const h = {};
h.code = readString('event_code');
h.name = readString('name');
h.url = readString('source_url');
need(16 + NUM_INT_FIELDS * 4 + 4, 'campos del evento');
h.id = view.getBigInt64(o, true).toString(); o += 8;
h.countryId = Number(view.getBigInt64(o, true)); o += 8;
const f = [];
for (let i = 0; i < NUM_INT_FIELDS; i++) { f.push(view.getInt32(o, true)); o += 4; }
[h.importance, h.type, h.sector, h.frequency, h.timeMode, h.multiplier, h.unit, h.digits] = f;
const n = view.getInt32(o, true); o += 4;
if (n < 0) throw new Error('Cantidad de registros invalida (' + n + ')');
h.declaredCount = n;
h.dataOffset = o;
const avail = Math.floor((buf.byteLength - o) / RECORD_SIZE);
h.count = Math.min(n, avail);
h.warning = null;
if (avail < n) h.warning = 'El archivo declara ' + n + ' registros pero solo caben ' + avail + ' (truncado).';
else if (buf.byteLength - o !== n * RECORD_SIZE) h.warning = 'Sobran ' + (buf.byteLength - o - n * RECORD_SIZE) + ' bytes al final del archivo.';
h.view = view;
return h;
}
// Registro i (0-based, orden del archivo)
function readRow(h, i) {
const v = h.view, o = h.dataOffset + i * RECORD_SIZE;
const meta = v.getUint32(o + 48, true);
return {
actual: v.getFloat64(o, true),
previous: v.getFloat64(o + 8, true),
revised: v.getFloat64(o + 16, true),
forecast: v.getFloat64(o + 24, true),
id: v.getBigUint64(o + 32, true).toString(),
time: Number(v.getBigInt64(o + 40, true)), // segundos (hora del servidor)
meta: meta,
countryIdx: meta & 0xff,
eventIdx: (meta >>> 8) & 0xfffff,
processed: ((meta >>> 28) & 1) === 1,
impact: v.getInt32(o + 52, true)
};
}
function isEmpty(x) { return x === ERROR_VARIABLE || !isFinite(x); }
function fmtValue(x, h) {
if (isEmpty(x)) return '—';
const s = x.toFixed(Math.max(0, Math.min(h.digits, 12)));
const suffix = (MULTIPLIER[h.multiplier] || '') + (h.unit === 1 ? '%' : '');
return s + suffix;
}
// El tiempo se guarda como segundos de la hora del servidor; se muestran los campos UTC tal cual (sin convertir zona).
function fmtTime(sec) {
if (!sec) return '—';
const d = new Date(sec * 1000);
const p = (n) => String(n).padStart(2, '0');
return d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate()) + ' ' + p(d.getUTCHours()) + ':' + p(d.getUTCMinutes());
}
return {
RECORD_SIZE, ERROR_VARIABLE, parseHeader, readRow, fmtValue, fmtTime, isEmpty, label,
IMPORTANCE, TYPE, SECTOR, FREQUENCY, TIMEMODE, MULT_NAME, UNIT, IMPACT, COUNTRY
};
});