"""Turn raw (day, price-bin, weight) profiles into the operational levels the books use. Every level here is STRICTLY CAUSAL: a level in force on broker-day d is computed only from days < d. That is not a formality. A session VPOC is the single most tempting lookahead in this whole subject, because the natural way to draw it on a chart is over the day you are looking at - and a level fitted to the day it is tested on will "hold" beautifully. Levels produced --------------- vpoc / vah / val previous session's point of control and value area edges naked vpoc a past session VPOC that price has NOT traded through since. Held to be an unfinished auction and therefore a magnet. hvn / lvn peaks and valleys of a rolling composite profile over the previous N sessions. HVN = agreement, claimed to attract and to make good targets; LVN = rejection, claimed to make good stop locations. """ import numpy as np from scipy.signal import find_peaks BROKER_OFFSET_MS = 2 * 3600 * 1000 DAY_MS = 86400 * 1000 def broker_day(t_ms): return ((np.asarray(t_ms, np.int64) + BROKER_OFFSET_MS) // DAY_MS).astype(np.int64) def load(sym, path='c:/Users/admin/Documents/Workspaces/Market Data/profiles/'): z = np.load(f"{path}{sym}_vp.npz") return z['day'], z['bin'], z['ticks'], z['dwell'], float(z['binsize'][0]) def day_index(day, b, w): """Group the sparse cells by day. Returns (days, list of (bins, weights)) sorted.""" order = np.lexsort((b, day)) day, b, w = day[order], b[order], w[order] starts = np.concatenate(([0], np.flatnonzero(np.diff(day)) + 1)) ends = np.concatenate((starts[1:], [len(day)])) return day[starts], [(b[s:e], w[s:e]) for s, e in zip(starts, ends)] def densify(bins, w): """Sparse bins -> contiguous array with explicit zeros for untraded levels inside the range. Without the zeros a value-area walk annexes across a hole as if it were adjacent and low-volume nodes cease to exist - which is the whole point of them.""" full = np.arange(bins[0], bins[-1] + 1) dense = np.zeros(len(full)) dense[bins - bins[0]] = w return full, dense def session_levels(days, cells, frac=0.70): """Per-session VPOC / VAL / VAH / high / low, all as integer bin indices.""" from profiles import value_area out = np.empty((len(days), 6), np.int64) for i, (bins, w) in enumerate(cells): full, dense = densify(bins, w) poc, lo, hi = value_area(full, dense, frac) out[i] = (days[i], poc, lo, hi, bins[0], bins[-1]) return out # day, vpoc, val, vah, low, high def composite_nodes(days, cells, lookback=20, smooth=5, prominence=0.35): """HVN/LVN from a rolling composite of the PREVIOUS `lookback` sessions. Smoothing first is not cosmetic: on a raw 0.1-pip grid every second bin is a local maximum, and "find the peaks" without it returns a few hundred meaningless nodes per day, which would let any subsequent test claim a hit against whatever price did. `prominence` is in units of the smoothed profile's own standard deviation, so it means the same thing on gold and on EURUSD. Returns {day: (hvn_bins, lvn_bins)} for every day that has a full lookback behind it. """ out = {} for i in range(lookback, len(days)): bb = np.concatenate([cells[j][0] for j in range(i - lookback, i)]) ww = np.concatenate([cells[j][1] for j in range(i - lookback, i)]) k, inv = np.unique(bb, return_inverse=True) agg = np.bincount(inv, weights=ww, minlength=len(k)) full, dense = densify(k, agg) if len(full) < 4 * smooth: continue ker = np.ones(smooth) / smooth sm = np.convolve(dense, ker, mode='same') p = prominence * sm.std() hv, _ = find_peaks(sm, prominence=p) lv, _ = find_peaks(-sm, prominence=p) out[int(days[i])] = (full[hv], full[lv]) return out def naked_vpocs(sess, max_age=60): """For each day, the past VPOCs price has not traded through yet. A VPOC is 'naked' until a LATER session's range covers it. Only sessions strictly before the current one can retire it, so the answer for day d never depends on day d. Capped at `max_age` sessions back: a naked VPOC from three years ago is not an unfinished auction, it is a price the market has left behind, and including it would load the test with levels no trader would draw. Returns {day: array of still-naked vpoc bins}, nearest-in-time first. """ out = {} vpoc = sess[:, 1]; lo = sess[:, 4]; hi = sess[:, 5] for i in range(1, len(sess)): j0 = max(0, i - max_age) cand = vpoc[j0:i] #--- covered by any session strictly between its own and today alive = np.ones(len(cand), bool) for k in range(len(cand)): src = j0 + k if src + 1 >= i: continue rl = lo[src + 1:i]; rh = hi[src + 1:i] if np.any((rl <= cand[k]) & (rh >= cand[k])): alive[k] = False out[int(sess[i, 0])] = cand[alive][::-1] return out