SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.6 KB · 110 lines typescript
Raw Blame History
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