Refactor dashboard JS to ES modules; fix collector strategy loading and add auto-backfill

- Split inline JS into separate ES module files (indicators/, strategies/, ui/, utils/)
- Fix brain.py strategy registry to use MAStrategy directly instead of missing modules
- Add auto-backfill for detected data gaps in collector monitoring loop
- Fix chart resize on sidebar toggle
- Fix chart scrollToTime -> setVisibleLogicalRange
This commit is contained in:
BTC Bot
2026-02-18 11:07:30 +01:00
parent eafba745b1
commit 30bedcbb67
31 changed files with 2516 additions and 2054 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js';
import { restoreSidebarState, toggleSidebar } 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 {
renderIndicatorList,
addNewIndicator,
selectIndicator,
applyIndicatorConfig,
removeIndicator,
removeIndicatorByIndex,
drawIndicatorsOnChart
} from './ui/indicators-panel.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.renderSavedSimulations = renderSavedSimulations;
window.loadSavedSimulation = loadSavedSimulation;
window.deleteSavedSimulation = deleteSavedSimulation;
window.clearSimulationResults = clearSimulationResults;
window.updateTimeframeDisplay = updateTimeframeDisplay;
window.StrategyParams = StrategyParams;
window.SimulationStorage = SimulationStorage;
window.IndicatorRegistry = IndicatorRegistry;
document.addEventListener('DOMContentLoaded', async () => {
window.dashboard = new TradingDashboard();
restoreSidebarState();
setDefaultStartDate();
updateTimeframeDisplay();
renderSavedSimulations();
await loadStrategies();
renderIndicatorList();
const originalSwitchTimeframe = window.dashboard.switchTimeframe.bind(window.dashboard);
window.dashboard.switchTimeframe = function(interval) {
originalSwitchTimeframe(interval);
setTimeout(() => drawIndicatorsOnChart(), 500);
};
});

View File

@ -0,0 +1,15 @@
export const INTERVALS = ['1m', '3m', '5m', '15m', '30m', '37m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M'];
export const COLORS = {
tvBg: '#131722',
tvPanelBg: '#1e222d',
tvBorder: '#2a2e39',
tvText: '#d1d4dc',
tvTextSecondary: '#787b86',
tvGreen: '#26a69a',
tvRed: '#ef5350',
tvBlue: '#2962ff',
tvHover: '#2a2e39'
};
export const API_BASE = '/api/v1';

View File

@ -0,0 +1 @@
export { INTERVALS, COLORS, API_BASE } from './constants.js';

View File

@ -0,0 +1,36 @@
import { BaseIndicator } from './base.js';
export class ATRIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 14;
const results = new Array(candles.length).fill(null);
const tr = new Array(candles.length).fill(0);
for (let i = 1; i < candles.length; i++) {
const h_l = candles[i].high - candles[i].low;
const h_pc = Math.abs(candles[i].high - candles[i-1].close);
const l_pc = Math.abs(candles[i].low - candles[i-1].close);
tr[i] = Math.max(h_l, h_pc, l_pc);
}
let atr = 0;
let sum = 0;
for (let i = 1; i <= period; i++) sum += tr[i];
atr = sum / period;
results[period] = atr;
for (let i = period + 1; i < candles.length; i++) {
atr = (atr * (period - 1) + tr[i]) / period;
results[i] = atr;
}
return results;
}
getMetadata() {
return {
name: 'ATR',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
plots: [{ id: 'value', color: '#795548', title: 'ATR' }]
};
}
}

View File

@ -0,0 +1,17 @@
export class BaseIndicator {
constructor(config) {
this.name = config.name;
this.type = config.type;
this.params = config.params || {};
this.timeframe = config.timeframe || '1m';
}
calculate(candles) { throw new Error("Not implemented"); }
getMetadata() {
return {
name: this.name,
inputs: [],
plots: []
};
}
}

View File

@ -0,0 +1,41 @@
import { BaseIndicator } from './base.js';
export class BollingerBandsIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 20;
const stdDevMult = this.params.stdDev || 2;
const results = new Array(candles.length).fill(null);
for (let i = period - 1; i < candles.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) sum += candles[i-j].close;
const sma = sum / period;
let diffSum = 0;
for (let j = 0; j < period; j++) diffSum += Math.pow(candles[i-j].close - sma, 2);
const stdDev = Math.sqrt(diffSum / period);
results[i] = {
middle: sma,
upper: sma + (stdDevMult * stdDev),
lower: sma - (stdDevMult * stdDev)
};
}
return results;
}
getMetadata() {
return {
name: 'Bollinger Bands',
inputs: [
{ name: 'period', label: 'Period', type: 'number', default: 20, min: 1, max: 100 },
{ name: 'stdDev', label: 'Std Dev', type: 'number', default: 2, min: 0.5, max: 5, step: 0.5 }
],
plots: [
{ id: 'upper', color: '#4caf50', title: 'Upper' },
{ id: 'middle', color: '#4caf50', title: 'Middle', lineStyle: 2 },
{ id: 'lower', color: '#4caf50', title: 'Lower' }
]
};
}
}

View File

@ -0,0 +1,17 @@
import { MA } from './ma.js';
import { BaseIndicator } from './base.js';
export class EMAIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 44;
return MA.ema(candles, period, 'close');
}
getMetadata() {
return {
name: 'EMA',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }],
plots: [{ id: 'value', color: '#ff9800', title: 'EMA' }]
};
}
}

View File

