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:
79
HTS_STRATEGY.md
Normal file
79
HTS_STRATEGY.md
Normal 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
|
||||
@ -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">
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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'
|
||||
};
|
||||
|
||||
@ -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 }
|
||||
]
|
||||
|
||||
423
src/api/dashboard/static/js/strategies/hts-engine.js
Normal file
423
src/api/dashboard/static/js/strategies/hts-engine.js
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
233
src/api/dashboard/static/js/ui/hts-visualizer.js
Normal file
233
src/api/dashboard/static/js/ui/hts-visualizer.js
Normal 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;
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user