Files
bini-trading-view/indicators.js

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;