feat: Implement HTS (Higher Timeframe Trend System) strategy

- Add HTS strategy engine with crossover and alignment-based entries
- Implement Auto HTS feature (computes on TF/4 from 1m data)
- Add 1H Red Zone filter to validate long signals
- Add channel-based stop loss with RTL functionality
- Enhanced visualization with 30% opacity channel lines
- Fixed data alignment in simulation (uses htsData instead of mismatched indices)
- Fixed syntax errors in hts-engine.js (malformed template literals)
- Fixed duplicate code in simulation.js
- Added showSimulationMarkers to window object for global access
- Enhanced logging for trade signals and simulation results
- Fix missing prevFastHigh/currFastHigh in hts-visualizer.js
- Disable trend zone overlays to prevent chart clutter
- Implement client-side visualization for trade markers using line series
- MA strategy indicator configuration fixed (was empty during engine run)
- Made entry conditions more permissive for shorter timeframes
- Added comprehensive error handling and console logging
This commit is contained in:
DiTus
2026-02-27 09:30:43 +01:00
parent 286975b01a
commit e457ce3e20
9 changed files with 1080 additions and 100 deletions

View File

@ -0,0 +1,233 @@
import { HTSStrategyEngine } from '../strategies/hts-engine.js';
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 && typeof candleSeries.setMarkers === 'function') {
candleSeries.setMarkers(markers);
}
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;
}

View File

