//+------------------------------------------------------------------+ //| 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 }; });