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

79
HTS_STRATEGY.md Normal file
View File

@ -0,0 +1,79 @@
# HTS (Higher Timeframe Trend System) Strategy
A trend-following strategy based on channel breakouts using fast and slow moving averages of High/Low prices.
## Strategy Rules
### 1. Core Trend Signal
- **Bullish Trend**: Price trading above the Red (Slow) Channel and Aqua (Fast) Channel is above Red Channel
- **Bearish Trend**: Price trading below the Red (Slow) Channel and Aqua (Fast) Channel is below Red Channel
### 2. Entry Rules
- **Long Entry**: Wait for price to break above Slow Red Channel. Candle close above shorth (Fast Low line) while fast lines are above slow lines.
- **Short Entry**: Wait for price to break below Slow Red Channel. Look for close below shortl (Fast Low line) while fast lines are below slow lines.
### 3. 1H Red Zone Filter
- Only take Longs if the price is above the 1H Red Zone (Slow Channel), regardless of fast line direction
- Can be disabled in configuration
### 4. Stop Loss & Trailing Stop
- **Stop Loss**: Place on opposite side of Red (Slow) Channel
- Long stop: longl (Slow Low) line
- Short stop: slowh (Slow High) line
- **Trailing Stop**: As Red Channel moves, move stop loss accordingly
### 5. RMA Default
- Uses RMA (Running Moving Average) by default - slower and smoother than EMA
- Designed for long-term trends, late to react to sudden crashes (feature, not bug)
## Configuration Parameters
| Parameter | Default | Range | Description |
|-----------|---------|-------|-------------|
| `shortPeriod` | 33 | 5-200 | Fast period for HTS |
| `longPeriod` | 144 | 10-500 | Slow period for HTS |
| `maType` | RMA | - | Moving average type (RMA/SMA/EMA/WMA/VWMA) |
| `useAutoHTS` | false | - | Compute HTS on timeframe/4 from 1m data |
| `use1HFilter` | true | - | Enable 1H Red Zone filter |
## Usage
1. Select "HTS Trend Strategy" from the strategies dropdown
2. Configure parameters:
- Periods: typically 33/144 for 15min-1hour charts
- Enable Auto HTS for multi-timeframe analysis
- Enable/disable 1H filter as needed
3. Run simulation to see backtesting results
4. View entry/exit markers on the chart
## Visualization
- **Cyan Lines**: Fast channels (33-period)
- **Red Lines**: Slow channels (144-period)
- **Green Arrows**: Buy signals (fast low crossover)
- **Red Arrows**: Sell signals (fast high crossover)
- **Background Shading**: Trend zones (green=bullish, red=bearish)
## Signal Strength
Pure HTS signals don't mix with other indicators. Signals are based solely on:
- Crossover detection
- Channel alignment
- Price position relative to channels
- Higher timeframe confirmation (1H filter if enabled)
## Example Setup
For a 15-minute chart:
- Fast Period: 33
- Slow Period: 144
- MA Type: RMA (default)
- Auto HTS: Disabled (or enable to see HTS on ~4-minute perspective)
- 1H Filter: Enabled (for better trade filtering)
## Notes
- This strategy is designed for trend-following, not ranging markets
- RMA is slower than EMA, giving smoother signals but later entries
- 1H filter significantly reduces false signals for long trades
- Works best in volatile but trending assets like BTC

View File

@ -1394,7 +1394,7 @@
<button class="sidebar-tab active" data-tab="indicators">📊 Indicators</button>
<button class="sidebar-tab" data-tab="strategies">📋 Strategies</button>
</div>
<button class="sidebar-toggle" onclick="toggleSidebar()"></button>
<button class="sidebar-toggle" id="sidebarToggleBtn"></button>
</div>
<div class="sidebar-content">

View File

