Suivre
1
0
Forker
Vous avez déjà forké okWarrior_EA
0
okWarrior_EA/research/sqx_decode/sqxbars.py

214 lignes
8,7 Kio
Python
BrutLien permanentVue normaleHistorique

"""Decode SQX .dat BAR files - the free breadth sitting in the SQX install.
`sqx.py` decodes the TICK files, where every record has FOUR fields (time, ask, bid, volume).
The bar files use the identical container with SIX (time, open, high, low, close, volume),
and that is the only reason the tick decoder cannot read them. Everything else - the block
magic, the delta encoding, the per-record field map - is the same, so this module is that
decoder generalised over the field count rather than a second implementation.
THE FORMAT, STATED PLAINLY
--------------------------
header as sqx.read_header
block anchor MAGIC (15 bytes) + int32 block index + cfg + N ABSOLUTE int64s
ordinary record cfg + N values, each a DELTA on the running value
cfg ONE NIBBLE PER FIELD, so N fields occupy ceil(N/2) bytes:
tick 4 fields -> 2 bytes, bar 6 fields -> 3 bytes.
nibble = (logic << 2) | dtype, dtype indexes (1,2,4,8) bytes.
The records are delta-encoded, which is why reading at a fixed stride recovers the first
record perfectly and then produces noise - a failure mode worth naming, because the first
record looking right is exactly what makes it tempting to trust the rest.
THE SCALE IS THE DANGEROUS PART
-------------------------------
Prices are integers and the decimal count is NOT in the header. A wrong scale does not crash
and does not even hurt a correlation - it silently rescales every price, which is recorded
twice already in this project's notes as the trap that costs the most time. So it is never
inferred here: `catalog` pins it, states the three independent checks that agree on it, and
this module ASSERTS it per file against the instrument's known price range rather than
trusting the constant.
These are M1 bars with no ask, so any trade simulation on them must substitute the5ers' cost
from an instrument where the real spread is known. Never Dukascopy's own.
"""
import numpy as np, os, sys, struct
import sqx, catalog
from sqx import MAGIC, _SIZE, MINUS, ASIS
CACHE = catalog.CACHE
COLS = ['time', 'open', 'high', 'low', 'close', 'volume']
def _fields_n(cfg, nf):
"""Unpack `nf` (logic, dtype) pairs from ceil(nf/2) config bytes, one nibble each."""
out = []
for i in range(nf):
nib = (cfg[i >> 1] >> (4 if i % 2 == 0 else 0)) & 0xF
out.append((nib >> 2, nib & 3))
return out
def field_count(path):
"""Read the first block's cfg width to learn how many fields the file carries."""
with open(path, 'rb') as fh:
head = fh.read(1 << 16)
p, _ = sqx.read_header(head)
p += 15 + 4
n = 0
while head[p + n] == 0xBB:
n += 1
return n * 2
def decode(path, nf=None, max_records=None):
"""-> (n, nf) int64 array of absolute values: time_ms, open, high, low, close, volume."""
nf = nf or field_count(path)
ncfg = (nf + 1) // 2
fh = open(path, 'rb')
head = fh.read(1 << 16)
start, _ = sqx.read_header(head)
fh.seek(start)
cur = [0] * nf
rows = []
buf = b''
pos = 0
CH = 1 << 22
eof = False
n = 0
need_max = ncfg + 8 * nf + 32
while True:
if len(buf) - pos < need_max and not eof:
buf = buf[pos:]; pos = 0
more = fh.read(CH)
if not more:
eof = True
buf += more
if len(buf) - pos < ncfg + 1:
break
if buf[pos:pos + 15] == MAGIC:
pos += 15 + 4 # magic + int32 block index
if len(buf) - pos < ncfg + 8 * nf:
break
pos += ncfg # anchor cfg, values are absolute
cur = list(struct.unpack_from(f'>{nf}q', buf, pos))
pos += 8 * nf
else:
cfg = buf[pos:pos + ncfg]
fs = _fields_n(cfg, nf)
size = ncfg + sum(_SIZE[d] for _, d in fs)
if len(buf) - pos < size:
if eof:
break
buf = buf[pos:]; pos = 0
more = fh.read(CH)
if not more:
eof = True
buf += more
if len(buf) < size:
break
continue
q = pos + ncfg
for k, (logic, dtp) in enumerate(fs):
sz = _SIZE[dtp]
raw = int.from_bytes(buf[q:q + sz], 'big', signed=(logic == ASIS))
q += sz
cur[k] += (-raw if logic == MINUS else raw)
pos = q
rows.append(tuple(cur))
n += 1
if max_records and n >= max_records:
break
fh.close()
return np.array(rows, dtype=np.int64), nf
def check_scale(close, key):
"""Refuse a decoded file whose prices do not land in the instrument's known range.
The scale is pinned in `catalog`, not fitted, because the reference series it used to be
fitted against no longer exists. That makes this check the only thing standing between a
silently rescaled price and every ATR-normalised feature downstream, so it is an
assertion rather than a warning. The neighbouring exponents are 10x away; nothing about
this call is ever marginal.
"""
lo, hi = catalog.px_range(key)
med = float(np.median(close))
if not (lo <= med <= hi):
raise ValueError(f"{catalog.nice(key)}: median close {med:,.4f} outside the known "
f"range [{lo:,}, {hi:,}] - scale 1e{catalog.DECIMALS} is wrong "
f"for this file")
return med
def load(key, tf='M1', verbose=True):
"""Decoded, scaled, sorted bars for a catalog key. Cached as .npz after the first call."""
os.makedirs(CACHE, exist_ok=True)
out = f"{CACHE}{catalog.nice(key)}_{tf}.npz"
if os.path.exists(out):
z = np.load(out)
return z['bars'], list(z['columns'])
path = catalog.path(key, tf)
if not os.path.exists(path):
raise FileNotFoundError(path)
a, nf = decode(path)
if nf < 6:
raise ValueError(f"{key}: {nf} fields, not a bar file")
s = 10.0 ** catalog.DECIMALS
bars = np.column_stack([a[:, 0].astype(np.float64), a[:, 1] / s, a[:, 2] / s,
a[:, 3] / s, a[:, 4] / s, a[:, 5].astype(np.float64)])
bars = bars[np.argsort(bars[:, 0], kind='stable')]
med = check_scale(bars[:, 4], key)
#--- structural sanity a scale check cannot do: a high below its low means the FIELD
#--- ORDER is wrong, which rescaling would never reveal
bad = int((bars[:, 2] < bars[:, 3]).sum())
if bad > len(bars) // 1000:
raise ValueError(f"{key}: {bad:,}/{len(bars):,} bars have high < low - field order")
if verbose:
import datetime as dt
print(f" {catalog.nice(key):>9} {tf}: {len(bars):>10,} bars "
f"{dt.datetime.fromtimestamp(bars[0,0]/1000, dt.UTC):%Y-%m-%d}.."
f"{dt.datetime.fromtimestamp(bars[-1,0]/1000, dt.UTC):%Y-%m-%d}"
f" median {med:>11,.4f} high<low {bad}")
np.savez_compressed(out, bars=bars, columns=np.array(COLS))
return bars, COLS
def _cross_feed_report():
"""The replication check that replaced the lost reference series.
Six of these markets are carried TWICE, by vendors that share no data path. Agreement on
the shared minutes is therefore an independent validation of the decoder AND of the
scale, in the same way matching the tick-derived SP500 once was - and it is the only
such check that survives the tick files being gone.
"""
print()
print(f"{'pair':>19}{'shared min':>13}{'corr':>10}{'median diff':>14}{'in bp':>9}")
for a, b in catalog.PAIRS:
try:
ba, _ = load(catalog.KEY[a], 'M1', verbose=False)
bb, _ = load(catalog.KEY[b], 'M1', verbose=False)
except Exception as ex:
print(f"{a+'/'+b:>19} SKIPPED: {ex}")
continue
_, ia, ib = np.intersect1d(ba[:, 0].astype(np.int64),
bb[:, 0].astype(np.int64), return_indices=True)
if len(ia) < 1000:
print(f"{a+'/'+b:>19}{len(ia):>13,} too few shared minutes to compare")
continue
ca, cb = ba[ia, 4], bb[ib, 4]
d = np.median(ca - cb)
print(f"{a+'/'+b:>19}{len(ia):>13,}{np.corrcoef(ca, cb)[0,1]:>10.6f}"
f"{d:>14.5f}{1e4*d/np.median(cb):>9.2f}")
if __name__ == '__main__':
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
print("=== SQX BAR DECODER: build M1 cache for every catalog instrument ===")
for k in catalog.SYMS:
try:
tf = catalog.base_tf(k)
load(k, tf)
except Exception as ex:
print(f" {catalog.nice(k):>9}: FAILED - {ex}")
_cross_feed_report()