Files
goldenChart/frontend/src/utils/calculations.ts
T
2026-05-23 15:11:48 +09:00

129 lines
4.2 KiB
TypeScript

import type { OHLCVBar } from '../types';
export interface PriceStats {
current: number;
change: number;
changePct: number;
high52w: number;
low52w: number;
avgVol: number;
avgRange: number;
volatility: number; // annualized std dev of returns (%)
rrsi14: number; // rough RSI(14) approximation
pivotH: number; // classic pivot points
pivotL: number;
pivotP: number;
support1: number;
support2: number;
resistance1: number;
resistance2: number;
trend: 'up' | 'down' | 'sideways';
}
export function calcStats(bars: OHLCVBar[]): PriceStats {
if (bars.length < 2) {
return {
current: 0, change: 0, changePct: 0,
high52w: 0, low52w: 0, avgVol: 0, avgRange: 0,
volatility: 0, rrsi14: 50,
pivotH: 0, pivotL: 0, pivotP: 0,
support1: 0, support2: 0, resistance1: 0, resistance2: 0,
trend: 'sideways',
};
}
const last = bars[bars.length - 1];
const prev = bars[bars.length - 2];
const change = last.close - prev.close;
const changePct = change / prev.close * 100;
const lookback = Math.min(bars.length, 252);
const slice = bars.slice(-lookback);
const high52w = Math.max(...slice.map(b => b.high));
const low52w = Math.min(...slice.map(b => b.low));
const avgVol = slice.reduce((s, b) => s + b.volume, 0) / slice.length;
const avgRange= slice.reduce((s, b) => s + (b.high - b.low), 0) / slice.length;
// Annualized volatility
const returns = slice.slice(1).map((b, i) => Math.log(b.close / slice[i].close));
const mean = returns.reduce((s, r) => s + r, 0) / returns.length;
const variance = returns.reduce((s, r) => s + (r - mean) ** 2, 0) / returns.length;
const volatility = Math.sqrt(variance * 252) * 100;
// Rough RSI(14)
const rsiSlice = bars.slice(-15);
let gains = 0, losses = 0;
for (let i = 1; i < rsiSlice.length; i++) {
const d = rsiSlice[i].close - rsiSlice[i - 1].close;
if (d > 0) gains += d; else losses -= d;
}
const avgGain = gains / 14;
const avgLoss = losses / 14;
const rrsi14 = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
// Classic Pivot Points (from last bar)
const pivotH = last.high;
const pivotL = last.low;
const pivotP = (last.high + last.low + last.close) / 3;
const resistance1 = 2 * pivotP - last.low;
const support1 = 2 * pivotP - last.high;
const resistance2 = pivotP + (last.high - last.low);
const support2 = pivotP - (last.high - last.low);
// Trend (20-bar EMA vs 50-bar EMA)
const ema20 = calcEMA(bars.map(b => b.close), 20);
const ema50 = calcEMA(bars.map(b => b.close), 50);
const e20 = ema20[ema20.length - 1];
const e50 = ema50[ema50.length - 1];
const trend = e20 > e50 * 1.002 ? 'up' : e20 < e50 * 0.998 ? 'down' : 'sideways';
return {
current: last.close, change, changePct,
high52w, low52w, avgVol, avgRange, volatility, rrsi14,
pivotH, pivotL, pivotP,
support1, support2, resistance1, resistance2,
trend,
};
}
function calcEMA(prices: number[], period: number): number[] {
const k = 2 / (period + 1);
const result: number[] = [];
let ema = prices[0];
for (const p of prices) {
ema = p * k + ema * (1 - k);
result.push(ema);
}
return result;
}
/** Auto-calculate Fibonacci retracement levels from visible data */
export function calcAutoFib(bars: OHLCVBar[], lookback = 50): {
high: number; low: number; highTime: number; lowTime: number;
levels: Array<{ ratio: number; price: number; label: string }>;
} {
const slice = bars.slice(-lookback);
let high = -Infinity, low = Infinity, highTime = 0, lowTime = 0;
for (const b of slice) {
if (b.high > high) { high = b.high; highTime = b.time; }
if (b.low < low) { low = b.low; lowTime = b.time; }
}
const range = high - low;
const LEVELS = [
{ ratio: 0, label: '0%' },
{ ratio: 0.236, label: '23.6%' },
{ ratio: 0.382, label: '38.2%' },
{ ratio: 0.5, label: '50%' },
{ ratio: 0.618, label: '61.8%' },
{ ratio: 0.786, label: '78.6%' },
{ ratio: 1, label: '100%' },
{ ratio: 1.272, label: '127.2%' },
{ ratio: 1.618, label: '161.8%' },
];
return {
high, low, highTime, lowTime,
levels: LEVELS.map(l => ({ ...l, price: high - range * l.ratio })),
};
}