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