@ -0,0 +1,40 @@
import { MA } from './ma.js';
import { BaseIndicator } from './base.js';
export class HTSIndicator extends BaseIndicator {
calculate(candles) {
const shortPeriod = this.params.short || 33;
const longPeriod = this.params.long || 144;
const maType = this.params.maType || 'RMA';
const shortHigh = MA.get(maType, candles, shortPeriod, 'high');
const shortLow = MA.get(maType, candles, shortPeriod, 'low');
const longHigh = MA.get(maType, candles, longPeriod, 'high');
const longLow = MA.get(maType, candles, longPeriod, 'low');
return candles.map((_, i) => ({
fastHigh: shortHigh[i],
fastLow: shortLow[i],
slowHigh: longHigh[i],
slowLow: longLow[i]
}));
}
getMetadata() {
return {
name: 'HTS Trend System',
description: 'High/Low Trend System with Fast and Slow MAs',
inputs: [
{ name: 'short', label: 'Fast Period', type: 'number', default: 33, min: 1, max: 500 },
{ name: 'long', label: 'Slow Period', type: 'number', default: 144, min: 1, max: 500 },
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'RMA' }
],
plots: [
{ id: 'fastHigh', color: '#00bcd4', title: 'Fast High', width: 1 },
{ id: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: 1 },
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: 2 },
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', width: 2 }
]
};
}
}

View File

@ -0,0 +1,30 @@
export { MA } from './ma.js';
export { BaseIndicator } from './base.js';
export { HTSIndicator } from './hts.js';
export { SMAIndicator } from './sma.js';
export { EMAIndicator } from './ema.js';
export { RSIIndicator } from './rsi.js';
export { BollingerBandsIndicator } from './bb.js';
export { MACDIndicator } from './macd.js';
export { StochasticIndicator } from './stoch.js';
export { ATRIndicator } from './atr.js';
import { HTSIndicator } from './hts.js';
import { SMAIndicator } from './sma.js';
import { EMAIndicator } from './ema.js';
import { RSIIndicator } from './rsi.js';
import { BollingerBandsIndicator } from './bb.js';
import { MACDIndicator } from './macd.js';
import { StochasticIndicator } from './stoch.js';
import { ATRIndicator } from './atr.js';
export const IndicatorRegistry = {
hts: HTSIndicator,
sma: SMAIndicator,
ema: EMAIndicator,
rsi: RSIIndicator,
bb: BollingerBandsIndicator,
macd: MACDIndicator,
stoch: StochasticIndicator,
atr: ATRIndicator
};

View File

@ -0,0 +1,93 @@
export class MA {
static get(type, candles, period, source = 'close') {
switch (type.toUpperCase()) {
case 'SMA': return MA.sma(candles, period, source);
case 'EMA': return MA.ema(candles, period, source);
case 'RMA': return MA.rma(candles, period, source);
case 'WMA': return MA.wma(candles, period, source);
case 'VWMA': return MA.vwma(candles, period, source);
default: return MA.sma(candles, period, source);
}
}
static sma(candles, period, source = 'close') {
const results = new Array(candles.length).fill(null);
let sum = 0;
for (let i = 0; i < candles.length; i++) {
sum += candles[i][source];
if (i >= period) sum -= candles[i - period][source];
if (i >= period - 1) results[i] = sum / period;
}
return results;
}
static ema(candles, period, source = 'close') {
const multiplier = 2 / (period + 1);
const results = new Array(candles.length).fill(null);
let ema = 0;
let sum = 0;
for (let i = 0; i < candles.length; i++) {
if (i < period) {
sum += candles[i][source];
if (i === period - 1) {
ema = sum / period;
results[i] = ema;
}
} else {
ema = (candles[i][source] - ema) * multiplier + ema;
results[i] = ema;
}
}
return results;
}
static rma(candles, period, source = 'close') {
const multiplier = 1 / period;
const results = new Array(candles.length).fill(null);
let rma = 0;
let sum = 0;
for (let i = 0; i < candles.length; i++) {
if (i < period) {
sum += candles[i][source];
if (i === period - 1) {
rma = sum / period;
results[i] = rma;
}
} else {
rma = (candles[i][source] - rma) * multiplier + rma;
results[i] = rma;
}
}
return results;
}
static wma(candles, period, source = 'close') {
const results = new Array(candles.length).fill(null);
const weightSum = (period * (period + 1)) / 2;
for (let i = period - 1; i < candles.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) {
sum += candles[i - j][source] * (period - j);
}
results[i] = sum / weightSum;
}
return results;
}
static vwma(candles, period, source = 'close') {
const results = new Array(candles.length).fill(null);
for (let i = period - 1; i < candles.length; i++) {
let sumPV = 0;
let sumV = 0;
for (let j = 0; j < period; j++) {
sumPV += candles[i - j][source] * candles[i - j].volume;
sumV += candles[i - j].volume;
}
results[i] = sumV !== 0 ? sumPV / sumV : null;
}
return results;
}
}

View File

@ -0,0 +1,58 @@
import { MA } from './ma.js';
import { BaseIndicator } from './base.js';
export class MACDIndicator extends BaseIndicator {
calculate(candles) {
const fast = this.params.fast || 12;
const slow = this.params.slow || 26;
const signal = this.params.signal || 9;
const fastEma = MA.ema(candles, fast, 'close');
const slowEma = MA.ema(candles, slow, 'close');
const macdLine = fastEma.map((f, i) => (f !== null && slowEma[i] !== null) ? f - slowEma[i] : null);
const signalLine = new Array(candles.length).fill(null);
const multiplier = 2 / (signal + 1);
let ema = 0;
let sum = 0;
let count = 0;
for (let i = 0; i < macdLine.length; i++) {
if (macdLine[i] === null) continue;
count++;
if (count < signal) {
sum += macdLine[i];
} else if (count === signal) {
sum += macdLine[i];
ema = sum / signal;
signalLine[i] = ema;
} else {
ema = (macdLine[i] - ema) * multiplier + ema;
signalLine[i] = ema;
}
}
return macdLine.map((m, i) => ({
macd: m,
signal: signalLine[i],
histogram: (m !== null && signalLine[i] !== null) ? m - signalLine[i] : null
}));
}
getMetadata() {
return {
name: 'MACD',
inputs: [
{ name: 'fast', label: 'Fast Period', type: 'number', default: 12 },
{ name: 'slow', label: 'Slow Period', type: 'number', default: 26 },
{ name: 'signal', label: 'Signal Period', type: 'number', default: 9 }
],
plots: [
{ id: 'macd', color: '#2196f3', title: 'MACD' },
{ id: 'signal', color: '#ff5722', title: 'Signal' },
{ id: 'histogram', color: '#607d8b', title: 'Histogram' }
]
};
}
}

