bifurcado de nique_372/FastNLExamples
133 líneas
6,2 KiB
JavaScript
//+------------------------------------------------------------------+
| |||
//| 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
| |||
};
| |||
});
|