Pre-refactor: commit before converting indicators to self-contained files

This commit is contained in:
DiTus
2026-03-01 19:37:07 +01:00
parent e457ce3e20
commit fdab0a3faa
22 changed files with 96 additions and 2726 deletions

View File

@ -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();

View File

@ -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 }
]
};

View File

@ -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';
}
}

View File

@ -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
};
}
}

View File

@ -1,3 +0,0 @@
export { StrategyParams } from './config.js';
export { RiskManager } from './risk-manager.js';
export { ClientStrategyEngine } from './engine.js';

View File

@ -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;
}
}

View File

@ -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;

View File

@ -1,5 +1,3 @@
import { HTSStrategyEngine } from '../strategies/hts-engine.js';
const HTS_COLORS = {
fastHigh: '#00bcd4',
fastLow: '#00bcd4',

View File

@ -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,

View File

@ -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>

View File

@ -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
});
}

View File

@ -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
};
}

View File

@ -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);
}
};

View File

@ -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;