View File

@ -0,0 +1,37 @@
import { BaseIndicator } from './base.js';
export class RSIIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 14;
const results = new Array(candles.length).fill(null);
let gains = 0, losses = 0;
for (let i = 1; i < candles.length; i++) {
const diff = candles[i].close - candles[i-1].close;
const gain = diff > 0 ? diff : 0;
const loss = diff < 0 ? -diff : 0;
if (i <= period) {
gains += gain;
losses += loss;
if (i === period) {
let avgGain = gains / period;
let avgLoss = losses / period;
results[i] = 100 - (100 / (1 + (avgGain / (avgLoss || 0.00001))));
}
} else {
const lastAvgGain = (results[i-1] ? (results[i-1] > 0 ? (period-1) * (results[i-1] * (gains+losses)/100) : 0) : 0);
gains = (gains * (period - 1) + gain) / period;
losses = (losses * (period - 1) + loss) / period;
results[i] = 100 - (100 / (1 + (gains / (losses || 0.00001))));
}
}
return results;
}
getMetadata() {
return {
name: 'RSI',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
plots: [{ id: 'value', color: '#9c27b0', title: 'RSI' }]
};
}
}

View File

@ -0,0 +1,17 @@
import { MA } from './ma.js';
import { BaseIndicator } from './base.js';
export class SMAIndicator extends BaseIndicator {
calculate(candles) {
const period = this.params.period || 44;
return MA.sma(candles, period, 'close');
}
getMetadata() {
return {
name: 'SMA',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }],
plots: [{ id: 'value', color: '#2962ff', title: 'SMA' }]
};
}
}

View File

@ -0,0 +1,44 @@
import { BaseIndicator } from './base.js';
export class StochasticIndicator extends BaseIndicator {
calculate(candles) {
const kPeriod = this.params.kPeriod || 14;
const dPeriod = this.params.dPeriod || 3;
const results = new Array(candles.length).fill(null);
const kValues = new Array(candles.length).fill(null);
for (let i = kPeriod - 1; i < candles.length; i++) {
let lowest = Infinity;
let highest = -Infinity;
for (let j = 0; j < kPeriod; j++) {
lowest = Math.min(lowest, candles[i-j].low);
highest = Math.max(highest, candles[i-j].high);
}
const diff = highest - lowest;
kValues[i] = diff === 0 ? 50 : ((candles[i].close - lowest) / diff) * 100;
}
for (let i = kPeriod + dPeriod - 2; i < candles.length; i++) {
let sum = 0;
for (let j = 0; j < dPeriod; j++) sum += kValues[i-j];
results[i] = { k: kValues[i], d: sum / dPeriod };
}
return results;
}
getMetadata() {
return {
name: 'Stochastic',
inputs: [
{ name: 'kPeriod', label: 'K Period', type: 'number', default: 14 },
{ name: 'dPeriod', label: 'D Period', type: 'number', default: 3 }
],
plots: [
{ id: 'k', color: '#3f51b5', title: '%K' },
{ id: 'd', color: '#ff9800', title: '%D' }
]
};
}
}

View File

@ -0,0 +1,16 @@
export const StrategyParams = {
ma_trend: [
{ name: 'period', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 }
]
};
export const AVAILABLE_INDICATORS = [
{ type: 'hts', name: 'HTS Trend System', description: 'Fast/Slow MAs of High/Low prices' },
{ type: 'sma', name: 'SMA', description: 'Simple Moving Average' },
{ type: 'ema', name: 'EMA', description: 'Exponential Moving Average' },
{ type: 'rsi', name: 'RSI', description: 'Relative Strength Index' },
{ type: 'bb', name: 'Bollinger Bands', description: 'Volatility bands' },
{ type: 'macd', name: 'MACD', description: 'Moving Average Convergence Divergence' },
{ type: 'stoch', name: 'Stochastic', description: 'Stochastic Oscillator' },
{ type: 'atr', name: 'ATR', description: 'Average True Range' }
];

View File

@ -0,0 +1,167 @@
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

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

View File

@ -0,0 +1,17 @@
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

