Created
April 17, 2024 12:05
-
-
Save matthewgream/1a3bf613424f3c7c499727afb3988a3f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function __mq_datfile_decode_data (buffer) { | |
const __bar_full = (view, addr, open = view.getUint32 (addr + 4, true)) => ({ | |
time: (new Date (view.getUint32 (addr, true) * 1000)).getTime (), | |
open, | |
high: open + view.getUint16 (addr + 8, true), | |
low: open - view.getUint16 (addr + 10, true), | |
close: open + view.getInt16 (addr + 12, true), | |
volume: view.getUint16 (addr + 14, true) | |
}); | |
const __bar_incr = (view, addr, last, time = 1, open = last.close + view.getInt8 (addr)) => ({ | |
time: (new Date (last.time + (time * 60000))).getTime (), | |
open, | |
high: open + view.getUint8 (addr + 1), | |
low: open - view.getUint8 (addr + 2), | |
close: open + view.getInt8 (addr + 3), | |
volume: view.getUint8 (addr + 4) | |
}); | |
const view = new DataView (buffer); | |
if (!(view.getUint8 (0) > 0x3F && view.getUint8 (0) <= 0x7F)) | |
__THROW (`bad data: compressed series does not start with type-1`); | |
let addr = 0, last = undefined; | |
const bars = [], stop = view.byteLength; | |
while (addr < stop) { | |
const byte = view.getUint8 (addr ++); | |
if (byte > 0xBF) // Type-3 Block | |
for (let offs = 0; offs < (byte - 0xBF); offs ++, addr += 5) | |
bars.push (last = { ...__bar_incr (view, addr, last), type: 3, addr }); | |
else if (byte > 0x7F) // Type-2 Block | |
for (let offs = 0; offs < (byte - 0x7F); offs ++, addr += 6) | |
bars.push (last = { ...__bar_incr (view, addr + 1, last, view.getInt8 (addr)), type: 2, addr }); | |
else if (byte > 0x3F) // Type-1 Block | |
for (let offs = 0; offs < (byte - 0x3F); offs ++, addr += 16) | |
bars.push (last = { ...__bar_full (view, addr), type: 1, addr }); | |
} | |
return bars; | |
} | |
function __mq_datfile_checks (bars) { | |
return bars.map (bar => { | |
const anomalies = [], prices = [ bar.open, bar.high, bar.low, bar.close ]; | |
if (bar.open > bar.high || bar.open < bar.low) | |
anomalies.push ("open"); | |
if (prices.max () !== bar.high) | |
anomalies.push ("high"); | |
if (prices.min () !== bar.low) | |
anomalies.push ("low"); | |
if (bar.close > bar.high || bar.close < bar.low) | |
anomalies.push ("close"); | |
return (anomalies.length > 0) && | |
`[ANOMALY] ${util_date_epochToStr_ISO (bar.time)} type-${bar.type} ${bar.addr.toString (16)} ${anomalies.join (' ')} [${util_obj_strNice (bar)}]`; | |
}).filter (Boolean); | |
} | |
function __mq_datfile_decode_key (bytes) { | |
const k = '0x' + util_uint8array_to_base16 (bytes), e = 17, m = '0x' + [ // LSB | |
'2300905D', '1B6C06DF', 'E4D0D140', 'ED8B47C4', '93970C42', '920C45E6', '22C90AFB', '37B67A10', | |
'0F67F0F6', '4237AB4F', '9FA30B14', '916B3CA6', 'D48FA715', '689FCCA6', 'D3DBE628', '5200D9B3', | |
'732F7BBC', 'DC592279', '39861B5F', '0A007CBA', 'BF311219', 'D3461CB2', '519A4042', 'DE59FBB0', | |
'DD6662ED', 'E9D7BAFC', '878F5459', '63294CBF', '103206C9', 'D2FA9C90', '49832FEF', 'ADEAAD39', | |
'00000000', '00000000', | |
].reverse ().join (''); | |
const r = (BigInt (k) ** BigInt (e)) % BigInt (m); | |
return util_base16_to_uint8array (r.toString (16).slice (-128*2).padStart (128*2, '0')); | |
} | |
function __mq_datfile_decode_body (buffer) { | |
const head_length = 0x88, key_length = 0x80, last_bytes = [ 0x11, 0x00, 0x00 ]; | |
const content = new Uint8Array (buffer); | |
const head = content.slice (0, head_length); | |
const key = __mq_datfile_decode_key (content.slice (head_length, head_length + key_length).reverse ()).reverse (); // LE | |
if (key.length != key_length) | |
__THROW (`bad data: key length, got ${key.length}, expected ${key_length}`); | |
const body = util_uint8array_xor (key, content.slice (head_length + key_length)); | |
const view = new DataView (body.buffer); | |
const packed_size = view.getUint32 (0, true), unpacked_size = view.getUint32 (4, true); | |
if (packed_size != (body.length - 8)) | |
__THROW (`bad data: packed_size, got ${body.length - 8}, expected ${packed_size}`); | |
if (!body.slice (-1 * last_bytes.length).every ((byte, offset) => (byte == last_bytes [offset]))) | |
__THROW (`bad data: end bytes, got [${body.slice (-1 * last_bytes.length).join (',')}], expected ${last_bytes.join (',')}`); | |
const stat = { packed_size, unpacked_size, body_length: body.length - 8, key_length: key.length }; | |
const data = LZ01X_Implementation.decompress (body.slice (8), { blockSize: 16384 }); | |
if (unpacked_size != data.length) | |
__THROW (`bad data: unpacked_size, got ${data.length}, expected ${unpacked_size}`); | |
return { head, data, stat }; | |
} | |
function util_uint8array_to_base16 (arr) { | |
return Array.from (arr, byte => byte.toString (16).padStart (2, '0')).join (''); | |
} | |
function util_base16_to_uint8array (str) { | |
const bytes = []; | |
for (var i = 0, l = str.length; i < l; i += 2) | |
bytes.push (Number.parseInt (str.slice (i, i + 2), 16)); | |
return new Uint8Array (bytes); | |
} | |
function util_uint8array_xor (key, arr) { | |
for (var i = 0, j = 0, l = arr.length, k = key.length; i < l; i ++, j = (j + 1) % k) | |
arr [i] ^= key [j]; | |
return arr; | |
} | |
const __mq_history_link = (symbol, filename) => `http://history.metaquotes.net/symbols/${symbol}/${filename}`; | |
const __mq_history_link_list = (symbol) => __mq_history_link (symbol, 'list.txt'); | |
const __mq_history_link_data = ({ symbol, year, month, checksum }) => __mq_history_link (symbol,`${symbol}_${year}_${month.padStart (2, '0')}_${checksum}.dat`); | |
const __mq_history_csum = (data, csum) => util_buffer_tohex (util_hash_md5 (data)) === csum; | |
const __mq_history_spec = (name) => name.match (/^(?<symbol>\w+)_(?<year>\d{4})_(?<month>\d{2})_(?<checksum>[\da-f]{32})\.dat$/)?.groups || {}; | |
function mq_history_list (symbol) { | |
return util_exception_catcher (() => system_connect.request (util_function_name (icmarkets_history_list), __mq_history_link_list (symbol))) | |
?.split ('\n').filter (Boolean).map (name => __mq_history_spec (name)); | |
} | |
function mq_history_data (spec) { | |
const buffer = system_connect.request (util_function_name (icmarkets_history_data), typeof spec === 'string' ? spec : __mq_history_link_data (spec), {}, 'arraybuffer'); | |
if (buffer && __mq_history_csum (buffer, typeof spec === 'string' ? spec.match (/_(?<checksum>[\da-f]{32})\.dat$/)?.groups?.checksum : spec.checksum)) return buffer; | |
return undefined; | |
} | |
... | |
const buffer = mq_history_data ('http://history.metaquotes.net/symbols/EURJPY/EURJPY_2023_09_3d2055bf5d8d3e8700c35587d612f64b.dat'); | |
const { head, data, stat } = __mq_datfile_decode_body (buffer); | |
console.log (stat); | |
console.log (util_buffer_toString (head)); | |
const bars = __mq_datfile_decode_data (data.buffer); | |
console.log ({ bars: bars.length, first: util_date_epochToStr_ISO (bars.first ().time), final: util_date_epochToStr_ISO (bars.final ().time) }); | |
__mq_datfile_checks (bars).forEach (result => console.log (result)); | |
console.log ('***'); | |
bars.forEach (bar => console.log (util_obj_strNiceOrdered ({ ...bar, timestamp: util_date_epochToStr_ISO (bar.time) }, ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'type', 'addr']))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment