chore: add AGENTS.md with build, lint, test commands and style guidelines
This commit is contained in:
255
js/indicators/hts.js
Normal file
255
js/indicators/hts.js
Normal file
@ -0,0 +1,255 @@
|
||||
// Self-contained HTS Trend System indicator
|
||||
// Includes math, metadata, signal calculation, and base class
|
||||
|
||||
// Signal constants (defined in each indicator file)
|
||||
const SIGNAL_TYPES = {
|
||||
BUY: 'buy',
|
||||
SELL: 'sell',
|
||||
HOLD: 'hold'
|
||||
};
|
||||
|
||||
const SIGNAL_COLORS = {
|
||||
buy: '#26a69a',
|
||||
hold: '#787b86',
|
||||
sell: '#ef5350'
|
||||
};
|
||||
|
||||
// Base class (inline replacement for BaseIndicator)
|
||||
class BaseIndicator {
|
||||
constructor(config) {
|
||||
this.id = config.id;
|
||||
this.type = config.type;
|
||||
this.name = config.name;
|
||||
this.params = config.params || {};
|
||||
this.timeframe = config.timeframe || '1m';
|
||||
this.series = [];
|
||||
this.visible = config.visible !== false;
|
||||
this.cachedResults = null;
|
||||
this.cachedMeta = null;
|
||||
this.lastSignalTimestamp = null;
|
||||
this.lastSignalType = null;
|
||||
}
|
||||
}
|
||||
|
||||
// MA calculations inline (SMA/EMA/RMA/WMA/VWMA)
|
||||
function calculateSMA(candles, period, source = 'close') {
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
sum += candles[i][source];
|
||||
if (i >= period) sum -= candles[i - period][source];
|
||||
if (i >= period - 1) results[i] = sum / period;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function calculateEMA(candles, period, source = 'close') {
|
||||
const multiplier = 2 / (period + 1);
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let ema = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
if (i < period) {
|
||||
sum += candles[i][source];
|
||||
if (i === period - 1) {
|
||||
ema = sum / period;
|
||||
results[i] = ema;
|
||||
}
|
||||
} else {
|
||||
ema = (candles[i][source] - ema) * multiplier + ema;
|
||||
results[i] = ema;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function calculateRMA(candles, period, source = 'close') {
|
||||
const multiplier = 1 / period;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let rma = 0;
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
if (i < period) {
|
||||
sum += candles[i][source];
|
||||
if (i === period - 1) {
|
||||
rma = sum / period;
|
||||
results[i] = rma;
|
||||
}
|
||||
} else {
|
||||
rma = (candles[i][source] - rma) * multiplier + rma;
|
||||
results[i] = rma;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function calculateWMA(candles, period, source = 'close') {
|
||||
const results = new Array(candles.length).fill(null);
|
||||
const weightSum = (period * (period + 1)) / 2;
|
||||
|
||||
for (let i = period - 1; i < candles.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sum += candles[i - j][source] * (period - j);
|
||||
}
|
||||
results[i] = sum / weightSum;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function calculateVWMA(candles, period, source = 'close') {
|
||||
const results = new Array(candles.length).fill(null);
|
||||
|
||||
for (let i = period - 1; i < candles.length; i++) {
|
||||
let sumPV = 0;
|
||||
let sumV = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sumPV += candles[i - j][source] * candles[i - j].volume;
|
||||
sumV += candles[i - j].volume;
|
||||
}
|
||||
results[i] = sumV !== 0 ? sumPV / sumV : null;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// MA dispatcher function
|
||||
function getMA(type, candles, period, source = 'close') {
|
||||
switch (type.toUpperCase()) {
|
||||
case 'SMA': return calculateSMA(candles, period, source);
|
||||
case 'EMA': return calculateEMA(candles, period, source);
|
||||
case 'RMA': return calculateRMA(candles, period, source);
|
||||
case 'WMA': return calculateWMA(candles, period, source);
|
||||
case 'VWMA': return calculateVWMA(candles, period, source);
|
||||
default: return calculateSMA(candles, period, source);
|
||||
}
|
||||
}
|
||||
|
||||
// Signal calculation for HTS
|
||||
function calculateHTSSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const slowLow = values?.slowLow;
|
||||
const slowHigh = values?.slowHigh;
|
||||
const prevSlowLow = prevValues?.slowLow;
|
||||
const prevSlowHigh = prevValues?.slowHigh;
|
||||
|
||||
if (!slowLow || !slowHigh || !prevSlowLow || !prevSlowHigh) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const close = lastCandle.close;
|
||||
const prevClose = prevCandle?.close;
|
||||
|
||||
if (prevClose === undefined) return null;
|
||||
|
||||
// BUY: Price crosses UP through slow low
|
||||
if (prevClose <= prevSlowLow && close > slowLow) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: 85,
|
||||
value: close,
|
||||
reasoning: `Price crossed UP through slow low`
|
||||
};
|
||||
}
|
||||
// SELL: Price crosses DOWN through slow high
|
||||
else if (prevClose >= prevSlowHigh && close < slowHigh) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: 85,
|
||||
value: close,
|
||||
reasoning: `Price crossed DOWN through slow high`
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// HTS Indicator class
|
||||
export class HTSIndicator extends BaseIndicator {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this.lastSignalTimestamp = null;
|
||||
this.lastSignalType = null;
|
||||
}
|
||||
|
||||
calculate(candles, oneMinCandles = null, targetTF = null) {
|
||||
const shortPeriod = this.params.short || 33;
|
||||
const longPeriod = this.params.long || 144;
|
||||
const maType = this.params.maType || 'RMA';
|
||||
const useAutoHTS = this.params.useAutoHTS || false;
|
||||
|
||||
let workingCandles = candles;
|
||||
|
||||
if (useAutoHTS && oneMinCandles && targetTF) {
|
||||
const tfMultipliers = {
|
||||
'5m': 5,
|
||||
'15m': 15,
|
||||
'30m': 30,
|
||||
'37m': 37,
|
||||
'1h': 60,
|
||||
'4h': 240
|
||||
};
|
||||
|
||||
const tfGroup = tfMultipliers[targetTF] || 5;
|
||||
|
||||
const grouped = [];
|
||||
let currentGroup = [];
|
||||
for (let i = 0; i < oneMinCandles.length; i++) {
|
||||
currentGroup.push(oneMinCandles[i]);
|
||||
if (currentGroup.length >= tfGroup) {
|
||||
grouped.push({
|
||||
time: currentGroup[tfGroup - 1].time,
|
||||
open: currentGroup[tfGroup - 1].open,
|
||||
high: currentGroup[tfGroup - 1].high,
|
||||
low: currentGroup[tfGroup - 1].low,
|
||||
close: currentGroup[tfGroup - 1].close,
|
||||
volume: currentGroup[tfGroup - 1].volume
|
||||
});
|
||||
currentGroup = [];
|
||||
}
|
||||
}
|
||||
|
||||
workingCandles = grouped;
|
||||
}
|
||||
|
||||
const shortHigh = getMA(maType, workingCandles, shortPeriod, 'high');
|
||||
const shortLow = getMA(maType, workingCandles, shortPeriod, 'low');
|
||||
const longHigh = getMA(maType, workingCandles, longPeriod, 'high');
|
||||
const longLow = getMA(maType, workingCandles, longPeriod, 'low');
|
||||
|
||||
return workingCandles.map((_, i) => ({
|
||||
fastHigh: shortHigh[i],
|
||||
fastLow: shortLow[i],
|
||||
slowHigh: longHigh[i],
|
||||
slowLow: longLow[i],
|
||||
fastMidpoint: ((shortHigh[i] || 0) + (shortLow[i] || 0)) / 2,
|
||||
slowMidpoint: ((longHigh[i] || 0) + (longLow[i] || 0)) / 2
|
||||
}));
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
const useAutoHTS = this.params?.useAutoHTS || false;
|
||||
|
||||
const fastLineWidth = useAutoHTS ? 1 : 1;
|
||||
const slowLineWidth = useAutoHTS ? 2 : 2;
|
||||
|
||||
return {
|
||||
name: 'HTS Trend System',
|
||||
description: 'High/Low Trend System with Fast and Slow MAs',
|
||||
inputs: [
|
||||
{ name: 'short', label: 'Fast Period', type: 'number', default: 33, min: 1, max: 500 },
|
||||
{ name: 'long', label: 'Slow Period', type: 'number', default: 144, min: 1, max: 500 },
|
||||
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'RMA' },
|
||||
{ name: 'useAutoHTS', label: 'Auto HTS (TF/4)', type: 'boolean', default: false }
|
||||
],
|
||||
plots: [
|
||||
{ id: 'fastHigh', color: '#00bcd4', title: 'Fast High', width: fastLineWidth },
|
||||
{ id: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: fastLineWidth },
|
||||
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: slowLineWidth },
|
||||
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', width: slowLineWidth }
|
||||
],
|
||||
displayMode: 'overlay'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { calculateHTSSignal };
|
||||
Reference in New Issue
Block a user