Pre-refactor: commit before converting indicators to self-contained files
This commit is contained in:
@ -1,25 +1,5 @@
|
||||
import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js';
|
||||
import { restoreSidebarState, toggleSidebar, initSidebarTabs, restoreSidebarTabState } from './ui/sidebar.js';
|
||||
import { SimulationStorage } from './ui/storage.js';
|
||||
import { showExportDialog, closeExportDialog, performExport, exportSavedSimulation } from './ui/export.js';
|
||||
import {
|
||||
runSimulation,
|
||||
displayEnhancedResults,
|
||||
showSimulationMarkers,
|
||||
clearSimulationResults,
|
||||
getLastResults,
|
||||
setLastResults
|
||||
} from './ui/simulation.js';
|
||||
import {
|
||||
renderStrategies,
|
||||
selectStrategy,
|
||||
loadStrategies,
|
||||
saveSimulation,
|
||||
renderSavedSimulations,
|
||||
loadSavedSimulation,
|
||||
deleteSavedSimulation,
|
||||
setCurrentStrategy
|
||||
} from './ui/strategies-panel.js';
|
||||
import {
|
||||
initIndicatorPanel,
|
||||
getActiveIndicators,
|
||||
@ -28,42 +8,13 @@ import {
|
||||
addIndicator,
|
||||
removeIndicatorById
|
||||
} from './ui/indicators-panel-new.js';
|
||||
import { StrategyParams } from './strategies/config.js';
|
||||
import { IndicatorRegistry } from './indicators/index.js';
|
||||
|
||||
window.dashboard = null;
|
||||
|
||||
function setDefaultStartDate() {
|
||||
const startDateInput = document.getElementById('simStartDate');
|
||||
if (startDateInput) {
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
startDateInput.value = sevenDaysAgo.toISOString().slice(0, 16);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTimeframeDisplay() {
|
||||
const display = document.getElementById('simTimeframeDisplay');
|
||||
if (display && window.dashboard) {
|
||||
display.value = window.dashboard.currentInterval.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
window.toggleSidebar = toggleSidebar;
|
||||
window.refreshTA = refreshTA;
|
||||
window.openAIAnalysis = openAIAnalysis;
|
||||
window.showExportDialog = showExportDialog;
|
||||
window.closeExportDialog = closeExportDialog;
|
||||
window.performExport = performExport;
|
||||
window.exportSavedSimulation = exportSavedSimulation;
|
||||
window.runSimulation = runSimulation;
|
||||
window.saveSimulation = saveSimulation;
|
||||
window.showSimulationMarkers = showSimulationMarkers;
|
||||
window.renderSavedSimulations = renderSavedSimulations;
|
||||
window.loadSavedSimulation = loadSavedSimulation;
|
||||
window.deleteSavedSimulation = deleteSavedSimulation;
|
||||
window.clearSimulationResults = clearSimulationResults;
|
||||
window.updateTimeframeDisplay = updateTimeframeDisplay;
|
||||
window.renderIndicatorList = function() {
|
||||
// This function is no longer needed for sidebar indicators
|
||||
};
|
||||
@ -73,8 +24,6 @@ window.initIndicatorPanel = initIndicatorPanel;
|
||||
window.addIndicator = addIndicator;
|
||||
window.toggleIndicator = addIndicator;
|
||||
|
||||
window.StrategyParams = StrategyParams;
|
||||
window.SimulationStorage = SimulationStorage;
|
||||
window.IndicatorRegistry = IndicatorRegistry;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
@ -88,11 +37,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
restoreSidebarState();
|
||||
restoreSidebarTabState();
|
||||
initSidebarTabs();
|
||||
setDefaultStartDate();
|
||||
updateTimeframeDisplay();
|
||||
renderSavedSimulations();
|
||||
|
||||
await loadStrategies();
|
||||
|
||||
// Initialize indicator panel
|
||||
window.initIndicatorPanel();
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
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 }
|
||||
]
|
||||
};
|
||||
@ -1,167 +0,0 @@
|
||||
import { IndicatorRegistry } from '../indicators/index.js';
|
||||
import { RiskManager } from './risk-manager.js';
|
||||
|
||||
export class ClientStrategyEngine {
|
||||
constructor() {
|
||||
this.indicatorTypes = IndicatorRegistry;
|
||||
}
|
||||
|
||||
run(candlesMap, strategyConfig, riskConfig, simulationStart) {
|
||||
const primaryTF = strategyConfig.timeframes?.primary || '1d';
|
||||
const candles = candlesMap[primaryTF];
|
||||
if (!candles) return { error: `No candles for primary timeframe ${primaryTF}` };
|
||||
|
||||
const indicatorResults = {};
|
||||
console.log('Calculating indicators for timeframes:', Object.keys(candlesMap));
|
||||
for (const tf in candlesMap) {
|
||||
indicatorResults[tf] = {};
|
||||
const tfCandles = candlesMap[tf];
|
||||
const tfIndicators = (strategyConfig.indicators || []).filter(ind => (ind.timeframe || primaryTF) === tf);
|
||||
|
||||
console.log(` TF ${tf}: ${tfIndicators.length} indicators to calculate`);
|
||||
|
||||
for (const ind of tfIndicators) {
|
||||
const IndicatorClass = this.indicatorTypes[ind.type];
|
||||
if (IndicatorClass) {
|
||||
const instance = new IndicatorClass(ind);
|
||||
indicatorResults[tf][ind.name] = instance.calculate(tfCandles);
|
||||
const validValues = indicatorResults[tf][ind.name].filter(v => v !== null).length;
|
||||
console.log(` Calculated ${ind.name} on ${tf}: ${validValues} valid values`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const risk = new RiskManager(riskConfig);
|
||||
const trades = [];
|
||||
let position = null;
|
||||
const startTimeSec = Math.floor(new Date(simulationStart).getTime() / 1000);
|
||||
console.log('Simulation start (seconds):', startTimeSec, 'Date:', simulationStart);
|
||||
console.log('Total candles available:', candles.length);
|
||||
console.log('First candle time:', candles[0].time, 'Last candle time:', candles[candles.length - 1].time);
|
||||
|
||||
const pointers = {};
|
||||
for (const tf in candlesMap) pointers[tf] = 0;
|
||||
|
||||
let processedCandles = 0;
|
||||
|
||||
for (let i = 1; i < candles.length; i++) {
|
||||
const time = candles[i].time;
|
||||
const price = candles[i].close;
|
||||
|
||||
if (time < startTimeSec) {
|
||||
for (const tf in candlesMap) {
|
||||
while (pointers[tf] < candlesMap[tf].length - 1 &&
|
||||
candlesMap[tf][pointers[tf] + 1].time <= time) {
|
||||
pointers[tf]++;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
processedCandles++;
|
||||
|
||||
for (const tf in candlesMap) {
|
||||
while (pointers[tf] < candlesMap[tf].length - 1 &&
|
||||
candlesMap[tf][pointers[tf] + 1].time <= time) {
|
||||
pointers[tf]++;
|
||||
}
|
||||
}
|
||||
|
||||
const signal = this.evaluate(i, pointers, candles, candlesMap, indicatorResults, strategyConfig, position);
|
||||
|
||||
if (signal === 'BUY' && !position) {
|
||||
const size = risk.calculateSize(price);
|
||||
position = { type: 'long', entryPrice: price, entryTime: candles[i].time, size };
|
||||
} else if (signal === 'SELL' && position) {
|
||||
const pnl = (price - position.entryPrice) * position.size;
|
||||
trades.push({ ...position, exitPrice: price, exitTime: candles[i].time, pnl, pnlPct: (pnl / (position.entryPrice * position.size)) * 100 });
|
||||
risk.balance += pnl;
|
||||
position = null;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Simulation complete: ${processedCandles} candles processed after start date, ${trades.length} trades`);
|
||||
|
||||
return {
|
||||
total_trades: trades.length,
|
||||
win_rate: (trades.filter(t => t.pnl > 0).length / (trades.length || 1)) * 100,
|
||||
total_pnl: risk.balance - 1000,
|
||||
trades
|
||||
};
|
||||
}
|
||||
|
||||
evaluate(index, pointers, candles, candlesMap, indicatorResults, config, position) {
|
||||
const primaryTF = config.timeframes?.primary || '1d';
|
||||
|
||||
const getVal = (indName, tf) => {
|
||||
const tfValues = indicatorResults[tf]?.[indName];
|
||||
if (!tfValues) return null;
|
||||
return tfValues[pointers[tf]];
|
||||
};
|
||||
|
||||
const getPrice = (tf) => {
|
||||
const tfCandles = candlesMap[tf];
|
||||
if (!tfCandles) return null;
|
||||
return tfCandles[pointers[tf]].close;
|
||||
};
|
||||
|
||||
if (config.id === 'ma_trend') {
|
||||
const period = config.params?.period || 44;
|
||||
|
||||
if (index === 1) {
|
||||
console.log('First candle time:', candles[index].time, 'Date:', new Date(candles[index].time * 1000));
|
||||
console.log(`MA${period} value:`, getVal(`ma${period}`, primaryTF));
|
||||
}
|
||||
const maValue = getVal(`ma${period}`, primaryTF);
|
||||
const price = candles[index].close;
|
||||
|
||||
const secondaryTF = config.timeframes?.secondary?.[0];
|
||||
let secondaryBullish = true;
|
||||
let secondaryBearish = true;
|
||||
if (secondaryTF) {
|
||||
const secondaryPrice = getPrice(secondaryTF);
|
||||
const secondaryMA = getVal(`ma${period}_${secondaryTF}`, secondaryTF);
|
||||
if (secondaryPrice !== null && secondaryMA !== null) {
|
||||
secondaryBullish = secondaryPrice > secondaryMA;
|
||||
secondaryBearish = secondaryPrice < secondaryMA;
|
||||
}
|
||||
if (index === 1) {
|
||||
console.log(`Trend check: ${secondaryTF} price=${secondaryPrice}, MA=${secondaryMA}, bullish=${secondaryBullish}, bearish=${secondaryBearish}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (maValue) {
|
||||
if (price > maValue && secondaryBullish) return 'BUY';
|
||||
if (price < maValue && secondaryBearish) return 'SELL';
|
||||
}
|
||||
}
|
||||
|
||||
const evaluateConditions = (conds) => {
|
||||
if (!conds || !conds.conditions) return false;
|
||||
const results = conds.conditions.map(c => {
|
||||
const targetTF = c.timeframe || primaryTF;
|
||||
const leftVal = c.indicator === 'price' ? getPrice(targetTF) : getVal(c.indicator, targetTF);
|
||||
const rightVal = typeof c.value === 'number' ? c.value : (c.value === 'price' ? getPrice(targetTF) : getVal(c.value, targetTF));
|
||||
|
||||
if (leftVal === null || rightVal === null) return false;
|
||||
|
||||
switch(c.operator) {
|
||||
case '>': return leftVal > rightVal;
|
||||
case '<': return leftVal < rightVal;
|
||||
case '>=': return leftVal >= rightVal;
|
||||
case '<=': return leftVal <= rightVal;
|
||||
case '==': return leftVal == rightVal;
|
||||
default: return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (conds.logic === 'OR') return results.some(r => r);
|
||||
return results.every(r => r);
|
||||
};
|
||||
|
||||
if (evaluateConditions(config.entryLong)) return 'BUY';
|
||||
if (evaluateConditions(config.exitLong)) return 'SELL';
|
||||
|
||||
return 'HOLD';
|
||||
}
|
||||
}
|
||||
@ -1,423 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export { StrategyParams } from './config.js';
|
||||
export { RiskManager } from './risk-manager.js';
|
||||
export { ClientStrategyEngine } from './engine.js';
|
||||
@ -1,17 +0,0 @@
|
||||
export class RiskManager {
|
||||
constructor(config, initialBalance = 1000) {
|
||||
this.config = config || {
|
||||
positionSizing: { method: 'percent', value: 0.1 },
|
||||
stopLoss: { enabled: true, method: 'percent', value: 0.02 },
|
||||
takeProfit: { enabled: true, method: 'percent', value: 0.04 }
|
||||
};
|
||||
this.balance = initialBalance;
|
||||
this.equity = initialBalance;
|
||||
}
|
||||
calculateSize(price) {
|
||||
if (this.config.positionSizing.method === 'percent') {
|
||||
return (this.balance * this.config.positionSizing.value) / price;
|
||||
}
|
||||
return this.config.positionSizing.value / price;
|
||||
}
|
||||
}
|
||||
@ -1,140 +0,0 @@
|
||||
import { downloadFile } from '../utils/index.js';
|
||||
|
||||
export function showExportDialog() {
|
||||
if (!window.lastSimulationResults) {
|
||||
alert('Please run a simulation first');
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'dialog-overlay';
|
||||
overlay.onclick = () => closeExportDialog();
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const dialog = document.createElement('div');
|
||||
dialog.className = 'export-dialog';
|
||||
dialog.id = 'exportDialog';
|
||||
dialog.innerHTML = `
|
||||
<div class="export-dialog-title">📥 Export Simulation Report</div>
|
||||
<div class="export-options">
|
||||
<label class="export-option">
|
||||
<input type="radio" name="exportFormat" value="csv" checked>
|
||||
<span>CSV (Trades list)</span>
|
||||
</label>
|
||||
<label class="export-option">
|
||||
<input type="radio" name="exportFormat" value="json">
|
||||
<span>JSON (Full data)</span>
|
||||
</label>
|
||||
<label class="export-option">
|
||||
<input type="radio" name="exportFormat" value="both">
|
||||
<span>Both CSV + JSON</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="action-btn secondary" onclick="closeExportDialog()" style="flex: 1;">Cancel</button>
|
||||
<button class="action-btn primary" onclick="performExport()" style="flex: 1;">Export</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
}
|
||||
|
||||
export function closeExportDialog() {
|
||||
const overlay = document.querySelector('.dialog-overlay');
|
||||
const dialog = document.getElementById('exportDialog');
|
||||
if (overlay) overlay.remove();
|
||||
if (dialog) dialog.remove();
|
||||
}
|
||||
|
||||
export function performExport() {
|
||||
const format = document.querySelector('input[name="exportFormat"]:checked').value;
|
||||
const sim = window.lastSimulationResults;
|
||||
const config = sim.config || {};
|
||||
const dateStr = new Date().toISOString().slice(0, 10);
|
||||
const baseFilename = generateSimulationName(config).replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
|
||||
if (format === 'csv' || format === 'both') {
|
||||
exportToCSV(sim, `${baseFilename}.csv`);
|
||||
}
|
||||
|
||||
if (format === 'json' || format === 'both') {
|
||||
exportToJSON(sim, `${baseFilename}.json`);
|
||||
}
|
||||
|
||||
closeExportDialog();
|
||||
}
|
||||
|
||||
function generateSimulationName(config) {
|
||||
if (!config) return 'Unnamed Simulation';
|
||||
|
||||
const start = new Date(config.startDate);
|
||||
const now = new Date();
|
||||
const duration = now - start;
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
|
||||
let dateStr;
|
||||
if (duration < oneDay) {
|
||||
dateStr = start.toISOString().slice(0, 16).replace('T', ' ');
|
||||
} else {
|
||||
dateStr = start.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
return `${config.strategyName}_${config.timeframe}_${dateStr}`;
|
||||
}
|
||||
|
||||
function exportToCSV(simulation, filename) {
|
||||
const results = simulation.results || simulation;
|
||||
const config = simulation.config || {};
|
||||
|
||||
let csv = 'Trade #,Entry Time,Exit Time,Entry Price,Exit Price,Size,P&L ($),P&L (%),Type\n';
|
||||
|
||||
(results.trades || []).forEach((trade, i) => {
|
||||
csv += `${i + 1},${trade.entryTime},${trade.exitTime},${trade.entryPrice},${trade.exitPrice},${trade.size},${trade.pnl},${trade.pnlPct},${trade.type}\n`;
|
||||
});
|
||||
|
||||
csv += '\n';
|
||||
csv += 'Summary\n';
|
||||
csv += `Strategy,${config.strategyName || 'Unknown'}\n`;
|
||||
csv += `Timeframe,${config.timeframe || 'Unknown'}\n`;
|
||||
csv += `Start Date,${config.startDate || 'Unknown'}\n`;
|
||||
csv += `Total Trades,${results.total_trades || 0}\n`;
|
||||
csv += `Win Rate (%),${(results.win_rate || 0).toFixed(2)}\n`;
|
||||
csv += `Total P&L ($),${(results.total_pnl || 0).toFixed(2)}\n`;
|
||||
csv += `Risk % per Trade,${config.riskPercent || 2}\n`;
|
||||
csv += `Stop Loss %,${config.stopLossPercent || 2}\n`;
|
||||
|
||||
downloadFile(csv, filename, 'text/csv');
|
||||
}
|
||||
|
||||
function exportToJSON(simulation, filename) {
|
||||
const exportData = {
|
||||
metadata: {
|
||||
exported_at: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
},
|
||||
configuration: simulation.config || {},
|
||||
results: {
|
||||
summary: {
|
||||
total_trades: simulation.total_trades || simulation.results?.total_trades || 0,
|
||||
win_rate: simulation.win_rate || simulation.results?.win_rate || 0,
|
||||
total_pnl: simulation.total_pnl || simulation.results?.total_pnl || 0
|
||||
},
|
||||
trades: simulation.trades || simulation.results?.trades || [],
|
||||
equity_curve: simulation.equity_curve || []
|
||||
}
|
||||
};
|
||||
|
||||
downloadFile(JSON.stringify(exportData, null, 2), filename, 'application/json');
|
||||
}
|
||||
|
||||
export function exportSavedSimulation(id) {
|
||||
const sim = window.SimulationStorage?.get(id);
|
||||
if (!sim) {
|
||||
alert('Simulation not found');
|
||||
return;
|
||||
}
|
||||
|
||||
window.lastSimulationResults = sim;
|
||||
showExportDialog();
|
||||
}
|
||||
|
||||
window.generateSimulationName = generateSimulationName;
|
||||
@ -1,5 +1,3 @@
|
||||
import { HTSStrategyEngine } from '../strategies/hts-engine.js';
|
||||
|
||||
const HTS_COLORS = {
|
||||
fastHigh: '#00bcd4',
|
||||
fastLow: '#00bcd4',
|
||||
|
||||
@ -1,28 +1,5 @@
|
||||
export { TradingDashboard, refreshTA, openAIAnalysis } from './chart.js';
|
||||
export { toggleSidebar, restoreSidebarState } from './sidebar.js';
|
||||
export { SimulationStorage } from './storage.js';
|
||||
export { showExportDialog, closeExportDialog, performExport, exportSavedSimulation } from './export.js';
|
||||
export {
|
||||
runSimulation,
|
||||
displayEnhancedResults,
|
||||
showSimulationMarkers,
|
||||
clearSimulationMarkers,
|
||||
clearSimulationResults,
|
||||
getLastResults,
|
||||
setLastResults
|
||||
} from './simulation.js';
|
||||
export {
|
||||
renderStrategies,
|
||||
selectStrategy,
|
||||
renderStrategyParams,
|
||||
loadStrategies,
|
||||
saveSimulation,
|
||||
renderSavedSimulations,
|
||||
loadSavedSimulation,
|
||||
deleteSavedSimulation,
|
||||
getCurrentStrategy,
|
||||
setCurrentStrategy
|
||||
} from './strategies-panel.js';
|
||||
export {
|
||||
renderIndicatorList,
|
||||
addNewIndicator,
|
||||
|
||||
@ -295,9 +295,50 @@ function renderIndicatorConfig(indicator, meta) {
|
||||
<input type="range" min="1" max="5" value="${indicator.params._lineWidth || 2}" onchange="this.nextElementSibling.textContent = this.value; window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '_lineWidth', parseInt(this.value))">
|
||||
<span class="range-value">${indicator.params._lineWidth || 2}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${window.dashboard?.indicatorSignals ? `
|
||||
<div class="config-section">
|
||||
<div class="section-subtitle">Signal Status</div>
|
||||
${(() => {
|
||||
const indSignal = window.dashboard.indicatorSignals.find(s => s.id === indicator.id);
|
||||
if (!indSignal) {
|
||||
return `<div class="config-row" style="color: var(--tv-text-secondary); font-size: 12px;">No signal data available</div>`;
|
||||
}
|
||||
|
||||
const signalType = indSignal.signal.toUpperCase();
|
||||
const signalIcon = signalType === 'BUY' ? '🟢' : signalType === 'SELL' ? '🔴' : '⚪';
|
||||
const signalColor = indSignal.color || 'var(--tv-text-secondary)';
|
||||
const signalDate = indSignal.lastSignalDate ? new Date(indSignal.lastSignalDate * 1000).toLocaleString() : 'Waiting for crossover...';
|
||||
|
||||
return `
|
||||
<div class="config-row" style="align-items: center;">
|
||||
<label style="flex: 0 0 80px;">Status</label>
|
||||
<span class="ta-signal ${indSignal.signal}" style="font-size: 12px; padding: 4px 12px; min-width: 70px; text-align: center; background: ${signalColor}; color: white; border-radius: 4px;">
|
||||
${signalIcon} ${signalType}
|
||||
</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label style="flex: 0 0 80px;">Last Signal</label>
|
||||
<span style="color: var(--tv-text-primary); font-size: 12px;">${signalDate}</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label style="flex: 0 0 80px;">Strength</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px; flex: 1;">
|
||||
<input type="range" min="0" max="100" value="${indSignal.strength}" disabled style="width: 80px;">
|
||||
<span style="color: var(--tv-text-primary); font-size: 12px;">${indSignal.strength}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label style="flex: 0 0 80px;">Reasoning</label>
|
||||
<span style="color: var(--tv-text-primary); font-size: 11px;">${indSignal.reasoning}</span>
|
||||
</div>
|
||||
`;
|
||||
})()}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${meta?.inputs && meta.inputs.length > 0 ? `
|
||||
<div class="config-section">
|
||||
<div class="section-subtitle">Parameters</div>
|
||||
|
||||
@ -236,18 +236,16 @@ function calculateMASignal(indicator, lastCandle, prevCandle, values) {
|
||||
|
||||
let signalType, strength, reasoning;
|
||||
|
||||
if (close > ma * 1.02) {
|
||||
if (close > ma) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = Math.min(60 + ((close - ma) / ma) * 500, 100);
|
||||
reasoning = `Price (${close.toFixed(2)}) is strongly above ${maLabel} (${ma.toFixed(2)}), bullish trend`;
|
||||
} else if (close < ma * 0.98) {
|
||||
reasoning = `Price (${close.toFixed(2)}) is above ${maLabel} (${ma.toFixed(2)})`;
|
||||
} else if (close < ma) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = Math.min(60 + ((ma - close) / ma) * 500, 100);
|
||||
reasoning = `Price (${close.toFixed(2)}) is strongly below ${maLabel} (${ma.toFixed(2)}), bearish trend`;
|
||||
reasoning = `Price (${close.toFixed(2)}) is below ${maLabel} (${ma.toFixed(2)})`;
|
||||
} else {
|
||||
signalType = SIGNAL_TYPES.HOLD;
|
||||
strength = 30;
|
||||
reasoning = `Price (${close.toFixed(2)}) is near ${maLabel} (${ma.toFixed(2)}), sideways/consolidating`;
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('[calculateMASignal] Result:', signalType, strength);
|
||||
@ -406,6 +404,36 @@ export function calculateAllIndicatorSignals() {
|
||||
|
||||
const signal = calculateIndicatorSignal(indicator, candles, values);
|
||||
|
||||
let currentSignal = signal;
|
||||
let lastSignalDate = indicator.lastSignalTimestamp || null;
|
||||
let lastSignalType = indicator.lastSignalType || null;
|
||||
|
||||
if (!currentSignal || !currentSignal.type) {
|
||||
console.log('[Signals] No valid signal for', indicator.type, '- Using last signal if available');
|
||||
|
||||
if (lastSignalType && lastSignalDate) {
|
||||
currentSignal = {
|
||||
type: lastSignalType,
|
||||
strength: 50,
|
||||
value: candles[candles.length - 1]?.close,
|
||||
reasoning: `No crossover (price equals MA)`
|
||||
};
|
||||
} else {
|
||||
console.log('[Signals] No previous signal available - Skipping');
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
const currentCandleTimestamp = candles[candles.length - 1].time;
|
||||
|
||||
if (currentSignal.type !== lastSignalType || !lastSignalType) {
|
||||
console.log('[Signals] Signal changed for', indicator.type, ':', lastSignalType, '->', currentSignal.type);
|
||||
lastSignalDate = currentCandleTimestamp;
|
||||
lastSignalType = currentSignal.type;
|
||||
indicator.lastSignalTimestamp = lastSignalDate;
|
||||
indicator.lastSignalType = lastSignalType;
|
||||
}
|
||||
}
|
||||
|
||||
const label = indicator.type?.toUpperCase();
|
||||
const params = indicator.params && typeof indicator.params === 'object'
|
||||
? Object.entries(indicator.params)
|
||||
@ -420,11 +448,12 @@ export function calculateAllIndicatorSignals() {
|
||||
label: label,
|
||||
params: params || null,
|
||||
type: indicator.type,
|
||||
signal: signal.type,
|
||||
strength: Math.round(signal.strength),
|
||||
value: signal.value,
|
||||
reasoning: signal.reasoning,
|
||||
color: SIGNAL_COLORS[signal.type]
|
||||
signal: currentSignal.type,
|
||||
strength: Math.round(currentSignal.strength),
|
||||
value: currentSignal.value,
|
||||
reasoning: currentSignal.reasoning,
|
||||
color: SIGNAL_COLORS[currentSignal.type],
|
||||
lastSignalDate: lastSignalDate
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,567 +0,0 @@
|
||||
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';
|
||||
|
||||
let lastSimulationResults = null;
|
||||
|
||||
export function getLastResults() {
|
||||
return lastSimulationResults;
|
||||
}
|
||||
|
||||
export function setLastResults(results) {
|
||||
lastSimulationResults = results;
|
||||
window.lastSimulationResults = results;
|
||||
}
|
||||
|
||||
export async function runSimulation() {
|
||||
const strategyConfig = getStrategyConfig();
|
||||
if (!strategyConfig) {
|
||||
alert('Please select a strategy');
|
||||
return;
|
||||
}
|
||||
|
||||
const startDateInput = document.getElementById('simStartDate').value;
|
||||
if (!startDateInput) {
|
||||
alert('Please select a start date');
|
||||
return;
|
||||
}
|
||||
|
||||
const runBtn = document.getElementById('runSimBtn');
|
||||
runBtn.disabled = true;
|
||||
runBtn.textContent = '⏳ Running...';
|
||||
|
||||
try {
|
||||
const start = new Date(startDateInput);
|
||||
const fetchStart = new Date(start.getTime() - 200 * 24 * 60 * 60 * 1000);
|
||||
|
||||
if (!window.dashboard) {
|
||||
throw new Error('Dashboard not initialized');
|
||||
}
|
||||
const interval = window.dashboard.currentInterval;
|
||||
const secondaryTF = document.getElementById('simSecondaryTF').value;
|
||||
const riskPercent = parseFloat(document.getElementById('simRiskPercent').value);
|
||||
const stopLossPercent = parseFloat(document.getElementById('simStopLoss').value);
|
||||
|
||||
const timeframes = [interval];
|
||||
if (secondaryTF && secondaryTF !== '') {
|
||||
timeframes.push(secondaryTF);
|
||||
}
|
||||
|
||||
const query = new URLSearchParams({ symbol: 'BTC', start: fetchStart.toISOString() });
|
||||
timeframes.forEach(tf => query.append('timeframes', tf));
|
||||
|
||||
console.log('Fetching candles with query:', query.toString());
|
||||
|
||||
const response = await fetch(`/api/v1/candles/bulk?${query.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Candle data received:', data);
|
||||
console.log('Looking for interval:', interval);
|
||||
console.log('Available timeframes:', Object.keys(data));
|
||||
|
||||
if (!data[interval] || data[interval].length === 0) {
|
||||
throw new Error(`No candle data available for ${interval} timeframe. Check if data exists in database.`);
|
||||
}
|
||||
|
||||
const candlesMap = {
|
||||
[interval]: data[interval].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)
|
||||
}))
|
||||
};
|
||||
|
||||
if (secondaryTF && data[secondaryTF]) {
|
||||
candlesMap[secondaryTF] = data[secondaryTF].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)
|
||||
}));
|
||||
}
|
||||
|
||||
const engineConfig = {
|
||||
id: strategyConfig.id,
|
||||
params: strategyConfig.params,
|
||||
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));
|
||||
|
||||
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}`,
|
||||
type: 'sma',
|
||||
params: { period: period },
|
||||
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);
|
||||
}
|
||||
|
||||
if (results.error) throw new Error(results.error);
|
||||
|
||||
setLastResults({
|
||||
...results,
|
||||
config: {
|
||||
strategyId: strategyConfig.id,
|
||||
strategyName: window.availableStrategies?.find(s => s.id === strategyConfig.id)?.name || strategyConfig.id,
|
||||
timeframe: interval,
|
||||
secondaryTimeframe: secondaryTF,
|
||||
startDate: startDateInput,
|
||||
riskPercent: riskPercent,
|
||||
stopLossPercent: stopLossPercent,
|
||||
params: strategyConfig.params
|
||||
},
|
||||
runAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
displayEnhancedResults(lastSimulationResults);
|
||||
|
||||
document.getElementById('resultsSection').style.display = 'block';
|
||||
|
||||
if (window.dashboard && candlesMap[interval]) {
|
||||
const chartData = candlesMap[interval].map(c => ({
|
||||
time: c.time,
|
||||
open: c.open,
|
||||
high: c.high,
|
||||
low: c.low,
|
||||
close: c.close
|
||||
}));
|
||||
window.dashboard.candleSeries.setData(chartData);
|
||||
window.dashboard.allData.set(interval, chartData);
|
||||
console.log(`Chart updated with ${chartData.length} candles from simulation range`);
|
||||
}
|
||||
|
||||
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);
|
||||
} finally {
|
||||
runBtn.disabled = false;
|
||||
runBtn.textContent = '▶ Run Simulation';
|
||||
}
|
||||
}
|
||||
|
||||
export function displayEnhancedResults(simulation) {
|
||||
const results = simulation.results || simulation;
|
||||
|
||||
document.getElementById('simTrades').textContent = results.total_trades || '0';
|
||||
document.getElementById('simWinRate').textContent = (results.win_rate || 0).toFixed(1) + '%';
|
||||
|
||||
const pnl = results.total_pnl || 0;
|
||||
const pnlElement = document.getElementById('simPnL');
|
||||
pnlElement.textContent = (pnl >= 0 ? '+' : '') + '$' + pnl.toFixed(2);
|
||||
pnlElement.style.color = pnl >= 0 ? '#4caf50' : '#f44336';
|
||||
|
||||
let grossProfit = 0;
|
||||
let grossLoss = 0;
|
||||
(results.trades || []).forEach(trade => {
|
||||
if (trade.pnl > 0) grossProfit += trade.pnl;
|
||||
else grossLoss += Math.abs(trade.pnl);
|
||||
});
|
||||
const profitFactor = grossLoss > 0 ? (grossProfit / grossLoss).toFixed(2) : grossProfit > 0 ? '∞' : '0';
|
||||
document.getElementById('simProfitFactor').textContent = profitFactor;
|
||||
|
||||
drawEquitySparkline(results);
|
||||
}
|
||||
|
||||
function drawEquitySparkline(results) {
|
||||
const container = document.getElementById('equitySparkline');
|
||||
if (!container || !results.trades || results.trades.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px; font-size: 11px;">No trades</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let equity = 1000;
|
||||
const equityData = [{ time: results.trades[0].entryTime, equity: equity }];
|
||||
|
||||
results.trades.forEach(trade => {
|
||||
equity += trade.pnl;
|
||||
equityData.push({ time: trade.exitTime, equity: equity });
|
||||
});
|
||||
|
||||
if (lastSimulationResults) {
|
||||
lastSimulationResults.equity_curve = equityData;
|
||||
}
|
||||
|
||||
container.innerHTML = '<canvas id="sparklineCanvas" width="300" height="60"></canvas>';
|
||||
const canvas = document.getElementById('sparklineCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const minEquity = Math.min(...equityData.map(d => d.equity));
|
||||
const maxEquity = Math.max(...equityData.map(d => d.equity));
|
||||
const range = maxEquity - minEquity || 1;
|
||||
|
||||
ctx.strokeStyle = equityData[equityData.length - 1].equity >= equityData[0].equity ? '#4caf50' : '#f44336';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
equityData.forEach((point, i) => {
|
||||
const x = (i / (equityData.length - 1)) * canvas.width;
|
||||
const y = canvas.height - ((point.equity - minEquity) / range) * canvas.height;
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#888';
|
||||
ctx.font = '9px sans-serif';
|
||||
ctx.fillText('$' + equityData[0].equity.toFixed(0), 2, canvas.height - 2);
|
||||
ctx.fillText('$' + equityData[equityData.length - 1].equity.toFixed(0), canvas.width - 30, 10);
|
||||
}
|
||||
|
||||
let tradeLineSeries = [];
|
||||
let htsVisualizer = null;
|
||||
let tradeMarkerSeries = [];
|
||||
|
||||
export function showSimulationMarkers() {
|
||||
const results = getLastResults();
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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() {
|
||||
try {
|
||||
if (window.dashboard && window.dashboard.candleSeries && typeof window.dashboard.candleSeries.setMarkers === 'function') {
|
||||
window.dashboard.candleSeries.setMarkers([]);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors clearing markers
|
||||
}
|
||||
|
||||
try {
|
||||
tradeLineSeries.forEach(series => {
|
||||
try {
|
||||
if (window.dashboard && window.dashboard.chart) {
|
||||
window.dashboard.chart.removeSeries(series);
|
||||
}
|
||||
} catch (e) {
|
||||
// Series might already be removed
|
||||
}
|
||||
});
|
||||
} 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() {
|
||||
clearSimulationMarkers();
|
||||
|
||||
setLastResults(null);
|
||||
|
||||
const resultsSection = document.getElementById('resultsSection');
|
||||
if (resultsSection) {
|
||||
resultsSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const simTrades = document.getElementById('simTrades');
|
||||
const simWinRate = document.getElementById('simWinRate');
|
||||
const simPnL = document.getElementById('simPnL');
|
||||
const simProfitFactor = document.getElementById('simProfitFactor');
|
||||
const equitySparkline = document.getElementById('equitySparkline');
|
||||
|
||||
if (simTrades) simTrades.textContent = '0';
|
||||
if (simWinRate) simWinRate.textContent = '0%';
|
||||
if (simPnL) {
|
||||
simPnL.textContent = '$0.00';
|
||||
simPnL.style.color = '';
|
||||
}
|
||||
if (simProfitFactor) simProfitFactor.textContent = '0';
|
||||
if (equitySparkline) equitySparkline.innerHTML = '';
|
||||
}
|
||||
|
||||
function getStrategyConfig() {
|
||||
const strategyId = window.currentStrategy;
|
||||
if (!strategyId) return null;
|
||||
|
||||
const params = {};
|
||||
const paramDefs = window.StrategyParams?.[strategyId] || [];
|
||||
|
||||
paramDefs.forEach(def => {
|
||||
const input = document.getElementById(`param_${def.name}`);
|
||||
if (input) {
|
||||
params[def.name] = def.type === 'number' ? parseFloat(input.value) : input.value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: strategyId,
|
||||
params: params
|
||||
};
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
export const SimulationStorage = {
|
||||
STORAGE_KEY: 'btc_bot_simulations',
|
||||
|
||||
getAll() {
|
||||
try {
|
||||
const data = localStorage.getItem(this.STORAGE_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch (e) {
|
||||
console.error('Error reading simulations:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
save(simulation) {
|
||||
try {
|
||||
const simulations = this.getAll();
|
||||
simulation.id = simulation.id || 'sim_' + Date.now();
|
||||
simulation.createdAt = new Date().toISOString();
|
||||
simulations.push(simulation);
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(simulations));
|
||||
return simulation.id;
|
||||
} catch (e) {
|
||||
console.error('Error saving simulation:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
try {
|
||||
let simulations = this.getAll();
|
||||
simulations = simulations.filter(s => s.id !== id);
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(simulations));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Error deleting simulation:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
get(id) {
|
||||
return this.getAll().find(s => s.id === id);
|
||||
},
|
||||
|
||||
clear() {
|
||||
localStorage.removeItem(this.STORAGE_KEY);
|
||||
}
|
||||
};
|
||||
@ -1,318 +0,0 @@
|
||||
import { StrategyParams } from '../strategies/config.js';
|
||||
|
||||
let currentStrategy = null;
|
||||
|
||||
export function getCurrentStrategy() {
|
||||
return currentStrategy;
|
||||
}
|
||||
|
||||
export function setCurrentStrategy(strategyId) {
|
||||
currentStrategy = strategyId;
|
||||
window.currentStrategy = strategyId;
|
||||
}
|
||||
|
||||
export function renderStrategies(strategies) {
|
||||
const container = document.getElementById('strategyList');
|
||||
|
||||
if (!strategies || strategies.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px;">No strategies available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = strategies.map((s, index) => `
|
||||
<div class="strategy-item ${index === 0 ? 'selected' : ''}" data-strategy-id="${s.id}" onclick="selectStrategy('${s.id}')">
|
||||
<input type="radio" name="strategy" class="strategy-radio" ${index === 0 ? 'checked' : ''}>
|
||||
<span class="strategy-name">${s.name}</span>
|
||||
<span class="strategy-info" title="${s.description}">ⓘ</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
if (strategies.length > 0) {
|
||||
selectStrategy(strategies[0].id);
|
||||
}
|
||||
|
||||
document.getElementById('runSimBtn').disabled = false;
|
||||
}
|
||||
|
||||
export function selectStrategy(strategyId) {
|
||||
document.querySelectorAll('.strategy-item').forEach(item => {
|
||||
item.classList.toggle('selected', item.dataset.strategyId === strategyId);
|
||||
const radio = item.querySelector('input[type="radio"]');
|
||||
if (radio) radio.checked = item.dataset.strategyId === strategyId;
|
||||
});
|
||||
|
||||
setCurrentStrategy(strategyId);
|
||||
renderStrategyParams(strategyId);
|
||||
}
|
||||
|
||||
export function renderStrategyParams(strategyId) {
|
||||
const container = document.getElementById('strategyParams');
|
||||
const params = StrategyParams[strategyId] || [];
|
||||
|
||||
if (params.length === 0) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = params.map(param => `
|
||||
<div class="config-group">
|
||||
<label class="config-label">${param.label}</label>
|
||||
<input type="${param.type}"
|
||||
id="param_${param.name}"
|
||||
class="config-input"
|
||||
value="${param.default}"
|
||||
${param.min !== undefined ? `min="${param.min}"` : ''}
|
||||
${param.max !== undefined ? `max="${param.max}"` : ''}
|
||||
${param.step !== undefined ? `step="${param.step}"` : ''}
|
||||
>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('Error loading strategies:', error);
|
||||
|
||||
let errorMessage = error.message;
|
||||
if (error.name === 'AbortError') {
|
||||
errorMessage = 'Request timeout - API server not responding';
|
||||
} else if (error.message.includes('Failed to fetch')) {
|
||||
errorMessage = 'Cannot connect to API server - is it running?';
|
||||
}
|
||||
|
||||
document.getElementById('strategyList').innerHTML =
|
||||
`<div style="color: var(--tv-red); padding: 20px; text-align: center;">
|
||||
${errorMessage}<br>
|
||||
<small style="color: var(--tv-text-secondary);">Check console (F12) for details</small>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSimulation() {
|
||||
const results = getLastResults();
|
||||
if (!results) {
|
||||
alert('Please run a simulation first');
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultName = generateSimulationName(results.config);
|
||||
const name = prompt('Save simulation as:', defaultName);
|
||||
|
||||
if (!name || name.trim() === '') return;
|
||||
|
||||
const simulation = {
|
||||
name: name.trim(),
|
||||
config: results.config,
|
||||
results: {
|
||||
total_trades: results.total_trades,
|
||||
win_rate: results.win_rate,
|
||||
total_pnl: results.total_pnl,
|
||||
trades: results.trades,
|
||||
equity_curve: results.equity_curve
|
||||
}
|
||||
};
|
||||
|
||||
const id = window.SimulationStorage?.save(simulation);
|
||||
if (id) {
|
||||
renderSavedSimulations();
|
||||
alert('Simulation saved successfully!');
|
||||
} else {
|
||||
alert('Error saving simulation');
|
||||
}
|
||||
}
|
||||
|
||||
function generateSimulationName(config) {
|
||||
if (!config) return 'Unnamed Simulation';
|
||||
|
||||
const start = new Date(config.startDate);
|
||||
const now = new Date();
|
||||
const duration = now - start;
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
|
||||
let dateStr;
|
||||
if (duration < oneDay) {
|
||||
dateStr = start.toISOString().slice(0, 16).replace('T', ' ');
|
||||
} else {
|
||||
dateStr = start.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
return `${config.strategyName}_${config.timeframe}_${dateStr}`;
|
||||
}
|
||||
|
||||
export function renderSavedSimulations() {
|
||||
const container = document.getElementById('savedSimulations');
|
||||
const simulations = window.SimulationStorage?.getAll() || [];
|
||||
|
||||
if (simulations.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 10px; font-size: 12px;">No saved simulations</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = simulations.map(sim => `
|
||||
<div class="saved-sim-item">
|
||||
<span class="saved-sim-name" onclick="loadSavedSimulation('${sim.id}')" title="${sim.name}">
|
||||
${sim.name.length > 25 ? sim.name.slice(0, 25) + '...' : sim.name}
|
||||
</span>
|
||||
<div class="saved-sim-actions">
|
||||
<button class="sim-action-btn" onclick="loadSavedSimulation('${sim.id}')" title="Load">📂</button>
|
||||
<button class="sim-action-btn" onclick="exportSavedSimulation('${sim.id}')" title="Export">📥</button>
|
||||
<button class="sim-action-btn" onclick="deleteSavedSimulation('${sim.id}')" title="Delete">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
export function loadSavedSimulation(id) {
|
||||
const sim = window.SimulationStorage?.get(id);
|
||||
if (!sim) {
|
||||
alert('Simulation not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (sim.config) {
|
||||
document.getElementById('simSecondaryTF').value = sim.config.secondaryTimeframe || '';
|
||||
document.getElementById('simStartDate').value = sim.config.startDate || '';
|
||||
document.getElementById('simRiskPercent').value = sim.config.riskPercent || 2;
|
||||
document.getElementById('simStopLoss').value = sim.config.stopLossPercent || 2;
|
||||
|
||||
if (sim.config.strategyId) {
|
||||
selectStrategy(sim.config.strategyId);
|
||||
|
||||
if (sim.config.params) {
|
||||
Object.entries(sim.config.params).forEach(([key, value]) => {
|
||||
const input = document.getElementById(`param_${key}`);
|
||||
if (input) input.value = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLastResults(sim);
|
||||
displayEnhancedResults(sim.results);
|
||||
document.getElementById('resultsSection').style.display = 'block';
|
||||
}
|
||||
|
||||
export function deleteSavedSimulation(id) {
|
||||
if (!confirm('Are you sure you want to delete this simulation?')) return;
|
||||
|
||||
if (window.SimulationStorage?.delete(id)) {
|
||||
renderSavedSimulations();
|
||||
}
|
||||
}
|
||||
|
||||
function displayEnhancedResults(simulation) {
|
||||
const results = simulation.results || simulation;
|
||||
|
||||
document.getElementById('simTrades').textContent = results.total_trades || '0';
|
||||
document.getElementById('simWinRate').textContent = (results.win_rate || 0).toFixed(1) + '%';
|
||||
|
||||
const pnl = results.total_pnl || 0;
|
||||
const pnlElement = document.getElementById('simPnL');
|
||||
pnlElement.textContent = (pnl >= 0 ? '+' : '') + '$' + pnl.toFixed(2);
|
||||
pnlElement.style.color = pnl >= 0 ? '#4caf50' : '#f44336';
|
||||
|
||||
let grossProfit = 0;
|
||||
let grossLoss = 0;
|
||||
(results.trades || []).forEach(trade => {
|
||||
if (trade.pnl > 0) grossProfit += trade.pnl;
|
||||
else grossLoss += Math.abs(trade.pnl);
|
||||
});
|
||||
const profitFactor = grossLoss > 0 ? (grossProfit / grossLoss).toFixed(2) : grossProfit > 0 ? '∞' : '0';
|
||||
document.getElementById('simProfitFactor').textContent = profitFactor;
|
||||
|
||||
drawEquitySparkline(results);
|
||||
}
|
||||
|
||||
function drawEquitySparkline(results) {
|
||||
const container = document.getElementById('equitySparkline');
|
||||
if (!container || !results.trades || results.trades.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px; font-size: 11px;">No trades</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let equity = 1000;
|
||||
const equityData = [{ time: results.trades[0].entryTime, equity: equity }];
|
||||
|
||||
results.trades.forEach(trade => {
|
||||
equity += trade.pnl;
|
||||
equityData.push({ time: trade.exitTime, equity: equity });
|
||||
});
|
||||
|
||||
const sim = getLastResults();
|
||||
if (sim) {
|
||||
sim.equity_curve = equityData;
|
||||
}
|
||||
|
||||
container.innerHTML = '<canvas id="sparklineCanvas" width="300" height="60"></canvas>';
|
||||
const canvas = document.getElementById('sparklineCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const minEquity = Math.min(...equityData.map(d => d.equity));
|
||||
const maxEquity = Math.max(...equityData.map(d => d.equity));
|
||||
const range = maxEquity - minEquity || 1;
|
||||
|
||||
ctx.strokeStyle = equityData[equityData.length - 1].equity >= equityData[0].equity ? '#4caf50' : '#f44336';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
equityData.forEach((point, i) => {
|
||||
const x = (i / (equityData.length - 1)) * canvas.width;
|
||||
const y = canvas.height - ((point.equity - minEquity) / range) * canvas.height;
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#888';
|
||||
ctx.font = '9px sans-serif';
|
||||
ctx.fillText('$' + equityData[0].equity.toFixed(0), 2, canvas.height - 2);
|
||||
ctx.fillText('$' + equityData[equityData.length - 1].equity.toFixed(0), canvas.width - 30, 10);
|
||||
}
|
||||
|
||||
function getLastResults() {
|
||||
return window.lastSimulationResults;
|
||||
}
|
||||
|
||||
function setLastResults(results) {
|
||||
window.lastSimulationResults = results;
|
||||
}
|
||||
|
||||
window.selectStrategy = selectStrategy;
|
||||
window.loadSavedSimulation = loadSavedSimulation;
|
||||
window.deleteSavedSimulation = deleteSavedSimulation;
|
||||
window.renderSavedSimulations = renderSavedSimulations;
|
||||
Reference in New Issue
Block a user