ta-pattern-lib
Version:
Technical Analysis and Backtesting Framework for Node.js
350 lines • 13.7 kB
JavaScript
import round from "lodash.round";
import { compute_ema, stats_mean } from "./feature_statistics";
export const compute_adx = (ohlc_data, period) => {
const true_ranges = [];
const positive_dms = [];
const negative_dms = [];
const dx_values = [];
const adx_values = [];
// Calculate True Range (TR), Positive Directional Movement (+DM), Negative Directional Movement (-DM)
for (let i = 1; i < ohlc_data.length; i++) {
const current_high = ohlc_data[i].high;
const current_low = ohlc_data[i].low;
const previous_close = ohlc_data[i - 1].close;
const true_range = Math.max(current_high - current_low, Math.abs(current_high - previous_close), Math.abs(current_low - previous_close));
true_ranges.push(true_range);
const positive_dm = current_high - ohlc_data[i - 1].high;
const negative_dm = ohlc_data[i - 1].low - current_low;
positive_dms.push(positive_dm > 0 ? positive_dm : 0);
negative_dms.push(negative_dm > 0 ? negative_dm : 0);
}
// Calculate the smoothed average of True Range (ATR)
const atr_values = calculate_smoothed_average(true_ranges, period);
// Calculate the smoothed averages of Positive Directional Movement (+DM) and Negative Directional Movement (-DM)
const positive_dm_mas = calculate_smoothed_average(positive_dms, period);
const negative_dm_mas = calculate_smoothed_average(negative_dms, period);
// Calculate the Directional Movement Index (DX) and ADX
for (let i = period; i < ohlc_data.length - 1; i++) {
const di_plus = (positive_dm_mas[i - period] / atr_values[i - period]) * 100;
const di_minus = (negative_dm_mas[i - period] / atr_values[i - period]) * 100;
const dx = (Math.abs(di_plus - di_minus) / (di_plus + di_minus)) * 100;
dx_values.push(dx);
if (dx_values.length >= period) {
const adx = calculate_smoothed_average(dx_values, period)[dx_values.length - period];
adx_values.push(adx);
}
}
return adx_values;
};
export const compute_macd = (data, short_period = 12, long_period = 26, signal_period = 9) => {
// Calculate the short EMA
const short_ema = compute_ema(data, short_period);
// Calculate the long EMA
const long_ema = compute_ema(data, long_period);
// Calculate the MACD line
const macd_line = [];
for (let i = 0; i < data.length; i++) {
macd_line.push(short_ema[i] - long_ema[i]);
}
// Calculate the signal line using SMA of the MACD line
const signal_line = compute_ema(macd_line, signal_period);
// Return the MACD line and signal line
return {
macd: macd_line,
signal_line: signal_line,
};
};
export const compute_obv = (data) => {
const obv_values = [];
obv_values.push(0); // Initialize OBV with zero as the starting value
for (let i = 1; i < data.length; i++) {
const current_close = data[i].close;
const previous_close = data[i - 1].close;
const volume = parseInt(data[i].volume.toString());
if (current_close > previous_close) {
// Add the volume to OBV if the current close price is higher than the previous close price
obv_values.push(obv_values[i - 1] + volume);
}
else if (current_close < previous_close) {
// Subtract the volume from OBV if the current close price is lower than the previous close price
obv_values.push(obv_values[i - 1] - volume);
}
else {
// If the current close price is equal to the previous close price, keep the OBV value unchanged
obv_values.push(obv_values[i - 1]);
}
}
return obv_values;
};
export const compute_alligator = (ohlc_data, jaw_period, teeth_period, lips_period) => {
const jaw_values = [];
const teeth_values = [];
const lips_values = [];
for (let i = jaw_period - 1; i < ohlc_data.length; i++) {
const jaw_start_index = i - jaw_period + 1;
const teeth_start_index = i - teeth_period + 1;
const lips_start_index = i - lips_period + 1;
const jaw_highs = ohlc_data.slice(jaw_start_index, i + 1).map((d) => d.high);
const jaw_lowest_high = Math.min(...jaw_highs);
jaw_values.push(jaw_lowest_high);
const teeth_highs = ohlc_data.slice(teeth_start_index, i + 1).map((d) => d.high);
const teeth_lowest_high = Math.min(...teeth_highs);
teeth_values.push(teeth_lowest_high);
const lips_highs = ohlc_data.slice(lips_start_index, i + 1).map((d) => d.high);
const lips_lowest_high = Math.min(...lips_highs);
lips_values.push(lips_lowest_high);
}
return {
jaw: jaw_values,
teeth: teeth_values,
lips: lips_values,
};
};
export const compute_rsi = (data, period = 7) => {
const gains = [];
const losses = [];
const rsi = [];
// Calculate the price changes and separate gains and losses
for (let i = 1; i < data.length; i++) {
const change = data[i] - data[i - 1];
if (change >= 0) {
gains.push(change);
losses.push(0);
}
else {
gains.push(0);
losses.push(-change);
}
}
// Calculate the average gains and losses for the first period
let avg_gain = stats_mean(gains.slice(0, period));
let avg_loss = stats_mean(losses.slice(0, period));
// Calculate the initial RSI value
let rs = avg_gain / avg_loss;
const initial_rsi = 100 - 100 / (1 + rs);
rsi.push(initial_rsi);
// Calculate RSI for the remaining data
for (let i = period; i < data.length - 1; i++) {
const current_gain = gains[i];
const current_loss = losses[i];
// Smooth the average gains and losses using the previous averages
avg_gain = (avg_gain * (period - 1) + current_gain) / period;
avg_loss = (avg_loss * (period - 1) + current_loss) / period;
rs = avg_gain / avg_loss;
const current_rsi = 100 - 100 / (1 + rs);
rsi.push(current_rsi);
}
return rsi;
};
export const compute_bollinger_bands = (data, period = 20, multiplier = 2) => {
const close_prices = data.map((d) => round(Number(d.close), 2));
const middle = [];
const upper = [];
const lower = [];
function simple_moving_average(prices, period, index) {
const slice = prices.slice(index - period, index);
const sum = slice.reduce((acc, price) => acc + price, 0);
return sum / period;
}
function standard_deviation(prices, period, index, sma) {
const slice = prices.slice(index - period, index);
const variance = slice.reduce((acc, price) => acc + Math.pow(price - sma, 2), 0) / period;
return Math.sqrt(variance);
}
for (let i = period; i <= close_prices.length; i++) {
const sma = simple_moving_average(close_prices, period, i);
const std_dev = standard_deviation(close_prices, period, i, sma);
middle.push(sma);
upper.push(sma + multiplier * std_dev);
lower.push(sma - multiplier * std_dev);
}
return { middle, upper, lower };
};
export const compute_vwap = (data) => {
const vwap_values = [];
let cumulative_volume = 0;
let cumulative_price_volume = 0;
for (let i = 0; i < data.length; i++) {
const typical_price = (data[i].high + data[i].low + data[i].close) / 3;
const volume = parseInt(data[i].volume.toString());
cumulative_volume += volume;
cumulative_price_volume += typical_price * volume;
const vwap = cumulative_price_volume / cumulative_volume;
vwap_values.push(vwap);
}
return vwap_values;
};
export const compute_atr = (data, period = 14) => {
const true_ranges = [];
const atr_values = [];
// Calculate True Range for each candle
for (let i = 1; i < data.length; i++) {
const high = data[i].high;
const low = data[i].low;
const prev_close = data[i - 1].close;
const tr1 = high - low;
const tr2 = Math.abs(high - prev_close);
const tr3 = Math.abs(low - prev_close);
const true_range = Math.max(tr1, tr2, tr3);
true_ranges.push(true_range);
}
// Calculate initial ATR as simple average of first 'period' true ranges
if (true_ranges.length >= period) {
const first_atr = true_ranges.slice(0, period).reduce((sum, tr) => sum + tr, 0) / period;
atr_values.push(first_atr);
// Calculate subsequent ATR values using the smoothing formula
for (let i = period; i < true_ranges.length; i++) {
const current_atr = (atr_values[atr_values.length - 1] * (period - 1) + true_ranges[i]) / period;
atr_values.push(current_atr);
}
}
return atr_values;
};
const calculate_smoothed_average = (data, period) => {
const smoothed_data = [];
const sum = data.slice(0, period).reduce((acc, val) => acc + val, 0);
const average = sum / period;
smoothed_data.push(average);
for (let i = period; i < data.length; i++) {
const smoothed_value = (smoothed_data[i - period] * (period - 1) + data[i]) / period;
smoothed_data.push(smoothed_value);
}
return smoothed_data;
};
export const DEFAULT_FUNCTION_REGISTRY = {
open: {
fn: (candles, context, offset) => {
return round(candles[candles.length - 1 - offset].open, 2);
},
arity: 1,
},
high: {
fn: (candles, context, offset) => {
return round(candles[candles.length - 1 - offset].high, 2);
},
arity: 1,
},
low: {
fn: (candles, context, offset) => {
return round(candles[candles.length - 1 - offset].low, 2);
},
arity: 1,
},
close: {
fn: (candles, context, offset) => {
return round(candles[candles.length - 1 - offset].close, 2);
},
arity: 1,
},
volume: {
fn: (candles, context, offset) => {
return round(candles[candles.length - 1 - offset].volume, 2);
},
arity: 1,
},
ema: {
fn: (candles, context, period, offset) => {
const ema = compute_ema(candles.map((item) => item.close), period);
return round(ema[ema.length - 1 - offset], 2);
},
arity: 2,
},
rsi: {
fn: (candles, context, period, offset) => {
const rsi = compute_rsi(candles.map((item) => item.close), period);
return round(rsi[rsi.length - 1 - offset], 2);
},
arity: 2,
},
macd_macd_line: {
fn: (candles, context, short_period, long_period, signal_period, offset) => {
const macd = compute_macd(candles.map((item) => item.close), short_period, long_period, signal_period);
return round(macd.macd[macd.macd.length - 1 - offset], 2);
},
arity: 4,
},
macd_signal_line: {
fn: (candles, context, short_period, long_period, signal_period, offset) => {
const macd = compute_macd(candles.map((item) => item.close), short_period, long_period, signal_period);
return macd.signal_line[macd.signal_line.length - 1 - offset];
},
arity: 3,
},
obv: {
fn: (candles, context, offset) => {
const obv = compute_obv(candles);
return round(obv[obv.length - 1 - offset], 2);
},
arity: 1,
},
vwap: {
fn: (candles, context, offset) => {
const vwap = compute_vwap(candles);
return round(vwap[vwap.length - 1 - offset], 2);
},
arity: 1,
},
atr: {
fn: (candles, context, period, offset) => {
const atr_values = compute_atr(candles, period);
return round(atr_values[atr_values.length - 1 - offset], 2);
},
arity: 2,
},
min: {
fn: (candles, context, ...args) => Math.min(...args),
arity: null,
},
max: {
fn: (candles, context, ...args) => Math.max(...args),
arity: null,
},
abs: {
fn: (candles, context, value) => Math.abs(value),
arity: 1,
},
avg: {
fn: (candles, context, ...args) => args.reduce((sum, x) => sum + x, 0) / args.length,
arity: null,
},
entry_price: {
fn: (candles, context) => round(context.entry_price, 2),
arity: null,
},
exit_price: {
fn: (candles, context) => round(context.exit_price, 2),
arity: null,
},
risk_reward_ratio: {
fn: (candles, context, ohlc = 3) => {
// ohlc: 0 = OPEN, 1 = high, 2 = low, 3 = close
const candle = candles[candles.length - 1];
let price = candle.close;
switch (ohlc) {
case 0:
price = candle.open;
break;
case 1:
price = candle.high;
break;
case 2:
price = candle.low;
break;
case 3:
default:
price = candle.close;
}
const entry = context.entry_price;
const stop = context.stop_loss;
const target = context.target_price;
const is_long = entry < target;
return is_long ? round((price - entry) / (entry - stop), 2) : round((entry - price) / (stop - entry), 2);
},
arity: null,
},
stop_loss: {
fn: (candles, context) => context.stop_price,
arity: null,
},
};
//# sourceMappingURL=feature_indicators_formulaes.js.map