double short SMAs auto TF
This commit is contained in:
File diff suppressed because one or more lines are too long
98
static/hts.js
Normal file
98
static/hts.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* HTS Indicator Definition
|
||||||
|
* Allows selecting average type and fast interval.
|
||||||
|
*/
|
||||||
|
const HTS_INDICATOR = {
|
||||||
|
name: 'HTS',
|
||||||
|
label: 'HTS',
|
||||||
|
usesBaseData: false,
|
||||||
|
params: [
|
||||||
|
{ name: 'avgType', type: 'select', defaultValue: 'VWMA', options: ['RMA', 'EMA', 'SMA', 'WMA', 'VWMA'], label: 'Average Type' },
|
||||||
|
{ name: 'fast', type: 'number', defaultValue: 33, min: 1, label: 'Fast Interval' }
|
||||||
|
],
|
||||||
|
calculateFull: function(data, params) {
|
||||||
|
// determine data and period based on autoTF
|
||||||
|
let source = data;
|
||||||
|
const period = params.fast;
|
||||||
|
if (params.autoTF) {
|
||||||
|
// current timeframe in seconds between candles
|
||||||
|
const tfSec = (data.length > 1) ? (data[1].time - data[0].time) : 60;
|
||||||
|
const tfMin = tfSec / 60;
|
||||||
|
const autoInterval = Math.max(1, Math.floor(tfMin / 4));
|
||||||
|
// aggregate displayed data at autoInterval
|
||||||
|
source = aggregateCandles(data, autoInterval);
|
||||||
|
} else {
|
||||||
|
source = data;
|
||||||
|
}
|
||||||
|
const type = params.avgType;
|
||||||
|
if (!source || source.length < period) return { max: [], min: [] };
|
||||||
|
const maxSeries = [];
|
||||||
|
const minSeries = [];
|
||||||
|
let sumH, sumL, prevH, prevL, mult;
|
||||||
|
switch (type) {
|
||||||
|
case 'SMA':
|
||||||
|
sumH = 0; sumL = 0;
|
||||||
|
for (let i = 0; i < period; i++) { sumH += source[i].high; sumL += source[i].low; }
|
||||||
|
maxSeries.push({ time: source[period - 1].time, value: sumH / period });
|
||||||
|
minSeries.push({ time: source[period - 1].time, value: sumL / period });
|
||||||
|
for (let i = period; i < source.length; i++) {
|
||||||
|
sumH += source[i].high - source[i - period].high;
|
||||||
|
sumL += source[i].low - source[i - period].low;
|
||||||
|
maxSeries.push({ time: source[i].time, value: sumH / period });
|
||||||
|
minSeries.push({ time: source[i].time, value: sumL / period });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'EMA':
|
||||||
|
case 'VWMA':
|
||||||
|
mult = 2 / (period + 1);
|
||||||
|
// initialize
|
||||||
|
sumH = 0; sumL = 0;
|
||||||
|
for (let i = 0; i < period; i++) { sumH += source[i].high; sumL += source[i].low; }
|
||||||
|
prevH = sumH / period; prevL = sumL / period;
|
||||||
|
maxSeries.push({ time: source[period - 1].time, value: prevH });
|
||||||
|
minSeries.push({ time: source[period - 1].time, value: prevL });
|
||||||
|
for (let i = period; i < source.length; i++) {
|
||||||
|
const h = source[i].high, l = source[i].low;
|
||||||
|
prevH = (h - prevH) * mult + prevH;
|
||||||
|
prevL = (l - prevL) * mult + prevL;
|
||||||
|
maxSeries.push({ time: source[i].time, value: prevH });
|
||||||
|
minSeries.push({ time: source[i].time, value: prevL });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'RMA':
|
||||||
|
// Wilder's smoothing
|
||||||
|
sumH = 0; sumL = 0;
|
||||||
|
for (let i = 0; i < period; i++) { sumH += source[i].high; sumL += source[i].low; }
|
||||||
|
prevH = sumH / period; prevL = sumL / period;
|
||||||
|
maxSeries.push({ time: source[period - 1].time, value: prevH });
|
||||||
|
minSeries.push({ time: source[period - 1].time, value: prevL });
|
||||||
|
for (let i = period; i < source.length; i++) {
|
||||||
|
const h = source[i].high, l = source[i].low;
|
||||||
|
prevH = prevH + (h - prevH) / period;
|
||||||
|
prevL = prevL + (l - prevL) / period;
|
||||||
|
maxSeries.push({ time: source[i].time, value: prevH });
|
||||||
|
minSeries.push({ time: source[i].time, value: prevL });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'WMA':
|
||||||
|
const denom = period * (period + 1) / 2;
|
||||||
|
for (let i = period - 1; i < source.length; i++) {
|
||||||
|
let wSumH = 0, wSumL = 0;
|
||||||
|
for (let j = 0; j < period; j++) {
|
||||||
|
const w = period - j;
|
||||||
|
wSumH += source[i - j].high * w;
|
||||||
|
wSumL += source[i - j].low * w;
|
||||||
|
}
|
||||||
|
maxSeries.push({ time: source[i].time, value: wSumH / denom });
|
||||||
|
minSeries.push({ time: source[i].time, value: wSumL / denom });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
source.forEach(d => {
|
||||||
|
maxSeries.push({ time: d.time, value: d.high });
|
||||||
|
minSeries.push({ time: d.time, value: d.low });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { max: maxSeries, min: minSeries };
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -75,27 +75,72 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
|||||||
if (!definition) return;
|
if (!definition) return;
|
||||||
|
|
||||||
slot.definition = definition;
|
slot.definition = definition;
|
||||||
|
// Special case for HTS: hide avgType/fast controls, show only Auto TF checkbox
|
||||||
|
if (indicatorName === 'HTS') {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.textContent = 'Auto TF';
|
||||||
|
label.style.fontSize = '12px';
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.className = 'input-field';
|
||||||
|
// initialize params
|
||||||
|
// default HTS params
|
||||||
|
slot.params.autoTF = false;
|
||||||
|
slot.params.avgType = definition.params.find(p => p.name === 'avgType').defaultValue;
|
||||||
|
slot.params.fast = definition.params.find(p => p.name === 'fast').defaultValue;
|
||||||
|
checkbox.addEventListener('change', () => {
|
||||||
|
slot.params.autoTF = checkbox.checked;
|
||||||
|
updateIndicator(slot.id, true);
|
||||||
|
});
|
||||||
|
const controlGroup = document.createElement('div');
|
||||||
|
controlGroup.style.display = 'flex';
|
||||||
|
controlGroup.style.flexDirection = 'column';
|
||||||
|
controlGroup.appendChild(label);
|
||||||
|
controlGroup.appendChild(checkbox);
|
||||||
|
controlsContainer.appendChild(controlGroup);
|
||||||
|
// initial draw
|
||||||
|
updateIndicator(slot.id, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Default controls for other indicators
|
||||||
definition.params.forEach(param => {
|
definition.params.forEach(param => {
|
||||||
const label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
label.textContent = param.label || param.name;
|
label.textContent = param.label || param.name;
|
||||||
label.style.fontSize = '12px';
|
label.style.fontSize = '12px';
|
||||||
|
|
||||||
const input = document.createElement('input');
|
// Create select for dropdowns, input for numbers
|
||||||
input.type = param.type;
|
let input;
|
||||||
input.value = param.defaultValue;
|
if (param.type === 'select') {
|
||||||
if (param.min !== undefined) input.min = param.min;
|
input = document.createElement('select');
|
||||||
if (param.step !== undefined) input.step = param.step;
|
input.className = 'input-field';
|
||||||
input.className = 'input-field';
|
// populate options
|
||||||
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
param.options.forEach(opt => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = opt;
|
||||||
|
option.textContent = opt;
|
||||||
|
if (opt === param.defaultValue) option.selected = true;
|
||||||
|
input.appendChild(option);
|
||||||
|
});
|
||||||
|
slot.params[param.name] = input.value;
|
||||||
|
} else {
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = param.type;
|
||||||
|
input.value = param.defaultValue;
|
||||||
|
if (param.min !== undefined) input.min = param.min;
|
||||||
|
if (param.step !== undefined) input.step = param.step;
|
||||||
|
input.className = 'input-field';
|
||||||
|
slot.params[param.name] = parseFloat(input.value);
|
||||||
|
}
|
||||||
|
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
// --- FIX: --- Use the slot's `debounceTimerId` property to manage the timeout.
|
// debounce param changes
|
||||||
clearTimeout(slot.debounceTimerId);
|
clearTimeout(slot.debounceTimerId);
|
||||||
slot.debounceTimerId = setTimeout(() => {
|
slot.debounceTimerId = setTimeout(() => {
|
||||||
slot.params[param.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
|
// update param value
|
||||||
|
const val = input.value;
|
||||||
|
slot.params[param.name] = (param.type === 'number') ? parseFloat(val) : val;
|
||||||
updateIndicator(slot.id, true);
|
updateIndicator(slot.id, true);
|
||||||
slot.debounceTimerId = null; // Clear the ID after the function has run.
|
slot.debounceTimerId = null;
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
const controlGroup = document.createElement('div');
|
const controlGroup = document.createElement('div');
|
||||||
@ -113,7 +158,13 @@ function createIndicatorManager(chart, baseCandleDataRef, displayedCandleDataRef
|
|||||||
const slot = indicatorSlots.find(s => s.id === slotId);
|
const slot = indicatorSlots.find(s => s.id === slotId);
|
||||||
if (!slot || !slot.definition) return;
|
if (!slot || !slot.definition) return;
|
||||||
|
|
||||||
const candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleDataRef : displayedCandleDataRef;
|
// for HTS autoTF, always use base data for aggregation; else follow definition flag
|
||||||
|
let candleDataForCalc;
|
||||||
|
if (slot.definition.name === 'HTS' && slot.params.autoTF) {
|
||||||
|
candleDataForCalc = baseCandleDataRef;
|
||||||
|
} else {
|
||||||
|
candleDataForCalc = (slot.definition.usesBaseData) ? baseCandleDataRef : displayedCandleDataRef;
|
||||||
|
}
|
||||||
if (candleDataForCalc.length === 0) return;
|
if (candleDataForCalc.length === 0) return;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ const AVAILABLE_INDICATORS = [
|
|||||||
SMA_INDICATOR,
|
SMA_INDICATOR,
|
||||||
EMA_INDICATOR,
|
EMA_INDICATOR,
|
||||||
BB_INDICATOR, // Added the new Bollinger Bands indicator
|
BB_INDICATOR, // Added the new Bollinger Bands indicator
|
||||||
HURST_INDICATOR // Added the new Hurst Bands indicator
|
HURST_INDICATOR, // Added the new Hurst Bands indicator
|
||||||
|
HTS_INDICATOR // Added the new HTS indicator
|
||||||
// Add other indicators here as needed
|
// Add other indicators here as needed
|
||||||
];
|
];
|
||||||
@ -12,6 +12,7 @@
|
|||||||
<script src="{{ url_for('static', filename='ema.js') }}"></script>
|
<script src="{{ url_for('static', filename='ema.js') }}"></script>
|
||||||
<script src="{{ url_for('static',filename='bb.js') }}"></script>
|
<script src="{{ url_for('static',filename='bb.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='hurst.js') }}"></script>
|
<script src="{{ url_for('static', filename='hurst.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='hts.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='indicators.js') }}"></script>
|
<script src="{{ url_for('static', filename='indicators.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='indicator-manager.js') }}"></script>
|
<script src="{{ url_for('static', filename='indicator-manager.js') }}"></script>
|
||||||
|
|
||||||
@ -58,6 +59,7 @@
|
|||||||
}
|
}
|
||||||
.action-button:hover, .control-cell select:hover { background-color: var(--button-hover-bg); }
|
.action-button:hover, .control-cell select:hover { background-color: var(--button-hover-bg); }
|
||||||
.input-field { width: 60px; }
|
.input-field { width: 60px; }
|
||||||
|
select.input-field { width: 100px; }
|
||||||
#candle-timer { font-size: 2rem; font-weight: 500; color: var(--accent-orange); }
|
#candle-timer { font-size: 2rem; font-weight: 500; color: var(--accent-orange); }
|
||||||
#timeframe-display { margin-top: 10px; min-width: 60px; }
|
#timeframe-display { margin-top: 10px; min-width: 60px; }
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
|
|||||||
Reference in New Issue
Block a user