@ -0,0 +1,548 @@
import { INTERVALS, COLORS } from '../core/index.js';
export class TradingDashboard {
constructor() {
this.chart = null;
this.candleSeries = null;
this.currentInterval = '1d';
this.intervals = INTERVALS;
this.allData = new Map();
this.isLoading = false;
this.hasInitialLoad = false;
this.taData = null;
this.init();
}
init() {
this.createTimeframeButtons();
this.initChart();
this.initEventListeners();
this.loadInitialData();
setInterval(() => {
this.loadNewData();
if (new Date().getSeconds() < 15) this.loadTA();
}, 10000);
}
isAtRightEdge() {
const timeScale = this.chart.timeScale();
const visibleRange = timeScale.getVisibleLogicalRange();
if (!visibleRange) return true;
const data = this.candleSeries.data();
if (!data || data.length === 0) return true;
return visibleRange.to >= data.length - 5;
}
createTimeframeButtons() {
const container = document.getElementById('timeframeContainer');
container.innerHTML = '';
this.intervals.forEach(interval => {
const btn = document.createElement('button');
btn.className = 'timeframe-btn';
btn.dataset.interval = interval;
btn.textContent = interval;
if (interval === this.currentInterval) {
btn.classList.add('active');
}
btn.addEventListener('click', () => this.switchTimeframe(interval));
container.appendChild(btn);
});
}
initChart() {
const chartContainer = document.getElementById('chart');
this.chart = LightweightCharts.createChart(chartContainer, {
layout: {
background: { color: COLORS.tvBg },
textColor: COLORS.tvText,
},
grid: {
vertLines: { color: COLORS.tvBorder },
horzLines: { color: COLORS.tvBorder },
},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
},
rightPriceScale: {
borderColor: COLORS.tvBorder,
autoScale: true,
},
timeScale: {
borderColor: COLORS.tvBorder,
timeVisible: true,
secondsVisible: false,
rightOffset: 12,
barSpacing: 10,
},
handleScroll: {
vertTouchDrag: false,
},
});
this.candleSeries = this.chart.addCandlestickSeries({
upColor: '#ff9800',
downColor: '#ff9800',
borderUpColor: '#ff9800',
borderDownColor: '#ff9800',
wickUpColor: '#ff9800',
wickDownColor: '#ff9800',
});
this.initPriceScaleControls();
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
window.addEventListener('resize', () => {
this.chart.applyOptions({
width: chartContainer.clientWidth,
height: chartContainer.clientHeight,
});
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.loadNewData();
this.loadTA();
}
});
window.addEventListener('focus', () => {
this.loadNewData();
this.loadTA();
});
}
initPriceScaleControls() {
const btnAutoScale = document.getElementById('btnAutoScale');
const btnLogScale = document.getElementById('btnLogScale');
if (!btnAutoScale || !btnLogScale) return;
this.priceScaleState = {
autoScale: true,
logScale: false
};
btnAutoScale.addEventListener('click', () => {
this.priceScaleState.autoScale = !this.priceScaleState.autoScale;
btnAutoScale.classList.toggle('active', this.priceScaleState.autoScale);
this.candleSeries.priceScale().applyOptions({
autoScale: this.priceScaleState.autoScale
});
console.log('Auto Scale:', this.priceScaleState.autoScale ? 'ON' : 'OFF');
});
btnLogScale.addEventListener('click', () => {
this.priceScaleState.logScale = !this.priceScaleState.logScale;
btnLogScale.classList.toggle('active', this.priceScaleState.logScale);
let currentPriceRange = null;
let currentTimeRange = null;
if (!this.priceScaleState.autoScale) {
try {
currentPriceRange = this.candleSeries.priceScale().getVisiblePriceRange();
} catch (e) {
console.log('Could not get price range');
}
}
try {
currentTimeRange = this.chart.timeScale().getVisibleLogicalRange();
} catch (e) {
console.log('Could not get time range');
}
this.candleSeries.priceScale().applyOptions({
mode: this.priceScaleState.logScale ? LightweightCharts.PriceScaleMode.Logarithmic : LightweightCharts.PriceScaleMode.Normal
});
this.chart.applyOptions({});
setTimeout(() => {
if (currentTimeRange) {
try {
this.chart.timeScale().setVisibleLogicalRange(currentTimeRange);
} catch (e) {
console.log('Could not restore time range');
}
}
if (!this.priceScaleState.autoScale && currentPriceRange) {
try {
this.candleSeries.priceScale().setVisiblePriceRange(currentPriceRange);
} catch (e) {
console.log('Could not restore price range');
}
}
}, 100);
console.log('Log Scale:', this.priceScaleState.logScale ? 'ON' : 'OFF');
});
document.addEventListener('keydown', (e) => {
if (e.key === 'a' || e.key === 'A') {
if (e.target.tagName !== 'INPUT') {
btnAutoScale.click();
}
}
});
}
initEventListeners() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
const shortcuts = {
'1': '1m', '2': '3m', '3': '5m', '4': '15m', '5': '30m', '7': '37m',
'6': '1h', '8': '4h', '9': '8h', '0': '12h',
'd': '1d', 'D': '1d', 'w': '1w', 'W': '1w', 'm': '1M', 'M': '1M'
};
if (shortcuts[e.key]) {
this.switchTimeframe(shortcuts[e.key]);
}
});
}
async loadInitialData() {
await this.loadData(1000, true);
this.hasInitialLoad = true;
}
async loadData(limit = 1000, fitToContent = false) {
if (this.isLoading) return;
this.isLoading = true;
try {
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=${limit}`);
const data = await response.json();
if (data.candles && data.candles.length > 0) {
const chartData = data.candles.reverse().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 existingData = this.allData.get(this.currentInterval) || [];
const mergedData = this.mergeData(existingData, chartData);
this.allData.set(this.currentInterval, mergedData);
this.candleSeries.setData(mergedData);
if (fitToContent) {
this.chart.timeScale().fitContent();
} else if (visibleRange) {
this.chart.timeScale().setVisibleLogicalRange(visibleRange);
}
const latest = mergedData[mergedData.length - 1];
this.updateStats(latest);
}
} catch (error) {
console.error('Error loading data:', error);
} finally {
this.isLoading = false;
}
}
async loadNewData() {
if (!this.hasInitialLoad || this.isLoading) return;
try {
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`);
const data = await response.json();
if (data.candles && data.candles.length > 0) {
const atEdge = this.isAtRightEdge();
const currentSeriesData = this.candleSeries.data();
const lastTimestamp = currentSeriesData.length > 0
? currentSeriesData[currentSeriesData.length - 1].time
: 0;
const chartData = data.candles.reverse().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)
}));
chartData.forEach(candle => {
if (candle.time >= lastTimestamp) {
this.candleSeries.update(candle);
}
});
const existingData = this.allData.get(this.currentInterval) || [];
this.allData.set(this.currentInterval, this.mergeData(existingData, chartData));
if (atEdge) {
this.chart.timeScale().scrollToRealTime();
}
const latest = chartData[chartData.length - 1];
this.updateStats(latest);
}
} catch (error) {
console.error('Error loading new data:', error);
}
}
mergeData(existing, newData) {
const dataMap = new Map();
existing.forEach(c => dataMap.set(c.time, c));
newData.forEach(c => dataMap.set(c.time, c));
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
}
onVisibleRangeChange() {
if (!this.hasInitialLoad || this.isLoading) {
console.log('Skipping range change:', { hasInitialLoad: this.hasInitialLoad, isLoading: this.isLoading });
return;
}
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
if (!visibleRange) {
console.log('No visible range');
return;
}
const data = this.candleSeries.data();
if (!data || data.length === 0) {
console.log('No data available');
return;
}
const barsFromLeft = visibleRange.from;
const totalBars = data.length;
console.log('Visible range:', { from: visibleRange.from, to: visibleRange.to, barsFromLeft, totalBars });
if (barsFromLeft < 50) {
console.log('Near left edge (within 50 bars), loading historical data...');
const oldestCandle = data[0];
if (oldestCandle) {
this.loadHistoricalData(oldestCandle.time);
}
}
}
async loadHistoricalData(beforeTime) {
if (this.isLoading) {
console.log('Already loading, skipping...');
return;
}
this.isLoading = true;
console.log(`Loading historical data for ${this.currentInterval} before ${beforeTime}`);
const currentData = this.candleSeries.data();
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
let leftmostTime = null;
let savedPriceRange = null;
const isAutoScale = this.priceScaleState?.autoScale !== false;
if (!isAutoScale) {
try {
savedPriceRange = this.candleSeries.priceScale().getVisiblePriceRange();
console.log('Saving price range:', savedPriceRange);
} catch (e) {
console.log('Could not save price range');
}
}
if (visibleRange && currentData.length > 0) {
const leftmostIndex = Math.floor(visibleRange.from);
if (leftmostIndex >= 0 && leftmostIndex < currentData.length) {
leftmostTime = currentData[leftmostIndex].time;
}
}
try {
const endTime = new Date((beforeTime - 1) * 1000);
console.log(`Loading historical data before ${new Date((beforeTime - 1) * 1000).toISOString()}`);
const response = await fetch(
`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&end=${endTime.toISOString()}&limit=500`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.candles && data.candles.length > 0) {
const chartData = data.candles.reverse().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 existingData = this.allData.get(this.currentInterval) || [];
const mergedData = this.mergeData(existingData, chartData);
this.allData.set(this.currentInterval, mergedData);
this.candleSeries.setData(mergedData);
if (leftmostTime) {
const idx = mergedData.findIndex(c => c.time === leftmostTime);
if (idx >= 0) {
this.chart.timeScale().setVisibleLogicalRange({ from: idx, to: idx + 50 });
}
}
if (!isAutoScale && savedPriceRange) {
setTimeout(() => {
try {
this.candleSeries.priceScale().setVisiblePriceRange(savedPriceRange);
console.log('Restored price range:', savedPriceRange);
} catch (e) {
console.log('Could not restore price range');
}
}, 50);
}
console.log(`Loaded ${chartData.length} historical candles`);
} else {
console.log('No more historical data available');
}
} catch (error) {
console.error('Error loading historical data:', error);
} finally {
this.isLoading = false;
}
}
async loadTA() {
try {
const response = await fetch(`/api/v1/ta?symbol=BTC&interval=${this.currentInterval}`);
this.taData = await response.json();
this.renderTA();
} catch (error) {
console.error('Error loading TA:', error);
document.getElementById('taContent').innerHTML = '<div class="ta-error">Failed to load technical analysis</div>';
}
}
renderTA() {
if (!this.taData || this.taData.error) {
document.getElementById('taContent').innerHTML = `<div class="ta-error">${this.taData?.error || 'No data available'}</div>`;
return;
}
const data = this.taData;
const trendClass = data.trend.direction.toLowerCase();
const signalClass = data.trend.signal.toLowerCase();
const ma44Change = data.moving_averages.price_vs_ma44;
const ma125Change = data.moving_averages.price_vs_ma125;
document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase();
document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString();
document.getElementById('taContent').innerHTML = `
<div class="ta-section">
<div class="ta-section-title">Trend Analysis</div>
<div class="ta-trend ${trendClass}">
${data.trend.direction} ${trendClass === 'bullish' ? '↑' : trendClass === 'bearish' ? '↓' : '→'}
</div>
<div class="ta-strength">${data.trend.strength}</div>
<span class="ta-signal ${signalClass}">${data.trend.signal}</span>
</div>
<div class="ta-section">
<div class="ta-section-title">Moving Averages</div>
<div class="ta-ma-row">
<span class="ta-ma-label">MA 44</span>
<span class="ta-ma-value">
${data.moving_averages.ma_44 ? data.moving_averages.ma_44.toFixed(2) : 'N/A'}
${ma44Change !== null ? `<span class="ta-ma-change ${ma44Change >= 0 ? 'positive' : 'negative'}">${ma44Change >= 0 ? '+' : ''}${ma44Change.toFixed(1)}%</span>` : ''}
</span>
</div>
<div class="ta-ma-row">
<span class="ta-ma-label">MA 125</span>
<span class="ta-ma-value">
${data.moving_averages.ma_125 ? data.moving_averages.ma_125.toFixed(2) : 'N/A'}
${ma125Change !== null ? `<span class="ta-ma-change ${ma125Change >= 0 ? 'positive' : 'negative'}">${ma125Change >= 0 ? '+' : ''}${ma125Change.toFixed(1)}%</span>` : ''}
</span>
</div>
</div>
<div class="ta-section">
<div class="ta-section-title">Indicators</div>
<div id="indicatorList" style="display: flex; flex-direction: column; gap: 6px; margin-top: 8px;"></div>
<button class="ta-btn" onclick="addNewIndicator()" style="width: 100%; margin-top: 12px; font-size: 11px;">
+ Add Indicator
</button>
</div>
<div class="ta-section" id="indicatorConfigPanel">
<div class="ta-section-title">Configuration</div>
<div id="configForm" style="margin-top: 8px;"></div>
<div style="display: flex; gap: 8px; margin-top: 12px;">
<button class="ta-btn" onclick="applyIndicatorConfig()" style="flex: 1; font-size: 11px; background: var(--tv-blue); color: white; border: none;">Apply</button>
<button class="ta-btn" onclick="removeIndicator()" style="flex: 1; font-size: 11px; border-color: var(--tv-red); color: var(--tv-red);">Remove</button>
</div>
</div>
`;
}
updateStats(candle) {
const price = candle.close;
const change = ((price - candle.open) / candle.open * 100);
document.getElementById('currentPrice').textContent = price.toFixed(2);
document.getElementById('currentPrice').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
document.getElementById('priceChange').textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
document.getElementById('priceChange').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
document.getElementById('dailyHigh').textContent = candle.high.toFixed(2);
document.getElementById('dailyLow').textContent = candle.low.toFixed(2);
}
switchTimeframe(interval) {
if (!this.intervals.includes(interval) || interval === this.currentInterval) return;
this.currentInterval = interval;
this.hasInitialLoad = false;
document.querySelectorAll('.timeframe-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.interval === interval);
});
this.allData.delete(interval);
this.loadInitialData();
this.loadTA();
window.clearSimulationResults?.();
window.updateTimeframeDisplay?.();
}
}
export function refreshTA() {
if (window.dashboard) {
window.dashboard.loadTA();
}
}
export function openAIAnalysis() {
const symbol = 'BTC';
const interval = window.dashboard?.currentInterval || '1d';
const prompt = `Analyze Bitcoin (${symbol}) ${interval} chart. Current trend, support/resistance levels, and trading recommendation. Technical indicators: MA44, MA125.`;
const geminiUrl = `https://gemini.google.com/app?prompt=${encodeURIComponent(prompt)}`;
window.open(geminiUrl, '_blank');
}