@ -58,6 +58,7 @@ window.performExport = performExport;
window.exportSavedSimulation = exportSavedSimulation;
window.runSimulation = runSimulation;
window.saveSimulation = saveSimulation;
window.showSimulationMarkers = showSimulationMarkers;
window.renderSavedSimulations = renderSavedSimulations;
window.loadSavedSimulation = loadSavedSimulation;
window.deleteSavedSimulation = deleteSavedSimulation;
@ -77,6 +78,12 @@ window.SimulationStorage = SimulationStorage;
window.IndicatorRegistry = IndicatorRegistry;
document.addEventListener('DOMContentLoaded', async () => {
// Attach toggle sidebar event listener
const toggleBtn = document.getElementById('sidebarToggleBtn');
if (toggleBtn) {
toggleBtn.addEventListener('click', toggleSidebar);
}
window.dashboard = new TradingDashboard();
restoreSidebarState();
restoreSidebarTabState();
@ -84,9 +91,9 @@ document.addEventListener('DOMContentLoaded', async () => {
setDefaultStartDate();
updateTimeframeDisplay();
renderSavedSimulations();
await loadStrategies();
// Initialize indicator panel
window.initIndicatorPanel();
});

View File

@ -2,38 +2,81 @@ import { MA } from './ma.js';
import { BaseIndicator } from './base.js';
export class HTSIndicator extends BaseIndicator {
calculate(candles) {
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 shortHigh = MA.get(maType, candles, shortPeriod, 'high');
const shortLow = MA.get(maType, candles, shortPeriod, 'low');
const longHigh = MA.get(maType, candles, longPeriod, 'high');
const longLow = MA.get(maType, candles, longPeriod, 'low');
return candles.map((_, i) => ({
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 = MA.get(maType, workingCandles, shortPeriod, 'high');
const shortLow = MA.get(maType, workingCandles, shortPeriod, 'low');
const longHigh = MA.get(maType, workingCandles, longPeriod, 'high');
const longLow = MA.get(maType, workingCandles, longPeriod, 'low');
return workingCandles.map((_, i) => ({
fastHigh: shortHigh[i],
fastLow: shortLow[i],
slowHigh: longHigh[i],
slowLow: longLow[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: '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: 1 },
{ id: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: 1 },
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: 2 },
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', width: 2 }
{ 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'
};

View File

@ -1,4 +1,11 @@
export const StrategyParams = {
hts_trend: [
{ name: 'shortPeriod', label: 'Fast Period', type: 'number', default: 33, min: 5, max: 200 },
{ name: 'longPeriod', label: 'Slow Period', type: 'number', default: 144, min: 10, max: 500 },
{ name: 'maType', label: 'MA Type', type: 'select', options: ['RMA', 'SMA', 'EMA', 'WMA', 'VWMA'], default: 'RMA' },
{ name: 'useAutoHTS', label: 'Auto HTS (TF/4)', type: 'boolean', default: false },
{ name: 'use1HFilter', label: '1H Red Zone Filter', type: 'boolean', default: true }
],
ma_trend: [
{ name: 'period', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 }
]

View File

@ -0,0 +1,423 @@
import { MA } from '../indicators/ma.js';
/**
* HTS (Higher Timeframe Trend System) Strategy Engine
* Computes trading signals based on HTS indicator rules:
* 1. Trend detection using fast/slow channels
* 2. Entry rules on channel breakouts
* 3. 1H timeframe filter (optional)
* 4. Stop loss based on opposite channel
*/
export class HTSStrategyEngine {
constructor(config = {}) {
this.config = {
shortPeriod: config.shortPeriod || 33,
longPeriod: config.longPeriod || 144,
maType: config.maType || 'RMA',
useAutoHTS: config.useAutoHTS || false,
use1HFilter: config.use1HFilter !== false
};
}
calculateHTSCandles(candles, periodMult = 1) {
if (!candles || candles.length < this.config.longPeriod) return [];
const shortPeriod = this.config.shortPeriod;
const longPeriod = this.config.longPeriod;
const maType = this.config.maType;
const shortHigh = MA.get(maType, candles, shortPeriod, 'high');
const shortLow = MA.get(maType, candles, shortPeriod, 'low');
const longHigh = MA.get(maType, candles, longPeriod, 'high');
const longLow = MA.get(maType, candles, longPeriod, 'low');
const results = [];
for (let i = 0; i < candles.length; i++) {
if (!shortHigh[i] || !longLow[i]) continue;
results.push({
time: candles[i].time,
fastHigh: shortHigh[i],
fastLow: shortLow[i],
slowHigh: longHigh[i],
slowLow: longLow[i],
price: candles[i].close,
fastMidpoint: (shortHigh[i] + shortLow[i]) / 2,
slowMidpoint: (longHigh[i] + longLow[i]) / 2
});
}
return results;
}
computeAutoHTS(oneMinCandles, targetTF) {
if (!oneMinCandles || oneMinCandles.length < this.config.longPeriod) return [];
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(currentGroup[tfGroup - 1]);
currentGroup = [];
}
}
return this.calculateHTSCandles(grouped);
}
check1HFilter(price, h1hHTS) {
if (!this.config.use1HFilter || !h1hHTS || h1hHTS.length === 0) {
return { confirmed: true, reasoning: '1H filter disabled or no data' };
}
const latest = h1hHTS[h1hHTS.length - 1];
const slowLow = latest?.slowLow;
if (slowLow === null || slowLow === undefined) {
return { confirmed: true, reasoning: '1H HTS not ready' };
}
if (price < slowLow) {
return {
confirmed: false,
reasoning: `1H Filter: Long rejected - price ${price.toFixed(2)} below 1H slow channel (${slowLow.toFixed(2)})`
};
}
return {
confirmed: true,
reasoning: `1H Filter: Confirmed - price ${price.toFixed(2)} above 1H slow channel (${slowLow.toFixed(2)})`
};
}
calculateEntrySignal(primaryHTS, index, position, h1hHTS = null, prevHTS = null) {
if (!primaryHTS || index >= primaryHTS.length || index < 1) {
return { signal: 'HOLD', confidence: 0, reasoning: 'Insufficient data' };
}
const current = primaryHTS[index];
const currentPrice = current?.price;
if (!currentPrice) {
return { signal: 'HOLD', confidence: 0, reasoning: 'No price data' };
}
const prev = primaryHTS[index - 1];
const h1hCheck = this.check1HFilter(currentPrice, h1hHTS);
if (!h1hCheck.confirmed) {
return {
signal: 'HOLD',
confidence: 0,
reasoning: h1hCheck.reasoning,
filterStatus: '1H_REJECTED'
};
}
const fastLow = current?.fastLow;
const fastHigh = current?.fastHigh;
const slowLow = current?.slowLow;
const slowHigh = current?.slowHigh;
const prevFastLow = prev?.fastLow;
const prevSlowLow = prev?.slowLow;
const prevFastHigh = prev?.fastHigh;
const prevSlowHigh = prev?.slowHigh;
if (!fastLow || !slowLow) {
return { signal: 'HOLD', confidence: 0, reasoning: 'HTS data not ready' };
}
const inPositionLong = position?.type === 'long';
const inPositionShort = position?.type === 'short';
if (inPositionLong) {
if (currentPrice < slowLow || fastLow < slowLow) {
return {
signal: 'CLOSE_LONG',
confidence: 90,
reasoning: `Stop Loss: Price ${currentPrice.toFixed(2)} broke below slow channel (${slowLow.toFixed(2)})`,
stopPrice: slowLow
};
}
return {
signal: 'HOLD',
confidence: 50,
reasoning: `Long open, holding - price ${currentPrice.toFixed(2)} above stop at ${slowLow.toFixed(2)}`,
stopPrice: slowLow
};
}
if (inPositionShort) {
if (currentPrice > slowHigh || fastHigh > slowHigh) {
return {
signal: 'CLOSE_SHORT',
confidence: 90,
reasoning: `Stop Loss: Price ${currentPrice.toFixed(2)} broke above slow channel (${slowHigh.toFixed(2)})`,
stopPrice: slowHigh
};
}
return {
signal: 'HOLD',
confidence: 50,
reasoning: `Short open, holding - price ${currentPrice.toFixed(2)} below stop at ${slowHigh.toFixed(2)}`,
stopPrice: slowHigh
};
}
if (prevFastLow <= prevSlowLow && fastLow > slowLow) {
return {
signal: 'BUY',
confidence: 75,
reasoning: `Long Entry: Fast Low (${fastLow.toFixed(2)}) crossed above Slow Low (${slowLow.toFixed(2)})`,
entryType: 'CROSSOVER'
};
}
if (prevFastHigh >= prevSlowHigh && fastHigh < slowHigh) {
return {
signal: 'SELL',
confidence: 75,
reasoning: `Short Entry: Fast High (${fastHigh.toFixed(2)}) crossed below Slow High (${slowHigh.toFixed(2)})`,
entryType: 'CROSSUNDER'
};
}
const bullAlignment = slowLow < slowHigh && fastLow > slowLow;
const bearAlignment = slowLow < slowHigh && fastHigh < slowHigh;
if (bullAlignment && currentPrice > slowLow) {
return {
signal: 'BUY',
confidence: 50,
reasoning: `Long Entry: Price ${currentPrice.toFixed(2)} above Slow Low with bullish channel alignment`,
entryType: 'ALIGNED'
};
}
if (bearAlignment && currentPrice < slowHigh) {
return {
signal: 'SELL',
confidence: 50,
reasoning: `Short Entry: Price ${currentPrice.toFixed(2)} below Slow High with bearish channel alignment`,
entryType: 'ALIGNED'
};
}
return {
signal: 'HOLD',
confidence: 10,
reasoning: 'No clear signal - waiting for crossover'
};
}
computeStopLoss(position, currentHTS, index) {
if (!position || !currentHTS || index >= currentHTS.length) return null;
const current = currentHTS[index];
if (!current) return null;
if (position.type === 'long') {
return current.slowLow;
}
if (position.type === 'short') {
return current.slowHigh;
}
return null;
}
calculateHTSCandles(candles, periodMult = 1) {
if (!candles || candles.length < this.config.longPeriod) return [];
const shortPeriod = this.config.shortPeriod;
const longPeriod = this.config.longPeriod;
const maType = this.config.maType;
const shortHigh = MA.get(maType, candles, shortPeriod, 'high');
const shortLow = MA.get(maType, candles, shortPeriod, 'low');
const longHigh = MA.get(maType, candles, longPeriod, 'high');
const longLow = MA.get(maType, candles, longPeriod, 'low');
const results = [];
for (let i = 0; i < candles.length; i++) {
if (!shortHigh[i] || !longLow[i]) continue;
results.push({
time: candles[i].time,
fastHigh: shortHigh[i],
fastLow: shortLow[i],
slowHigh: longHigh[i],
slowLow: longLow[i],
price: candles[i].close,
fastMidpoint: ((shortHigh[i] + shortLow[i]) / 2),
slowMidpoint: ((longHigh[i] + longLow[i]) / 2),
open: candles[i].open,
high: candles[i].high,
low: candles[i].low,
close: candles[i].close,
volume: candles[i].volume
});
}
return results;
}
runSimulation(candles, oneMinCandles, h1hCandles) {
const primaryHTS = this.calculateHTSCandles(candles);
console.log('[HTS] Starting simulation');
console.log('[HTS] Primary HTS data length:', primaryHTS.length);
console.log('[HTS] Original candles length:', candles.length);
let h1hHTS = null;
if (h1hCandles && h1hCandles.length > 0) {
h1hHTS = this.calculateHTSCandles(h1hCandles);
console.log('[HTS] 1H HTS data length:', h1hHTS.length);
}
let position = null;
const trades = [];
let balance = 1000;
let rejectionsCount = 0;
let buySignals = 0;
let sellSignals = 0;
for (let i = 1; i < primaryHTS.length; i++) {
if (!primaryHTS[i]) continue;
const htsData = primaryHTS[i];
const signal = this.calculateEntrySignal(primaryHTS, i, position, h1hHTS, primaryHTS[i - 1]);
if (signal.filterStatus === '1H_REJECTED') {
rejectionsCount++;
continue;
}
if (signal.signal === 'BUY' && !position) {
buySignals++;
console.log('[HTS] BUY SIGNAL:', signal.reasoning);
position = {
type: 'long',
entryPrice: htsData.price,
entryTime: htsData.time,
size: 1,
stopLoss: signal.stopPrice || primaryHTS[i].slowLow
};
console.log('[HTS] LONG entry at', htsData.price, 'stop at', position.stopLoss);
}
if (signal.signal === 'SELL' && !position) {
sellSignals++;
console.log('[HTS] SELL SIGNAL:', signal.reasoning);
position = {
type: 'short',
entryPrice: htsData.price,
entryTime: htsData.time,
size: 1,
stopLoss: signal.stopPrice || primaryHTS[i].slowHigh
};
console.log('[HTS] SHORT entry at', htsData.price, 'stop at', position.stopLoss);
}
if ((signal.signal === 'CLOSE_LONG' || signal.signal === 'CLOSE_SHORT') && position) {
const pnl = position.type === 'long'
? (htsData.price - position.entryPrice)
: (position.entryPrice - htsData.price);
trades.push({
...position,
exitPrice: htsData.price,
exitTime: htsData.time,
pnl,
pnlPct: (pnl / position.entryPrice) * 100
});
balance += pnl;
console.log('[HTS] Position closed:', signal.signal, 'PnL:', pnl.toFixed(2));
position = null;
}
if (position) {
const sl = this.computeStopLoss(position, primaryHTS, i);
if (sl !== null) {
const slHit = position.type === 'long' ? htsData.price < sl : htsData.price > sl;
if (slHit) {
const pnl = position.type === 'long'
? (sl - position.entryPrice)
: (position.entryPrice - sl);
trades.push({
...position,
exitPrice: sl,
exitTime: htsData.time,
pnl,
pnlPct: (pnl / position.entryPrice) * 100,
exitReason: 'STOP_LOSS'
});
balance += pnl;
console.log('[HTS] Stop Loss hit, PnL:', pnl.toFixed(2));
position = null;
}
}
}
}
const winRate = trades.length > 0
? (trades.filter(t => t.pnl > 0).length / trades.length) * 100
: 0;
console.log('[HTS] Simulation complete:');
console.log('[HTS] - Buy signals:', buySignals);
console.log('[HTS] - Sell signals:', sellSignals);
console.log('[HTS] - 1H filter rejections:', rejectionsCount);
console.log('[HTS] - Total HTS candles processed:', primaryHTS.length);
console.log('[HTS] - Total trades:', trades.length);
console.log('[HTS] - Final balance:', balance.toFixed(2));
console.log('[HTS] - Win rate:', winRate.toFixed(1) + '%');
if (trades.length === 0) {
console.warn('[HTS] No trades generated! Possible reasons:');
if (rejectionsCount > 0) {
console.warn(` - 1H filter rejected ${rejectionsCount} potential trades`);
}
if (buySignals === 0 && sellSignals === 0) {
console.warn(' - No crossover or alignment signals detected');
console.warn(' - Market may be in ranging/sideways mode');
}
console.warn(' - Consider:');
console.warn(' * Reducing 1H filter strictness or disabling it');
console.warn(' * Adjusting short/long periods for current timeframe');
console.warn(' * Checking if data has sufficient price movement');
}
return {
trades,
balance,
totalTrades: trades.length,
winRate,
finalPnL: balance - 1000,
total_trades: trades.length,
win_rate: winRate,
total_pnl: balance - 1000,
htsData: primaryHTS
};
}
}

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) {