feat: dealer flow, mirror portfolio (M21), options convexity, FINRA short interest, alert producers, vendor gate
CI / Test & Type-Check (push) Canceled after 0s
CI / Test & Type-Check (push) Canceled after 0s
Snapshot of in-progress module work across multiple slices: - Dealer Flow: dealerExposureEngine, dealerMapService, dealerMapExplain, dealerMapIntegrity, dealerMapReplay, dealerStudyEngine, hanStyleLevels - Mirror Portfolio (M21): fundRepository, captureIngest, mirrorAlertProducers, fund holdings strip, live book, position capture ingest - Options: BSM, NormalizedOptionSurface types, OptionsChainRouter, ConvexityGate, option legs panel - Alert producers: vixLevel, rotation, thesis, unlock, portfolioRisk, mirror (fund_capture, fund_13f, mirror_diff) - FINRA short interest adapter + queue integration - SEC company tickers adapter + ingest (symbol search index seed) - Vendor gate (rate-limit-first data plane, ADR-0009) - CUSIP registry, reverse 13F refresh, stock float service - LRU cache, portfolio backtest engine - Frontend: dealer-flow, funds, journal, lab, monitor, plan, portfolio, reports, screener, strategies, theses, guided-start, exits, more pages - Volume profile, workspace profile, visibility-aware poll - ADRs 0010 (mirror math not advice), 0011 (symbol search index) - VENDOR_INTEGRATIONS.md, END_USER_TEST.md - .gitignore: exclude DBs, .DS_Store, local config, agent scratch
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Equity volume-by-price profile from OHLC bars (not options OI).
|
||||
* Uniform volume split across bins that a bar's low–high spans.
|
||||
*/
|
||||
|
||||
export interface VolumeProfileCandle {
|
||||
h: number;
|
||||
l: number;
|
||||
c?: number;
|
||||
v: number;
|
||||
}
|
||||
|
||||
export interface VolumeProfileBin {
|
||||
priceLo: number;
|
||||
priceHi: number;
|
||||
mid: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export interface VolumeProfileResult {
|
||||
bins: VolumeProfileBin[];
|
||||
/** Mid-price of the highest-volume bin. */
|
||||
pocMid: number | null;
|
||||
/** Value-area high / low (mids of outer bins in the value area). */
|
||||
vah: number | null;
|
||||
val: number | null;
|
||||
totalVolume: number;
|
||||
}
|
||||
|
||||
export interface BuildVolumeProfileOpts {
|
||||
/** Number of price bins. Default 32. */
|
||||
bins?: number;
|
||||
/** Fraction of volume for value area (e.g. 0.7). Default 0.7. */
|
||||
valueAreaPct?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a volume profile from candles in the visible range.
|
||||
* Empty or zero-volume inputs return empty bins.
|
||||
*/
|
||||
export function buildVolumeProfile(
|
||||
candles: VolumeProfileCandle[],
|
||||
opts: BuildVolumeProfileOpts = {},
|
||||
): VolumeProfileResult {
|
||||
const binCount = Math.max(2, Math.min(80, Math.floor(opts.bins ?? 32)));
|
||||
const valueAreaPct = Math.min(0.99, Math.max(0.5, opts.valueAreaPct ?? 0.7));
|
||||
|
||||
const usable = candles.filter(
|
||||
(c) =>
|
||||
Number.isFinite(c.h) &&
|
||||
Number.isFinite(c.l) &&
|
||||
Number.isFinite(c.v) &&
|
||||
c.v > 0 &&
|
||||
c.h >= c.l,
|
||||
);
|
||||
|
||||
if (usable.length === 0) {
|
||||
return { bins: [], pocMid: null, vah: null, val: null, totalVolume: 0 };
|
||||
}
|
||||
|
||||
let priceMin = Infinity;
|
||||
let priceMax = -Infinity;
|
||||
let totalVolume = 0;
|
||||
for (const c of usable) {
|
||||
if (c.l < priceMin) priceMin = c.l;
|
||||
if (c.h > priceMax) priceMax = c.h;
|
||||
totalVolume += c.v;
|
||||
}
|
||||
|
||||
// Flat range: single effective level
|
||||
if (!(priceMax > priceMin)) {
|
||||
const mid = priceMin;
|
||||
const bins: VolumeProfileBin[] = [
|
||||
{ priceLo: mid, priceHi: mid, mid, volume: totalVolume },
|
||||
];
|
||||
return { bins, pocMid: mid, vah: mid, val: mid, totalVolume };
|
||||
}
|
||||
|
||||
const width = (priceMax - priceMin) / binCount;
|
||||
const volumes = new Array<number>(binCount).fill(0);
|
||||
|
||||
for (const c of usable) {
|
||||
const lo = c.l;
|
||||
const hi = c.h;
|
||||
// Inclusive bins from low to high
|
||||
let i0 = Math.floor((lo - priceMin) / width);
|
||||
let i1 = Math.floor((hi - priceMin) / width);
|
||||
if (i0 < 0) i0 = 0;
|
||||
if (i1 >= binCount) i1 = binCount - 1;
|
||||
if (i1 < i0) i1 = i0;
|
||||
const n = i1 - i0 + 1;
|
||||
const share = c.v / n;
|
||||
for (let i = i0; i <= i1; i++) volumes[i] += share;
|
||||
}
|
||||
|
||||
const bins: VolumeProfileBin[] = volumes.map((volume, i) => {
|
||||
const priceLo = priceMin + i * width;
|
||||
const priceHi = i === binCount - 1 ? priceMax : priceMin + (i + 1) * width;
|
||||
return {
|
||||
priceLo,
|
||||
priceHi,
|
||||
mid: (priceLo + priceHi) / 2,
|
||||
volume,
|
||||
};
|
||||
});
|
||||
|
||||
// POC = max volume bin
|
||||
let pocIdx = 0;
|
||||
for (let i = 1; i < bins.length; i++) {
|
||||
if (bins[i].volume > bins[pocIdx].volume) pocIdx = i;
|
||||
}
|
||||
|
||||
// Value area: expand from POC until we cover valueAreaPct of total volume
|
||||
const target = totalVolume * valueAreaPct;
|
||||
let lo = pocIdx;
|
||||
let hi = pocIdx;
|
||||
let covered = bins[pocIdx].volume;
|
||||
while (covered < target && (lo > 0 || hi < bins.length - 1)) {
|
||||
const nextLo = lo > 0 ? bins[lo - 1].volume : -1;
|
||||
const nextHi = hi < bins.length - 1 ? bins[hi + 1].volume : -1;
|
||||
if (nextHi >= nextLo) {
|
||||
hi++;
|
||||
covered += bins[hi].volume;
|
||||
} else {
|
||||
lo--;
|
||||
covered += bins[lo].volume;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bins,
|
||||
pocMid: bins[pocIdx].mid,
|
||||
vah: bins[hi].priceHi,
|
||||
val: bins[lo].priceLo,
|
||||
totalVolume,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user