@ -1,4 +1,6 @@
import { ClientStrategyEngine } from '../strategies/index.js';
import { HTSStrategyEngine } from '../strategies/hts-engine.js';
import { addHTSVisualization } from './hts-visualizer.js';
import { SimulationStorage } from './storage.js';
import { downloadFile } from '../utils/index.js';
import { showExportDialog, closeExportDialog, performExport } from './export.js';
@ -94,40 +96,92 @@ export async function runSimulation() {
timeframes: { primary: interval, secondary: secondaryTF ? [secondaryTF] : [] },
indicators: []
};
console.log('Building strategy config:');
console.log(' Primary TF:', interval);
console.log(' Secondary TF:', secondaryTF);
console.log(' Available candles:', Object.keys(candlesMap));
if (strategyConfig.id === 'ma_trend') {
const period = strategyConfig.params?.period || 44;
engineConfig.indicators.push({
name: `ma${period}`,
type: 'sma',
params: { period: period },
timeframe: interval
});
if (secondaryTF) {
let results;
if (strategyConfig.id === 'hts_trend') {
console.log('Running HTS strategy simulation...');
const htsEngine = new HTSStrategyEngine(strategyConfig.params);
let oneMinCandles = null;
let h1hCandles = null;
if (strategyConfig.params?.useAutoHTS) {
console.log('Fetching 1m candles for Auto HTS...');
const oneMinQuery = `symbol=BTC&interval=1m&start=${fetchStart.toISOString()}&limit=10000`;
const oneMinResponse = await fetch(`/api/v1/candles?${oneMinQuery}`);
if (oneMinResponse.ok) {
const oneMinData = await oneMinResponse.json();
if (oneMinData.candles && oneMinData.candles.length > 0) {
oneMinCandles = oneMinData.candles.map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close),
volume: parseFloat(c.volume)
}));
console.log(`Got ${oneMinCandles.length} 1m candles`);
}
}
}
if (strategyConfig.params?.use1HFilter) {
console.log('Fetching 1H candles for HTS 1H filter...');
const h1hQuery = `symbol=BTC&interval=1h&start=${fetchStart.toISOString()}&limit=500`;
const h1hResponse = await fetch(`/api/v1/candles?${h1hQuery}`);
if (h1hResponse.ok) {
const h1hData = await h1hResponse.json();
if (h1hData.candles && h1hData.candles.length > 0) {
h1hCandles = h1hData.candles.map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close),
volume: parseFloat(c.volume)
}));
console.log(`Got ${h1hCandles.length} 1H candles`);
}
}
}
results = htsEngine.runSimulation(candlesMap[interval], oneMinCandles, h1hCandles);
} else {
if (strategyConfig.id === 'ma_trend') {
const period = strategyConfig.params?.period || 44;
engineConfig.indicators.push({
name: `ma${period}_${secondaryTF}`,
name: `ma${period}`,
type: 'sma',
params: { period: period },
timeframe: secondaryTF
timeframe: interval
});
if (secondaryTF) {
engineConfig.indicators.push({
name: `ma${period}_${secondaryTF}`,
type: 'sma',
params: { period: period },
timeframe: secondaryTF
});
}
}
console.log(' Indicators configured:', engineConfig.indicators.map(i => `${i.name} on ${i.timeframe}`));
const riskConfig = {
positionSizing: { method: 'percent', value: riskPercent },
stopLoss: { enabled: true, method: 'percent', value: stopLossPercent }
};
const engine = new ClientStrategyEngine();
results = engine.run(candlesMap, engineConfig, riskConfig, start);
}
console.log(' Indicators configured:', engineConfig.indicators.map(i => `${i.name} on ${i.timeframe}`));
const riskConfig = {
positionSizing: { method: 'percent', value: riskPercent },
stopLoss: { enabled: true, method: 'percent', value: stopLossPercent }
};
const engine = new ClientStrategyEngine();
const results = engine.run(candlesMap, engineConfig, riskConfig, start);
if (results.error) throw new Error(results.error);
setLastResults({
@ -163,7 +217,24 @@ export async function runSimulation() {
}
showSimulationMarkers();
if (strategyConfig.id === 'hts_trend' && results.htsData && window.dashboard) {
try {
console.log('Visualizing HTS channels...');
const candles = window.dashboard.allData.get(interval) || candlesMap[interval];
htsVisualizer = addHTSVisualization(
window.dashboard.chart,
window.dashboard.candleSeries,
results.htsData,
candles,
strategyConfig.params?.useAutoHTS
);
console.log('HTS channels visualized');
} catch (e) {
console.error('Error visualizing HTS channels:', e);
}
}
} catch (error) {
console.error('Simulation error:', error);
alert('Simulation error: ' + error.message);
@ -244,75 +315,158 @@ function drawEquitySparkline(results) {
}
let tradeLineSeries = [];
let htsVisualizer = null;
let tradeMarkerSeries = [];
export function showSimulationMarkers() {
const results = getLastResults();
if (!results || !window.dashboard) return;
if (!results || !window.dashboard) {
console.warn('Cannot show markers: no results or dashboard');
return;
}
const trades = results.trades || results.results?.trades || [];
const markers = [];
clearSimulationMarkers();
console.log('Plotting trades:', trades.length);
trades.forEach((trade, i) => {
console.log('Dashboard check:', {
dashboard: !!window.dashboard,
chart: !!window.dashboard?.chart,
candleSeries: !!window.dashboard?.candleSeries,
setMarkers: typeof window.dashboard?.candleSeries?.setMarkers
});
trades.forEach((trade, i) => {
let entryTime, exitTime;
if (typeof trade.entryTime === 'number') {
entryTime = trade.entryTime;
} else {
entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000);
}
if (typeof trade.exitTime === 'number') {
exitTime = trade.exitTime;
} else {
exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000);
}
const pnlSymbol = trade.pnl > 0 ? '+' : '';
markers.push({
time: entryTime,
position: 'belowBar',
color: '#2196f3',
shape: 'arrowUp',
text: 'BUY',
size: 1
});
markers.push({
time: exitTime,
position: 'aboveBar',
color: trade.pnl > 0 ? '#4caf50' : '#f44336',
shape: 'arrowDown',
text: `SELL ${pnlSymbol}${trade.pnlPct.toFixed(1)}%`,
size: 1
});
const lineSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: '#2196f3',
lineWidth: 1,
lastValueVisible: false,
title: '',
priceLineVisible: false,
crosshairMarkerVisible: false
}, 0);
lineSeries.setData([
{ time: entryTime, value: trade.entryPrice },
{ time: exitTime, value: trade.exitPrice }
]);
tradeLineSeries.push(lineSeries);
console.log(`[Trade ${i}] Entry: ${new Date(entryTime * 1000).toISOString()}, Exit: ${new Date(exitTime * 1000).toISOString()}`);
console.log(` Entry Price: $${trade.entryPrice.toFixed(2)} | Exit Price: $${trade.exitPrice.toFixed(2)} | PnL: ${trade.pnl > 0 ? '+' : ''}$${trade.pnl.toFixed(2)} (${trade.pnlPct.toFixed(1)}%)`);
if (window.dashboard && window.dashboard.chart && window.dashboard.candleSeries) {
try {
const data = window.dashboard.candleSeries.data();
if (!data || data.length === 0) {
console.warn('No chart data available for markers');
return;
}
const chartTimeRange = {
min: Math.min(...data.map(c => c.time)),
max: Math.max(...data.map(c => c.time))
};
if (entryTime < chartTimeRange.min || entryTime > chartTimeRange.max) {
console.log(`Skipping trade ${i} - entry time outside chart range`);
return;
}
if (exitTime < chartTimeRange.min || exitTime > chartTimeRange.max) {
console.log(`Skipping trade ${i} - exit time outside chart range`);
return;
}
const buyMarkerSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: '#2196f3',
lineWidth: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false
});
const entryPrice = trade.entryPrice;
buyMarkerSeries.setData([
{ time: entryTime, value: entryPrice },
{ time: entryTime + 1, value: entryPrice }
]);
tradeMarkerSeries.push({ series: buyMarkerSeries });
const pnlColor = trade.pnl > 0 ? '#4caf50' : '#f44336';
const sellMarkerSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: pnlColor,
lineWidth: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false
});
const exitPrice = trade.exitPrice;
sellMarkerSeries.setData([
{ time: exitTime, value: exitPrice },
{ time: exitTime + 1, value: exitPrice }
]);
tradeMarkerSeries.push({ series: sellMarkerSeries });
const lineSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: 'rgba(33, 150, 243, 0.3)',
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false
});
lineSeries.setData([
{ time: entryTime, value: trade.entryPrice },
{ time: exitTime, value: trade.exitPrice }
]);
tradeLineSeries.push(lineSeries);
} catch (e) {
console.error(`Error adding marker for trade ${i}:`, e.message);
}
}
});
markers.sort((a, b) => a.time - b.time);
window.dashboard.candleSeries.setMarkers(markers);
const candleSeries = window.dashboard.candleSeries;
if (candleSeries && typeof candleSeries.setMarkers === 'function') {
try {
console.log('Setting', markers.length, 'markers on candle series');
const currentData = candleSeries.data();
const markerTimes = new Set(markers.map(m => m.time));
const candleTimes = new Set(currentData.map(c => c.time));
const orphanMarkers = markers.filter(m => !candleTimes.has(m.time));
if (orphanMarkers.length > 0) {
console.warn(`${orphanMarkers.length} markers have timestamps not found in chart data:`);
orphanMarkers.forEach(m => {
console.warn(' - Time:', m.time, '(', new Date(m.time * 1000).toISOString(), ')');
});
console.log('First few candle times:', Array.from(candleTimes).slice(0, 5));
console.log('Last few candle times:', Array.from(candleTimes).slice(-5));
}
candleSeries.setMarkers(markers);
console.log('Markers set successfully');
} catch (e) {
console.error('Error setting markers:', e);
console.error('Marker methods available:', Object.getOwnPropertyNames(Object.getPrototypeOf(candleSeries)).filter(m => m.includes('marker')));
}
} else {
console.warn('Markers not supported on this chart version - trades will be logged but not visualized on chart');
}
console.log(`Plotted ${trades.length} trades with connection lines`);
console.log(`Trade markers created for ${trades.length} positions`);
}
export function clearSimulationMarkers() {
@ -323,7 +477,7 @@ export function clearSimulationMarkers() {
} catch (e) {
// Ignore errors clearing markers
}
try {
tradeLineSeries.forEach(series => {
try {
@ -337,8 +491,33 @@ export function clearSimulationMarkers() {
} catch (e) {
// Ignore errors removing series
}
tradeLineSeries = [];
try {
if (htsVisualizer) {
htsVisualizer.clear();
htsVisualizer = null;
}
} catch (e) {
// Ignore errors clearing HTS visualizer
}
// Clear trade marker series
try {
tradeMarkerSeries.forEach(marker => {
try {
if (window.dashboard && window.dashboard.chart && marker.series) {
window.dashboard.chart.removeSeries(marker.series);
}
} catch (e) {
// Ignore errors
}
});
tradeMarkerSeries = [];
} catch (e) {
// Ignore errors
}
}
export function clearSimulationResults() {

View File

@ -72,26 +72,35 @@ export function renderStrategyParams(strategyId) {
export async function loadStrategies() {
try {
console.log('Fetching strategies from API...');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch('/api/v1/strategies?_=' + Date.now(), {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
let data = await response.json();
console.log('Strategies loaded:', data);
if (!data.strategies) {
throw new Error('Invalid response format: missing strategies array');
}
data.strategies = data.strategies.filter(s => s.id !== 'hts_trend');
data.strategies.push({
id: 'hts_trend',
name: 'HTS Trend Strategy',
description: 'Higher Timeframe Trend System with Auto HTS, 1H filters, and channel-based entries/exits',
required_indicators: []
});
window.availableStrategies = data.strategies;
renderStrategies(data.strategies);
} catch (error) {