View File

@ -0,0 +1,140 @@
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

@ -0,0 +1,37 @@
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,
selectIndicator,
renderIndicatorConfig,
applyIndicatorConfig,
removeIndicator,
removeIndicatorByIndex,
drawIndicatorsOnChart,
getActiveIndicators,
setActiveIndicators
} from './indicators-panel.js';

View File

@ -0,0 +1,202 @@
import { AVAILABLE_INDICATORS } from '../strategies/config.js';
import { IndicatorRegistry as IR } from '../indicators/index.js';
let activeIndicators = [];
let selectedIndicatorIndex = -1;
export function getActiveIndicators() {
return activeIndicators;
}
export function setActiveIndicators(indicators) {
activeIndicators = indicators;
}
export function renderIndicatorList() {
const container = document.getElementById('indicatorList');
if (!container) return;
if (activeIndicators.length === 0) {
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 10px; font-size: 12px;">No indicators added</div>';
return;
}
container.innerHTML = activeIndicators.map((ind, idx) => `
<div class="indicator-item ${idx === selectedIndicatorIndex ? 'selected' : ''}"
onclick="selectIndicator(${idx})"
style="display: flex; align-items: center; justify-content: space-between; padding: 8px; background: var(--tv-bg); border-radius: 4px; cursor: pointer; border: 1px solid ${idx === selectedIndicatorIndex ? 'var(--tv-blue)' : 'var(--tv-border)'}">
<div>
<div style="font-size: 12px; font-weight: 600;">${ind.name}</div>
<div style="font-size: 10px; color: var(--tv-text-secondary);">${ind.params.short || ind.params.period || ind.params.fast || 'N/A'}</div>
</div>
<button onclick="event.stopPropagation(); removeIndicatorByIndex(${idx})" style="background: none; border: none; color: var(--tv-red); cursor: pointer; font-size: 14px;">×</button>
</div>
`).join('');
const configPanel = document.getElementById('indicatorConfigPanel');
if (selectedIndicatorIndex >= 0) {
configPanel.style.display = 'block';
renderIndicatorConfig(selectedIndicatorIndex);
} else {
configPanel.style.display = 'none';
}
}
export function addNewIndicator() {
const type = prompt('Enter indicator type:\n' + AVAILABLE_INDICATORS.map(i => `${i.type}: ${i.name}`).join('\n'));
if (!type) return;
const indicatorDef = AVAILABLE_INDICATORS.find(i => i.type === type.toLowerCase());
if (!indicatorDef) {
alert('Unknown indicator type');
return;
}
const IndicatorClass = IR?.[type.toLowerCase()];
if (!IndicatorClass) {
alert('Indicator class not found');
return;
}
const instance = new IndicatorClass({ type: type.toLowerCase(), params: {}, name: indicatorDef.name });
const metadata = instance.getMetadata();
const params = {};
metadata.inputs.forEach(input => {
params[input.name] = input.default;
});
activeIndicators.push({
type: type.toLowerCase(),
name: metadata.name,
params: params,
plots: metadata.plots,
series: []
});
selectedIndicatorIndex = activeIndicators.length - 1;
renderIndicatorList();
drawIndicatorsOnChart();
}
export function selectIndicator(index) {
selectedIndicatorIndex = index;
renderIndicatorList();
}
export function renderIndicatorConfig(index) {
const container = document.getElementById('configForm');
const indicator = activeIndicators[index];
if (!indicator || !indicator.plots) {
container.innerHTML = '';
return;
}
const IndicatorClass = IR?.[indicator.type];
if (!IndicatorClass) return;
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
const meta = instance.getMetadata();
container.innerHTML = meta.inputs.map(input => `
<div style="margin-bottom: 8px;">
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
${input.type === 'select' ?
`<select id="config_${input.name}" class="sim-input" style="font-size: 12px; padding: 6px;">${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}</select>` :
`<input type="number" id="config_${input.name}" class="sim-input" value="${indicator.params[input.name]}" ${input.min !== undefined ? `min="${input.min}"` : ''} ${input.max !== undefined ? `max="${input.max}"` : ''} style="font-size: 12px; padding: 6px;">`
}
</div>
`).join('');
}
export function applyIndicatorConfig() {
if (selectedIndicatorIndex < 0) return;
const indicator = activeIndicators[selectedIndicatorIndex];
const IndicatorClass = IR?.[indicator.type];
if (!IndicatorClass) return;
const instance = new IndicatorClass({ type: indicator.type, params: {}, name: indicator.name });
const meta = instance.getMetadata();
meta.inputs.forEach(input => {
const el = document.getElementById(`config_${input.name}`);
if (el) {
indicator.params[input.name] = input.type === 'select' ? el.value : parseFloat(el.value);
}
});
renderIndicatorList();
drawIndicatorsOnChart();
}
export function removeIndicator() {
if (selectedIndicatorIndex < 0) return;
activeIndicators.splice(selectedIndicatorIndex, 1);
selectedIndicatorIndex = -1;
renderIndicatorList();
drawIndicatorsOnChart();
}
export function removeIndicatorByIndex(index) {
activeIndicators.splice(index, 1);
if (selectedIndicatorIndex >= activeIndicators.length) {
selectedIndicatorIndex = activeIndicators.length - 1;
}
renderIndicatorList();
drawIndicatorsOnChart();
}
export function drawIndicatorsOnChart() {
if (!window.dashboard || !window.dashboard.chart) return;
activeIndicators.forEach(ind => {
ind.series?.forEach(s => {
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
});
});
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
if (!candles || candles.length === 0) return;
activeIndicators.forEach((indicator, idx) => {
const IndicatorClass = IR?.[indicator.type];
if (!IndicatorClass) return;
const instance = new IndicatorClass({
type: indicator.type,
params: indicator.params,
name: indicator.name
});
const results = instance.calculate(candles);
const meta = instance.getMetadata();
indicator.series = [];
meta.plots.forEach(plot => {
if (results[0] && typeof results[0][plot.id] === 'undefined') return;
const lineSeries = window.dashboard.chart.addLineSeries({
color: plot.color || '#2962ff',
lineWidth: plot.width || 1,
title: plot.title,
priceLineVisible: false,
lastValueVisible: true
});
const data = candles.map((c, i) => ({
time: c.time,
value: results[i]?.[plot.id] ?? null
})).filter(d => d.value !== null && d.value !== undefined);
lineSeries.setData(data);
indicator.series.push(lineSeries);
});
});
}
window.addNewIndicator = addNewIndicator;
window.selectIndicator = selectIndicator;
window.applyIndicatorConfig = applyIndicatorConfig;
window.removeIndicator = removeIndicator;
window.removeIndicatorByIndex = removeIndicatorByIndex;

