Files
winterfail/js/ui/hts-visualizer.js

243 lines
8.6 KiB
JavaScript

const HTS_COLORS = {
fastHigh: '#00bcd4',
fastLow: '#00bcd4',
slowHigh: '#f44336',
slowLow: '#f44336',
bullishZone: 'rgba(38, 166, 154, 0.1)',
bearishZone: 'rgba(239, 83, 80, 0.1)',
channelRegion: 'rgba(41, 98, 255, 0.05)'
};
let HTSOverlays = [];
export class HTSVisualizer {
constructor(chart, candles) {
this.chart = chart;
this.candles = candles;
this.overlays = [];
}
clear() {
this.overlays.forEach(overlay => {
try {
this.chart.removeSeries(overlay.series);
} catch (e) { }
});
this.overlays = [];
}
addHTSChannels(htsData, isAutoHTS = false) {
this.clear();
if (!htsData || htsData.length === 0) return;
const alpha = isAutoHTS ? 0.3 : 0.3;
const lineWidth = isAutoHTS ? 1 : 2;
const fastHighSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
color: `rgba(0, 188, 212, ${alpha})`,
lineWidth: lineWidth,
lastValueVisible: false,
title: 'HTS Fast High' + (isAutoHTS ? ' (Auto)' : ''),
priceLineVisible: false,
crosshairMarkerVisible: false
});
const fastLowSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
color: `rgba(0, 188, 212, ${alpha})`,
lineWidth: lineWidth,
lastValueVisible: false,
title: 'HTS Fast Low' + (isAutoHTS ? ' (Auto)' : ''),
priceLineVisible: false,
crosshairMarkerVisible: false
});
const slowHighSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
color: `rgba(244, 67, 54, ${alpha})`,
lineWidth: lineWidth + 1,
lastValueVisible: false,
title: 'HTS Slow High' + (isAutoHTS ? ' (Auto)' : ''),
priceLineVisible: false,
crosshairMarkerVisible: false
});
const slowLowSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
color: `rgba(244, 67, 54, ${alpha})`,
lineWidth: lineWidth + 1,
lastValueVisible: false,
title: 'HTS Slow Low' + (isAutoHTS ? ' (Auto)' : ''),
priceLineVisible: false,
crosshairMarkerVisible: false
});
const fastHighData = htsData.map(h => ({ time: h.time, value: h.fastHigh }));
const fastLowData = htsData.map(h => ({ time: h.time, value: h.fastLow }));
const slowHighData = htsData.map(h => ({ time: h.time, value: h.slowHigh }));
const slowLowData = htsData.map(h => ({ time: h.time, value: h.slowLow }));
fastHighSeries.setData(fastHighData);
fastLowSeries.setData(fastLowData);
slowHighSeries.setData(slowHighData);
slowLowSeries.setData(slowLowData);
this.overlays.push(
{ series: fastHighSeries, name: 'fastHigh' },
{ series: fastLowSeries, name: 'fastLow' },
{ series: slowHighSeries, name: 'slowHigh' },
{ series: slowLowSeries, name: 'slowLow' }
);
return {
fastHigh: fastHighSeries,
fastLow: fastLowSeries,
slowHigh: slowHighSeries,
slowLow: slowLowSeries
};
}
addTrendZones(htsData) {
if (!htsData || htsData.length < 2) return;
const trendZones = [];
let currentZone = null;
for (let i = 1; i < htsData.length; i++) {
const prev = htsData[i - 1];
const curr = htsData[i];
const prevBullish = prev.fastLow > prev.slowLow && prev.fastHigh > prev.slowHigh;
const currBullish = curr.fastLow > curr.slowLow && curr.fastHigh > curr.slowHigh;
const prevBearish = prev.fastLow < prev.slowLow && prev.fastHigh < prev.slowHigh;
const currBearish = curr.fastLow < curr.slowLow && curr.fastHigh < curr.slowHigh;
if (currBullish && !prevBullish) {
currentZone = { type: 'bullish', start: curr.time };
} else if (currBearish && !prevBearish) {
currentZone = { type: 'bearish', start: curr.time };
} else if (!currBullish && !currBearish && currentZone) {
currentZone.end = prev.time;
trendZones.push({ ...currentZone });
currentZone = null;
}
}
if (currentZone) {
currentZone.end = htsData[htsData.length - 1].time;
trendZones.push(currentZone);
}
trendZones.forEach(zone => {
const zoneSeries = this.chart.addSeries(LightweightCharts.AreaSeries, {
topColor: zone.type === 'bullish' ? 'rgba(38, 166, 154, 0.02)' : 'rgba(239, 83, 80, 0.02)',
bottomColor: zone.type === 'bullish' ? 'rgba(38, 166, 154, 0.02)' : 'rgba(239, 83, 80, 0.02)',
lineColor: 'transparent',
lastValueVisible: false,
priceLineVisible: false,
});
if (this.candles && this.candles.length > 0) {
const maxPrice = Math.max(...this.candles.map(c => c.high)) * 2;
const minPrice = Math.min(...this.candles.map(c => c.low)) * 0.5;
const startTime = zone.start || (this.candles[0]?.time);
const endTime = zone.end || (this.candles[this.candles.length - 1]?.time);
zoneSeries.setData([
{ time: startTime, value: minPrice },
{ time: startTime, value: maxPrice },
{ time: endTime, value: maxPrice },
{ time: endTime, value: minPrice }
]);
}
this.overlays.push({ series: zoneSeries, name: `trendZone_${zone.type}_${zone.start}` });
});
}
addCrossoverMarkers(htsData) {
if (!htsData || htsData.length < 2) return;
const markers = [];
for (let i = 1; i < htsData.length; i++) {
const prev = htsData[i - 1];
const curr = htsData[i];
if (!prev || !curr) continue;
const price = curr.price;
const prevFastLow = prev.fastLow;
const currFastLow = curr.fastLow;
const prevFastHigh = prev.fastHigh;
const currFastHigh = curr.fastHigh;
const prevSlowLow = prev.slowLow;
const currSlowLow = curr.slowLow;
const prevSlowHigh = prev.slowHigh;
const currSlowHigh = curr.slowHigh;
if (prevFastLow <= prevSlowLow && currFastLow > currSlowLow && price > currSlowLow) {
markers.push({
time: curr.time,
position: 'belowBar',
color: '#26a69a',
shape: 'arrowUp',
text: 'BUY',
size: 1.2
});
}
if (prevFastHigh >= prevSlowHigh && currFastHigh < currSlowHigh && price < currSlowHigh) {
markers.push({
time: curr.time,
position: 'aboveBar',
color: '#ef5350',
shape: 'arrowDown',
text: 'SELL',
size: 1.2
});
}
}
const candleSeries = this.candleData?.series;
if (candleSeries) {
try {
if (typeof candleSeries.setMarkers === 'function') {
candleSeries.setMarkers(markers);
} else if (typeof SeriesMarkersPrimitive !== 'undefined') {
if (!this.markerPrimitive) {
this.markerPrimitive = new SeriesMarkersPrimitive();
candleSeries.attachPrimitive(this.markerPrimitive);
}
this.markerPrimitive.setMarkers(markers);
}
} catch (e) {
console.warn('[HTS] Error setting markers:', e);
}
}
return markers;
}
}
export function addHTSVisualization(chart, candleSeries, htsData, candles, isAutoHTS = false) {
const visualizer = new HTSVisualizer(chart, candles);
visualizer.candleData = { series: candleSeries };
visualizer.addHTSChannels(htsData, isAutoHTS);
// Disable trend zones to avoid visual clutter
// visualizer.addTrendZones(htsData);
if (window.showCrossoverMarkers !== false) {
setTimeout(() => {
try {
visualizer.addCrossoverMarkers(htsData);
} catch (e) {
console.warn('Crossover markers skipped (API limitation):', e.message);
}
}, 100);
}
return visualizer;
}