TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1/** Small, dependency-free numeric helpers used by valuation, indices and scores. */23export function median(values: number[]): number | null {4 if (values.length === 0) return null;5 const s = [...values].sort((a, b) => a - b);6 const mid = Math.floor(s.length / 2);7 return s.length % 2 ? s[mid]! : (s[mid - 1]! + s[mid]!) / 2;8}910export function mean(values: number[]): number | null {11 if (values.length === 0) return null;12 return values.reduce((a, b) => a + b, 0) / values.length;13}1415export function quantile(values: number[], q: number): number | null {16 if (values.length === 0) return null;17 const s = [...values].sort((a, b) => a - b);18 const pos = (s.length - 1) * q;19 const lo = Math.floor(pos);20 const hi = Math.ceil(pos);21 if (lo === hi) return s[lo]!;22 return s[lo]! + (s[hi]! - s[lo]!) * (pos - lo);23}2425/** Trimmed mean dropping `trim` fraction from each tail (e.g. 0.1). */26export function trimmedMean(values: number[], trim = 0.1): number | null {27 if (values.length === 0) return null;28 const s = [...values].sort((a, b) => a - b);29 const k = Math.floor(s.length * trim);30 const core = s.slice(k, s.length - k);31 return mean(core.length ? core : s);32}3334export function stddev(values: number[]): number | null {35 if (values.length < 2) return null;36 const m = mean(values)!;37 const v = values.reduce((acc, x) => acc + (x - m) ** 2, 0) / (values.length - 1);38 return Math.sqrt(v);39}4041/** Median absolute deviation (robust dispersion). */42export function mad(values: number[]): number | null {43 const m = median(values);44 if (m === null) return null;45 return median(values.map((v) => Math.abs(v - m)));46}4748/** Exponentially weighted mean; `halfLifeDays` controls decay by age (days). Most recent = highest weight. */49export function ewMean(points: Array<{ value: number; ageDays: number }>, halfLifeDays = 60): number | null {50 if (points.length === 0) return null;51 let num = 0;52 let den = 0;53 for (const p of points) {54 const w = Math.pow(0.5, Math.max(0, p.ageDays) / halfLifeDays);55 num += w * p.value;56 den += w;57 }58 return den > 0 ? num / den : null;59}6061export function weightedMean(points: Array<{ value: number; weight: number }>): number | null {62 let num = 0;63 let den = 0;64 for (const p of points) {65 num += p.value * p.weight;66 den += p.weight;67 }68 return den > 0 ? num / den : null;69}7071export function clamp(x: number, lo: number, hi: number): number {72 return Math.min(hi, Math.max(lo, x));73}7475/** Percent change guarded against zero/null. */76export function pctChange(from: number | null | undefined, to: number | null | undefined): number | null {77 if (from === null || from === undefined || to === null || to === undefined || from === 0) return null;78 return (to - from) / from;79}8081/** Max drawdown of a value series (0..1). */82export function maxDrawdown(series: number[]): number | null {83 if (series.length < 2) return null;84 let peak = series[0]!;85 let mdd = 0;86 for (const v of series) {87 if (v > peak) peak = v;88 if (peak > 0) mdd = Math.max(mdd, (peak - v) / peak);89 }90 return mdd;91}9293/** Annualised volatility of log returns from an evenly spaced series. */94export function volatility(series: number[], periodsPerYear = 365): number | null {95 if (series.length < 3) return null;96 const rets: number[] = [];97 for (let i = 1; i < series.length; i++) {98 const a = series[i - 1]!;99 const b = series[i]!;100 if (a > 0 && b > 0) rets.push(Math.log(b / a));101 }102 const sd = stddev(rets);103 return sd === null ? null : sd * Math.sqrt(periodsPerYear);104}105106export function round(x: number, digits = 2): number {107 const f = 10 ** digits;108 return Math.round(x * f) / f;109}110