View File

@ -0,0 +1,23 @@
export function toggleSidebar() {
const sidebar = document.getElementById('rightSidebar');
sidebar.classList.toggle('collapsed');
localStorage.setItem('sidebar_collapsed', sidebar.classList.contains('collapsed'));
// Resize chart after sidebar toggle
setTimeout(() => {
if (window.dashboard && window.dashboard.chart) {
const container = document.getElementById('chart');
window.dashboard.chart.applyOptions({
width: container.clientWidth,
height: container.clientHeight
});
}
}, 350); // Wait for CSS transition
}
export function restoreSidebarState() {
const collapsed = localStorage.getItem('sidebar_collapsed') === 'true';
if (collapsed) {
document.getElementById('rightSidebar').classList.add('collapsed');
}
}

View File

@ -0,0 +1,377 @@
import { ClientStrategyEngine } from '../strategies/index.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));
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();
const 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();
} 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 = [];
export function showSimulationMarkers() {
const results = getLastResults();
if (!results || !window.dashboard) return;
const trades = results.trades || results.results?.trades || [];
const markers = [];
clearSimulationMarkers();
console.log('Plotting trades:', trades.length);
trades.forEach((trade, i) => {
let entryTime, exitTime;
if (typeof trade.entryTime === 'number') {
entryTime = trade.entryTime;
} else {
entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000);
}
if (typeof trade.exitTime === 'number') {
exitTime = trade.exitTime;
} else {
exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000);
}
const pnlSymbol = trade.pnl > 0 ? '+' : '';
markers.push({
time: entryTime,
position: 'belowBar',
color: '#2196f3',
shape: 'arrowUp',
text: 'BUY',
size: 1
});
markers.push({
time: exitTime,
position: 'aboveBar',
color: trade.pnl > 0 ? '#4caf50' : '#f44336',
shape: 'arrowDown',
text: `SELL ${pnlSymbol}${trade.pnlPct.toFixed(1)}%`,
size: 1
});
const lineSeries = window.dashboard.chart.addLineSeries({
color: '#2196f3',
lineWidth: 1,
lastValueVisible: false,
title: '',
priceLineVisible: false,
crosshairMarkerVisible: false
});
lineSeries.setData([
{ time: entryTime, value: trade.entryPrice },
{ time: exitTime, value: trade.exitPrice }
]);
tradeLineSeries.push(lineSeries);
});
markers.sort((a, b) => a.time - b.time);
window.dashboard.candleSeries.setMarkers(markers);
console.log(`Plotted ${trades.length} trades with connection lines`);
}
export function clearSimulationMarkers() {
if (window.dashboard) {
window.dashboard.candleSeries.setMarkers([]);
tradeLineSeries.forEach(series => {
try {
window.dashboard.chart.removeSeries(series);
} catch (e) {
// Series might already be removed
}
});
tradeLineSeries = [];
}
}
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

