CI / Test & Type-Check (push) Canceled after 0s
New functions in indicators.ts: - sma: Simple Moving Average - crossover: golden/death cross detector - roc: Rate of Change (momentum) - timeSeriesMomentum: 12-1 month momentum (Moskowitz et al.) - relativeStrength: symbol vs benchmark momentum differential - realizedVol: annualized realized volatility from log returns - volatilityPercentile: percentile rank of current vol vs history - fibLevels: Fibonacci retracement and extension levels - fibCluster: proximity check for Fib confluence zones New file volumeByPrice.ts: - buildVolumeProfile: ported from frontend volumeProfile.ts - Identical algorithm: bin clamping, uniform volume split, POC/VAH/VAL All functions are pure (zero deps, no I/O). Tests added for all new functions with edge cases, error handling, and numerical correctness.
121 lines
3.1 KiB
TypeScript
121 lines
3.1 KiB
TypeScript
// Investor Flow — Volume-by-price profile (backend port).
|
|
// Pure function. Zero external dependencies.
|
|
|
|
export interface VolumeProfileBin {
|
|
priceLo: number;
|
|
priceHi: number;
|
|
mid: number;
|
|
volume: number;
|
|
}
|
|
|
|
export interface VolumeProfileResult {
|
|
bins: VolumeProfileBin[];
|
|
pocMid: number | null;
|
|
vah: number | null;
|
|
val: number | null;
|
|
totalVolume: number;
|
|
}
|
|
|
|
export interface BuildVolumeProfileOpts {
|
|
bins?: number;
|
|
valueAreaPct?: number;
|
|
}
|
|
|
|
/**
|
|
* Build a volume profile from candles. Uniform volume split across bins that a
|
|
* bar's low-high spans. Empty or zero-volume inputs return empty bins.
|
|
*/
|
|
export function buildVolumeProfile(
|
|
candles: { h: number; l: number; v: number }[],
|
|
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;
|
|
}
|
|
|
|
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;
|
|
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,
|
|
};
|
|
});
|
|
|
|
let pocIdx = 0;
|
|
for (let i = 1; i < bins.length; i++) {
|
|
if (bins[i].volume > bins[pocIdx].volume) pocIdx = i;
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|