diff --git a/src/api/dashboard/static/index.html b/src/api/dashboard/static/index.html index 21ce853..102b1b4 100644 --- a/src/api/dashboard/static/index.html +++ b/src/api/dashboard/static/index.html @@ -722,6 +722,15 @@ .ta-ma-change.positive { color: var(--tv-green); } .ta-ma-change.negative { color: var(--tv-red); } + .indicator-item.selected { + border-color: var(--tv-blue) !important; + background: rgba(41, 98, 255, 0.1) !important; + } + + .indicator-item:hover { + background: var(--tv-hover); + } + .ta-level { display: flex; justify-content: space-between; @@ -1138,2045 +1147,20 @@ + + + diff --git a/src/api/dashboard/static/js/app.js b/src/api/dashboard/static/js/app.js new file mode 100644 index 0000000..f194506 --- /dev/null +++ b/src/api/dashboard/static/js/app.js @@ -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); + }; +}); diff --git a/src/api/dashboard/static/js/core/constants.js b/src/api/dashboard/static/js/core/constants.js new file mode 100644 index 0000000..d011415 --- /dev/null +++ b/src/api/dashboard/static/js/core/constants.js @@ -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'; diff --git a/src/api/dashboard/static/js/core/index.js b/src/api/dashboard/static/js/core/index.js new file mode 100644 index 0000000..79fc52b --- /dev/null +++ b/src/api/dashboard/static/js/core/index.js @@ -0,0 +1 @@ +export { INTERVALS, COLORS, API_BASE } from './constants.js'; diff --git a/src/api/dashboard/static/js/indicators/atr.js b/src/api/dashboard/static/js/indicators/atr.js new file mode 100644 index 0000000..6cc488f --- /dev/null +++ b/src/api/dashboard/static/js/indicators/atr.js @@ -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' }] + }; + } +} diff --git a/src/api/dashboard/static/js/indicators/base.js b/src/api/dashboard/static/js/indicators/base.js new file mode 100644 index 0000000..87f84ab --- /dev/null +++ b/src/api/dashboard/static/js/indicators/base.js @@ -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: [] + }; + } +} diff --git a/src/api/dashboard/static/js/indicators/bb.js b/src/api/dashboard/static/js/indicators/bb.js new file mode 100644 index 0000000..698a740 --- /dev/null +++ b/src/api/dashboard/static/js/indicators/bb.js @@ -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' } + ] + }; + } +} diff --git a/src/api/dashboard/static/js/indicators/ema.js b/src/api/dashboard/static/js/indicators/ema.js new file mode 100644 index 0000000..3eb7b95 --- /dev/null +++ b/src/api/dashboard/static/js/indicators/ema.js @@ -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' }] + }; + } +} diff --git a/src/api/dashboard/static/js/indicators/hts.js b/src/api/dashboard/static/js/indicators/hts.js new file mode 100644 index 0000000..838e883 --- /dev/null +++ b/src/api/dashboard/static/js/indicators/hts.js @@ -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 } + ] + }; + } +} diff --git a/src/api/dashboard/static/js/indicators/index.js b/src/api/dashboard/static/js/indicators/index.js new file mode 100644 index 0000000..3f626e9 --- /dev/null +++ b/src/api/dashboard/static/js/indicators/index.js @@ -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 +}; diff --git a/src/api/dashboard/static/js/indicators/ma.js b/src/api/dashboard/static/js/indicators/ma.js new file mode 100644 index 0000000..2d0c5f6 --- /dev/null +++ b/src/api/dashboard/static/js/indicators/ma.js @@ -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; + } +} diff --git a/src/api/dashboard/static/js/indicators/macd.js b/src/api/dashboard/static/js/indicators/macd.js new file mode 100644 index 0000000..aa2f9c1 --- /dev/null +++ b/src/api/dashboard/static/js/indicators/macd.js @@ -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' } + ] + }; + } +} diff --git a/src/api/dashboard/static/js/indicators/rsi.js b/src/api/dashboard/static/js/indicators/rsi.js new file mode 100644 index 0000000..221be5c --- /dev/null +++ b/src/api/dashboard/static/js/indicators/rsi.js @@ -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' }] + }; + } +} diff --git a/src/api/dashboard/static/js/indicators/sma.js b/src/api/dashboard/static/js/indicators/sma.js new file mode 100644 index 0000000..468e354 --- /dev/null +++ b/src/api/dashboard/static/js/indicators/sma.js @@ -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' }] + }; + } +} diff --git a/src/api/dashboard/static/js/indicators/stoch.js b/src/api/dashboard/static/js/indicators/stoch.js new file mode 100644 index 0000000..4702ec4 --- /dev/null +++ b/src/api/dashboard/static/js/indicators/stoch.js @@ -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' } + ] + }; + } +} diff --git a/src/api/dashboard/static/js/strategies/config.js b/src/api/dashboard/static/js/strategies/config.js new file mode 100644 index 0000000..dcf5c26 --- /dev/null +++ b/src/api/dashboard/static/js/strategies/config.js @@ -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' } +]; diff --git a/src/api/dashboard/static/js/strategies/engine.js b/src/api/dashboard/static/js/strategies/engine.js new file mode 100644 index 0000000..cfa47cf --- /dev/null +++ b/src/api/dashboard/static/js/strategies/engine.js @@ -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'; + } +} diff --git a/src/api/dashboard/static/js/strategies/index.js b/src/api/dashboard/static/js/strategies/index.js new file mode 100644 index 0000000..c7ca41d --- /dev/null +++ b/src/api/dashboard/static/js/strategies/index.js @@ -0,0 +1,3 @@ +export { StrategyParams, AVAILABLE_INDICATORS } from './config.js'; +export { RiskManager } from './risk-manager.js'; +export { ClientStrategyEngine } from './engine.js'; diff --git a/src/api/dashboard/static/js/strategies/risk-manager.js b/src/api/dashboard/static/js/strategies/risk-manager.js new file mode 100644 index 0000000..11c66dc --- /dev/null +++ b/src/api/dashboard/static/js/strategies/risk-manager.js @@ -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; + } +} diff --git a/src/api/dashboard/static/js/ui/chart.js b/src/api/dashboard/static/js/ui/chart.js new file mode 100644 index 0000000..a403885 --- /dev/null +++ b/src/api/dashboard/static/js/ui/chart.js @@ -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 = '
Failed to load technical analysis
'; + } + } + + renderTA() { + if (!this.taData || this.taData.error) { + document.getElementById('taContent').innerHTML = `
${this.taData?.error || 'No data available'}
`; + 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 = ` +
+
Trend Analysis
+
+ ${data.trend.direction} ${trendClass === 'bullish' ? '↑' : trendClass === 'bearish' ? '↓' : '→'} +
+
${data.trend.strength}
+ ${data.trend.signal} +
+ +
+
Moving Averages
+
+ MA 44 + + ${data.moving_averages.ma_44 ? data.moving_averages.ma_44.toFixed(2) : 'N/A'} + ${ma44Change !== null ? `${ma44Change >= 0 ? '+' : ''}${ma44Change.toFixed(1)}%` : ''} + +
+
+ MA 125 + + ${data.moving_averages.ma_125 ? data.moving_averages.ma_125.toFixed(2) : 'N/A'} + ${ma125Change !== null ? `${ma125Change >= 0 ? '+' : ''}${ma125Change.toFixed(1)}%` : ''} + +
+
+ +
+
Indicators
+
+ +
+ +
+
Configuration
+
+
+ + +
+
+ `; + } + + 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'); +} diff --git a/src/api/dashboard/static/js/ui/export.js b/src/api/dashboard/static/js/ui/export.js new file mode 100644 index 0000000..42dfc63 --- /dev/null +++ b/src/api/dashboard/static/js/ui/export.js @@ -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 = ` +
📥 Export Simulation Report
+
+ + + +
+
+ + +
+ `; + 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; diff --git a/src/api/dashboard/static/js/ui/index.js b/src/api/dashboard/static/js/ui/index.js new file mode 100644 index 0000000..99eae98 --- /dev/null +++ b/src/api/dashboard/static/js/ui/index.js @@ -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'; diff --git a/src/api/dashboard/static/js/ui/indicators-panel.js b/src/api/dashboard/static/js/ui/indicators-panel.js new file mode 100644 index 0000000..44167d8 --- /dev/null +++ b/src/api/dashboard/static/js/ui/indicators-panel.js @@ -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 = '
No indicators added
'; + return; + } + + container.innerHTML = activeIndicators.map((ind, idx) => ` +
+
+
${ind.name}
+
${ind.params.short || ind.params.period || ind.params.fast || 'N/A'}
+
+ +
+ `).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 => ` +
+ + ${input.type === 'select' ? + `` : + `` + } +
+ `).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; diff --git a/src/api/dashboard/static/js/ui/sidebar.js b/src/api/dashboard/static/js/ui/sidebar.js new file mode 100644 index 0000000..6d797a3 --- /dev/null +++ b/src/api/dashboard/static/js/ui/sidebar.js @@ -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'); + } +} diff --git a/src/api/dashboard/static/js/ui/simulation.js b/src/api/dashboard/static/js/ui/simulation.js new file mode 100644 index 0000000..e3d712e --- /dev/null +++ b/src/api/dashboard/static/js/ui/simulation.js @@ -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 = '
No trades
'; + 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 = ''; + 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 + }; +} diff --git a/src/api/dashboard/static/js/ui/storage.js b/src/api/dashboard/static/js/ui/storage.js new file mode 100644 index 0000000..48b2fa3 --- /dev/null +++ b/src/api/dashboard/static/js/ui/storage.js @@ -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); + } +}; diff --git a/src/api/dashboard/static/js/ui/strategies-panel.js b/src/api/dashboard/static/js/ui/strategies-panel.js new file mode 100644 index 0000000..8a26f6d --- /dev/null +++ b/src/api/dashboard/static/js/ui/strategies-panel.js @@ -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 = '
No strategies available
'; + return; + } + + container.innerHTML = strategies.map((s, index) => ` +
+ + ${s.name} + +
+ `).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 => ` +
+ + +
+ `).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 = + `
+ ${errorMessage}
+ Check console (F12) for details +
`; + } +} + +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 = '
No saved simulations
'; + return; + } + + container.innerHTML = simulations.map(sim => ` +
+ + ${sim.name.length > 25 ? sim.name.slice(0, 25) + '...' : sim.name} + +
+ + + +
+
+ `).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 = '
No trades
'; + 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 = ''; + 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; diff --git a/src/api/dashboard/static/js/utils/helpers.js b/src/api/dashboard/static/js/utils/helpers.js new file mode 100644 index 0000000..03df981 --- /dev/null +++ b/src/api/dashboard/static/js/utils/helpers.js @@ -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) + '%'; +} diff --git a/src/api/dashboard/static/js/utils/index.js b/src/api/dashboard/static/js/utils/index.js new file mode 100644 index 0000000..ea672c1 --- /dev/null +++ b/src/api/dashboard/static/js/utils/index.js @@ -0,0 +1 @@ +export { downloadFile, formatDate, formatPrice, formatPercent } from './helpers.js'; diff --git a/src/data_collector/brain.py b/src/data_collector/brain.py index 1c4d805..37ee6fe 100644 --- a/src/data_collector/brain.py +++ b/src/data_collector/brain.py @@ -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: diff --git a/src/data_collector/main.py b/src/data_collector/main.py index 2940f0a..5bf6410 100644 --- a/src/data_collector/main.py +++ b/src/data_collector/main.py @@ -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):