@ -0,0 +1,47 @@
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

@ -0,0 +1,309 @@
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}`);
}
const data = await response.json();
console.log('Strategies loaded:', data);
if (!data.strategies) {
throw new Error('Invalid response format: missing strategies array');
}
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;

View File

@ -0,0 +1,23 @@
export function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
export function formatDate(date) {
return new Date(date).toISOString().slice(0, 16);
}
export function formatPrice(price, decimals = 2) {
return price.toFixed(decimals);
}
export function formatPercent(value) {
return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
}

View File

@ -0,0 +1 @@
export { downloadFile, formatDate, formatPrice, formatPercent } from './helpers.js';

View File

@ -5,34 +5,37 @@ Pure strategy logic separated from DB I/O for testability
import json
import logging
from dataclasses import dataclass, asdict
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Dict, Optional, Any, List
import importlib
from typing import Dict, Optional, Any, List, Callable
from .database import DatabaseManager
from .indicator_engine import IndicatorEngine
from src.strategies.base import BaseStrategy, StrategySignal, SignalType
from src.strategies.ma_strategy import MAStrategy
logger = logging.getLogger(__name__)
# Registry of available strategies
STRATEGY_REGISTRY = {
"ma44_strategy": "src.strategies.ma44_strategy.MA44Strategy",
"ma125_strategy": "src.strategies.ma125_strategy.MA125Strategy",
def _create_ma44() -> BaseStrategy:
return MAStrategy(config={"period": 44})
def _create_ma125() -> BaseStrategy:
return MAStrategy(config={"period": 125})
STRATEGY_REGISTRY: Dict[str, Callable[[], BaseStrategy]] = {
"ma_trend": MAStrategy,
"ma44_strategy": _create_ma44,
"ma125_strategy": _create_ma125,
}
def load_strategy(strategy_name: str) -> BaseStrategy:
"""Dynamically load a strategy class"""
"""Load a strategy instance from registry"""
if strategy_name not in STRATEGY_REGISTRY:
# Default fallback or error
logger.warning(f"Strategy {strategy_name} not found, defaulting to MA44")
strategy_name = "ma44_strategy"
logger.warning(f"Strategy {strategy_name} not found, defaulting to ma_trend")
strategy_name = "ma_trend"
module_path, class_name = STRATEGY_REGISTRY[strategy_name].rsplit('.', 1)
module = importlib.import_module(module_path)
cls = getattr(module, class_name)
return cls()
factory = STRATEGY_REGISTRY[strategy_name]
return factory()
@dataclass
class Decision:

View File

@ -19,6 +19,7 @@ from .database import DatabaseManager
from .custom_timeframe_generator import CustomTimeframeGenerator
from .indicator_engine import IndicatorEngine, IndicatorConfig
from .brain import Brain
from .backfill import HyperliquidBackfill
# Configure logging
@ -267,7 +268,7 @@ class DataCollector:
gaps = await self.db.detect_gaps(self.symbol, self.interval)
if gaps:
logger.warning(f"Detected {len(gaps)} data gaps: {gaps}")
# Could trigger backfill here
await self._backfill_gaps(gaps)
# Log database health
health = await self.db.get_health_stats()
@ -278,6 +279,36 @@ class DataCollector:
except Exception as e:
logger.error(f"Error in monitoring loop: {e}")
async def _backfill_gaps(self, gaps: list) -> None:
"""Backfill detected data gaps from Hyperliquid"""
if not gaps:
return
logger.info(f"Starting backfill for {len(gaps)} gaps...")
try:
async with HyperliquidBackfill(self.db, self.symbol, [self.interval]) as backfill:
for gap in gaps:
gap_start = datetime.fromisoformat(gap['gap_start'].replace('Z', '+00:00'))
gap_end = datetime.fromisoformat(gap['gap_end'].replace('Z', '+00:00'))
logger.info(f"Backfilling gap: {gap_start} to {gap_end} ({gap['missing_candles']} candles)")
candles = await backfill.fetch_candles(self.interval, gap_start, gap_end)
if candles:
inserted = await self.db.insert_candles(candles)
logger.info(f"Backfilled {inserted} candles for gap {gap_start}")
# Update custom timeframes and indicators for backfilled data
if inserted > 0:
await self._update_custom_timeframes(candles)
else:
logger.warning(f"No candles available for gap {gap_start} to {gap_end}")
except Exception as e:
logger.error(f"Backfill failed: {e}")
def _setup_signal_handlers(self) -> None:
"""Setup handlers for graceful shutdown"""
def signal_handler(sig, frame):