377 lines
13 KiB
JavaScript
377 lines
13 KiB
JavaScript
// Technical Indicators Calculator
|
|
|
|
class TechnicalIndicators {
|
|
|
|
// Simple Moving Average (SMA)
|
|
static SMA(data, period) {
|
|
const result = [];
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (i < period - 1) {
|
|
result.push({ time: data[i].time, value: null });
|
|
} else {
|
|
let sum = 0;
|
|
for (let j = 0; j < period; j++) {
|
|
sum += data[i - j].close;
|
|
}
|
|
result.push({ time: data[i].time, value: sum / period });
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Exponential Moving Average (EMA)
|
|
static EMA(data, period) {
|
|
const result = [];
|
|
const multiplier = 2 / (period + 1);
|
|
|
|
// First EMA uses SMA
|
|
let sum = 0;
|
|
for (let i = 0; i < period; i++) {
|
|
sum += data[i].close;
|
|
result.push({ time: data[i].time, value: null });
|
|
}
|
|
result[period - 1] = { time: data[period - 1].time, value: sum / period };
|
|
|
|
// Calculate EMA for remaining data
|
|
for (let i = period; i < data.length; i++) {
|
|
const ema = (data[i].close - result[i - 1].value) * multiplier + result[i - 1].value;
|
|
result.push({ time: data[i].time, value: ema });
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Bollinger Bands
|
|
static BollingerBands(data, period = 20, stdDev = 2) {
|
|
const sma = this.SMA(data, period);
|
|
const upper = [];
|
|
const lower = [];
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (i < period - 1) {
|
|
upper.push({ time: data[i].time, value: null });
|
|
lower.push({ time: data[i].time, value: null });
|
|
} else {
|
|
// Calculate standard deviation
|
|
let sumSquares = 0;
|
|
for (let j = 0; j < period; j++) {
|
|
sumSquares += Math.pow(data[i - j].close - sma[i].value, 2);
|
|
}
|
|
const std = Math.sqrt(sumSquares / period);
|
|
|
|
upper.push({ time: data[i].time, value: sma[i].value + stdDev * std });
|
|
lower.push({ time: data[i].time, value: sma[i].value - stdDev * std });
|
|
}
|
|
}
|
|
|
|
return { middle: sma, upper, lower };
|
|
}
|
|
|
|
// MACD (Moving Average Convergence Divergence)
|
|
static MACD(data, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
|
|
const emaFast = this.EMA(data, fastPeriod);
|
|
const emaSlow = this.EMA(data, slowPeriod);
|
|
|
|
// MACD Line = Fast EMA - Slow EMA
|
|
const macdLine = [];
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (emaFast[i].value === null || emaSlow[i].value === null) {
|
|
macdLine.push({ time: data[i].time, value: null, close: null });
|
|
} else {
|
|
const value = emaFast[i].value - emaSlow[i].value;
|
|
macdLine.push({ time: data[i].time, value, close: value });
|
|
}
|
|
}
|
|
|
|
// Signal Line = EMA of MACD Line
|
|
const validMacdData = macdLine.filter(d => d.value !== null);
|
|
const signalEma = this.EMA(validMacdData, signalPeriod);
|
|
|
|
// Map signal back to full timeline
|
|
const signalLine = [];
|
|
let signalIndex = 0;
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (macdLine[i].value === null || signalIndex >= signalEma.length) {
|
|
signalLine.push({ time: data[i].time, value: null });
|
|
} else if (signalEma[signalIndex] && signalEma[signalIndex].time === data[i].time) {
|
|
signalLine.push({ time: data[i].time, value: signalEma[signalIndex].value });
|
|
signalIndex++;
|
|
} else {
|
|
signalLine.push({ time: data[i].time, value: null });
|
|
}
|
|
}
|
|
|
|
// Histogram = MACD Line - Signal Line
|
|
const histogram = [];
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (macdLine[i].value === null || signalLine[i].value === null) {
|
|
histogram.push({ time: data[i].time, value: 0, color: '#363a45' });
|
|
} else {
|
|
const value = macdLine[i].value - signalLine[i].value;
|
|
histogram.push({
|
|
time: data[i].time,
|
|
value,
|
|
color: value >= 0 ? '#26a69a' : '#ef5350'
|
|
});
|
|
}
|
|
}
|
|
|
|
return { macdLine, signalLine, histogram };
|
|
}
|
|
|
|
// RSI (Relative Strength Index)
|
|
static RSI(data, period = 14) {
|
|
const result = [];
|
|
const gains = [];
|
|
const losses = [];
|
|
|
|
// Calculate price changes
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (i === 0) {
|
|
gains.push(0);
|
|
losses.push(0);
|
|
result.push({ time: data[i].time, value: null });
|
|
continue;
|
|
}
|
|
|
|
const change = data[i].close - data[i - 1].close;
|
|
gains.push(change > 0 ? change : 0);
|
|
losses.push(change < 0 ? Math.abs(change) : 0);
|
|
|
|
if (i < period) {
|
|
result.push({ time: data[i].time, value: null });
|
|
} else if (i === period) {
|
|
// First RSI calculation uses simple average
|
|
let avgGain = 0, avgLoss = 0;
|
|
for (let j = 1; j <= period; j++) {
|
|
avgGain += gains[j];
|
|
avgLoss += losses[j];
|
|
}
|
|
avgGain /= period;
|
|
avgLoss /= period;
|
|
|
|
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
|
const rsi = 100 - (100 / (1 + rs));
|
|
result.push({ time: data[i].time, value: rsi, avgGain, avgLoss });
|
|
} else {
|
|
// Subsequent RSI uses smoothed average
|
|
const prevAvgGain = result[i - 1].avgGain;
|
|
const prevAvgLoss = result[i - 1].avgLoss;
|
|
const avgGain = (prevAvgGain * (period - 1) + gains[i]) / period;
|
|
const avgLoss = (prevAvgLoss * (period - 1) + losses[i]) / period;
|
|
|
|
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
|
const rsi = 100 - (100 / (1 + rs));
|
|
result.push({ time: data[i].time, value: rsi, avgGain, avgLoss });
|
|
}
|
|
}
|
|
|
|
return result.map(d => ({ time: d.time, value: d.value }));
|
|
}
|
|
|
|
// Stochastic Oscillator
|
|
static Stochastic(data, kPeriod = 14, dPeriod = 3, smooth = 3) {
|
|
const kValues = [];
|
|
|
|
// Calculate %K
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (i < kPeriod - 1) {
|
|
kValues.push({ time: data[i].time, value: null, close: null });
|
|
} else {
|
|
let highest = -Infinity;
|
|
let lowest = Infinity;
|
|
|
|
for (let j = 0; j < kPeriod; j++) {
|
|
highest = Math.max(highest, data[i - j].high);
|
|
lowest = Math.min(lowest, data[i - j].low);
|
|
}
|
|
|
|
const k = highest === lowest ? 50 : ((data[i].close - lowest) / (highest - lowest)) * 100;
|
|
kValues.push({ time: data[i].time, value: k, close: k });
|
|
}
|
|
}
|
|
|
|
// Smooth %K (Fast %K -> Slow %K)
|
|
const slowK = [];
|
|
for (let i = 0; i < kValues.length; i++) {
|
|
if (i < kPeriod - 1 + smooth - 1) {
|
|
slowK.push({ time: kValues[i].time, value: null, close: null });
|
|
} else {
|
|
let sum = 0;
|
|
let count = 0;
|
|
for (let j = 0; j < smooth; j++) {
|
|
if (kValues[i - j].value !== null) {
|
|
sum += kValues[i - j].value;
|
|
count++;
|
|
}
|
|
}
|
|
const value = count > 0 ? sum / count : null;
|
|
slowK.push({ time: kValues[i].time, value, close: value });
|
|
}
|
|
}
|
|
|
|
// Calculate %D (SMA of Slow %K)
|
|
const dValues = [];
|
|
for (let i = 0; i < slowK.length; i++) {
|
|
if (i < kPeriod - 1 + smooth - 1 + dPeriod - 1) {
|
|
dValues.push({ time: slowK[i].time, value: null });
|
|
} else {
|
|
let sum = 0;
|
|
let count = 0;
|
|
for (let j = 0; j < dPeriod; j++) {
|
|
if (slowK[i - j].value !== null) {
|
|
sum += slowK[i - j].value;
|
|
count++;
|
|
}
|
|
}
|
|
dValues.push({ time: slowK[i].time, value: count > 0 ? sum / count : null });
|
|
}
|
|
}
|
|
|
|
return {
|
|
k: slowK.map(d => ({ time: d.time, value: d.value })),
|
|
d: dValues
|
|
};
|
|
}
|
|
|
|
// Detect Golden Cross / Dead Cross
|
|
static detectCrosses(shortMA, longMA) {
|
|
const signals = [];
|
|
|
|
for (let i = 1; i < shortMA.length; i++) {
|
|
if (shortMA[i].value === null || longMA[i].value === null ||
|
|
shortMA[i - 1].value === null || longMA[i - 1].value === null) {
|
|
continue;
|
|
}
|
|
|
|
const prevShort = shortMA[i - 1].value;
|
|
const prevLong = longMA[i - 1].value;
|
|
const currShort = shortMA[i].value;
|
|
const currLong = longMA[i].value;
|
|
|
|
// Golden Cross: Short MA crosses above Long MA
|
|
if (prevShort <= prevLong && currShort > currLong) {
|
|
signals.push({
|
|
time: shortMA[i].time,
|
|
type: 'golden',
|
|
description: 'Golden Cross'
|
|
});
|
|
}
|
|
|
|
// Dead Cross: Short MA crosses below Long MA
|
|
if (prevShort >= prevLong && currShort < currLong) {
|
|
signals.push({
|
|
time: shortMA[i].time,
|
|
type: 'dead',
|
|
description: 'Dead Cross'
|
|
});
|
|
}
|
|
}
|
|
|
|
return signals;
|
|
}
|
|
|
|
// Detect RSI signals
|
|
static detectRSISignals(rsi, oversold = 30, overbought = 70) {
|
|
const signals = [];
|
|
|
|
for (let i = 1; i < rsi.length; i++) {
|
|
if (rsi[i].value === null || rsi[i - 1].value === null) continue;
|
|
|
|
// Oversold -> Recovery (Buy signal)
|
|
if (rsi[i - 1].value < oversold && rsi[i].value >= oversold) {
|
|
signals.push({
|
|
time: rsi[i].time,
|
|
type: 'buy',
|
|
description: 'RSI Oversold Recovery'
|
|
});
|
|
}
|
|
|
|
// Overbought -> Decline (Sell signal)
|
|
if (rsi[i - 1].value > overbought && rsi[i].value <= overbought) {
|
|
signals.push({
|
|
time: rsi[i].time,
|
|
type: 'sell',
|
|
description: 'RSI Overbought Decline'
|
|
});
|
|
}
|
|
}
|
|
|
|
return signals;
|
|
}
|
|
|
|
// Detect MACD signals
|
|
static detectMACDSignals(macd) {
|
|
const signals = [];
|
|
const { macdLine, signalLine } = macd;
|
|
|
|
for (let i = 1; i < macdLine.length; i++) {
|
|
if (macdLine[i].value === null || signalLine[i].value === null ||
|
|
macdLine[i - 1].value === null || signalLine[i - 1].value === null) {
|
|
continue;
|
|
}
|
|
|
|
const prevMacd = macdLine[i - 1].value;
|
|
const prevSignal = signalLine[i - 1].value;
|
|
const currMacd = macdLine[i].value;
|
|
const currSignal = signalLine[i].value;
|
|
|
|
// MACD crosses above Signal (Buy)
|
|
if (prevMacd <= prevSignal && currMacd > currSignal) {
|
|
signals.push({
|
|
time: macdLine[i].time,
|
|
type: 'buy',
|
|
description: 'MACD Bullish Cross'
|
|
});
|
|
}
|
|
|
|
// MACD crosses below Signal (Sell)
|
|
if (prevMacd >= prevSignal && currMacd < currSignal) {
|
|
signals.push({
|
|
time: macdLine[i].time,
|
|
type: 'sell',
|
|
description: 'MACD Bearish Cross'
|
|
});
|
|
}
|
|
}
|
|
|
|
return signals;
|
|
}
|
|
|
|
// Detect Stochastic signals
|
|
static detectStochasticSignals(stochastic, oversold = 20, overbought = 80) {
|
|
const signals = [];
|
|
const { k, d } = stochastic;
|
|
|
|
for (let i = 1; i < k.length; i++) {
|
|
if (k[i].value === null || d[i].value === null ||
|
|
k[i - 1].value === null || d[i - 1].value === null) {
|
|
continue;
|
|
}
|
|
|
|
// %K crosses above %D in oversold zone (Buy)
|
|
if (k[i - 1].value <= d[i - 1].value && k[i].value > d[i].value && k[i].value < oversold) {
|
|
signals.push({
|
|
time: k[i].time,
|
|
type: 'buy',
|
|
description: 'Stochastic Oversold Cross'
|
|
});
|
|
}
|
|
|
|
// %K crosses below %D in overbought zone (Sell)
|
|
if (k[i - 1].value >= d[i - 1].value && k[i].value < d[i].value && k[i].value > overbought) {
|
|
signals.push({
|
|
time: k[i].time,
|
|
type: 'sell',
|
|
description: 'Stochastic Overbought Cross'
|
|
});
|
|
}
|
|
}
|
|
|
|
return signals;
|
|
}
|
|
}
|
|
|
|
// Export for use in app.js
|
|
window.TechnicalIndicators = TechnicalIndicators;
|