From fdab0a3faa3eb069da080568588b50796924d1c7 Mon Sep 17 00:00:00 2001 From: DiTus Date: Sun, 1 Mar 2026 19:37:07 +0100 Subject: [PATCH] Pre-refactor: commit before converting indicators to self-contained files --- src/api/dashboard/static/index.html | 108 ---- src/api/dashboard/static/js/app.js | 56 -- .../dashboard/static/js/strategies/config.js | 12 - .../dashboard/static/js/strategies/engine.js | 167 ------ .../static/js/strategies/hts-engine.js | 423 ------------- .../dashboard/static/js/strategies/index.js | 3 - .../static/js/strategies/risk-manager.js | 17 - src/api/dashboard/static/js/ui/export.js | 140 ----- .../dashboard/static/js/ui/hts-visualizer.js | 2 - src/api/dashboard/static/js/ui/index.js | 23 - .../static/js/ui/indicators-panel-new.js | 43 +- .../static/js/ui/signals-calculator.js | 53 +- src/api/dashboard/static/js/ui/simulation.js | 567 ------------------ src/api/dashboard/static/js/ui/storage.js | 47 -- .../static/js/ui/strategies-panel.js | 318 ---------- src/api/server.py | 89 --- src/data_collector/__init__.py | 4 +- src/data_collector/backtester.py | 391 ------------ src/data_collector/brain.py | 54 +- src/data_collector/simulator.py | 160 ----- src/strategies/base.py | 68 --- src/strategies/ma_strategy.py | 77 --- 22 files changed, 96 insertions(+), 2726 deletions(-) delete mode 100644 src/api/dashboard/static/js/strategies/config.js delete mode 100644 src/api/dashboard/static/js/strategies/engine.js delete mode 100644 src/api/dashboard/static/js/strategies/hts-engine.js delete mode 100644 src/api/dashboard/static/js/strategies/index.js delete mode 100644 src/api/dashboard/static/js/strategies/risk-manager.js delete mode 100644 src/api/dashboard/static/js/ui/export.js delete mode 100644 src/api/dashboard/static/js/ui/simulation.js delete mode 100644 src/api/dashboard/static/js/ui/storage.js delete mode 100644 src/api/dashboard/static/js/ui/strategies-panel.js delete mode 100644 src/data_collector/backtester.py delete mode 100644 src/data_collector/simulator.py delete mode 100644 src/strategies/base.py delete mode 100644 src/strategies/ma_strategy.py diff --git a/src/api/dashboard/static/index.html b/src/api/dashboard/static/index.html index ba65160..445cde9 100644 --- a/src/api/dashboard/static/index.html +++ b/src/api/dashboard/static/index.html @@ -1392,7 +1392,6 @@ @@ -1406,113 +1405,6 @@ - - - diff --git a/src/api/dashboard/static/js/app.js b/src/api/dashboard/static/js/app.js index 40c6ce4..67b64af 100644 --- a/src/api/dashboard/static/js/app.js +++ b/src/api/dashboard/static/js/app.js @@ -1,25 +1,5 @@ import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js'; import { restoreSidebarState, toggleSidebar, initSidebarTabs, restoreSidebarTabState } from './ui/sidebar.js'; -import { SimulationStorage } from './ui/storage.js'; -import { showExportDialog, closeExportDialog, performExport, exportSavedSimulation } from './ui/export.js'; -import { - runSimulation, - displayEnhancedResults, - showSimulationMarkers, - clearSimulationResults, - getLastResults, - setLastResults -} from './ui/simulation.js'; -import { - renderStrategies, - selectStrategy, - loadStrategies, - saveSimulation, - renderSavedSimulations, - loadSavedSimulation, - deleteSavedSimulation, - setCurrentStrategy -} from './ui/strategies-panel.js'; import { initIndicatorPanel, getActiveIndicators, @@ -28,42 +8,13 @@ import { addIndicator, removeIndicatorById } from './ui/indicators-panel-new.js'; -import { StrategyParams } from './strategies/config.js'; import { IndicatorRegistry } from './indicators/index.js'; window.dashboard = null; -function setDefaultStartDate() { - const startDateInput = document.getElementById('simStartDate'); - if (startDateInput) { - const sevenDaysAgo = new Date(); - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - startDateInput.value = sevenDaysAgo.toISOString().slice(0, 16); - } -} - -function updateTimeframeDisplay() { - const display = document.getElementById('simTimeframeDisplay'); - if (display && window.dashboard) { - display.value = window.dashboard.currentInterval.toUpperCase(); - } -} - window.toggleSidebar = toggleSidebar; window.refreshTA = refreshTA; window.openAIAnalysis = openAIAnalysis; -window.showExportDialog = showExportDialog; -window.closeExportDialog = closeExportDialog; -window.performExport = performExport; -window.exportSavedSimulation = exportSavedSimulation; -window.runSimulation = runSimulation; -window.saveSimulation = saveSimulation; -window.showSimulationMarkers = showSimulationMarkers; -window.renderSavedSimulations = renderSavedSimulations; -window.loadSavedSimulation = loadSavedSimulation; -window.deleteSavedSimulation = deleteSavedSimulation; -window.clearSimulationResults = clearSimulationResults; -window.updateTimeframeDisplay = updateTimeframeDisplay; window.renderIndicatorList = function() { // This function is no longer needed for sidebar indicators }; @@ -73,8 +24,6 @@ window.initIndicatorPanel = initIndicatorPanel; window.addIndicator = addIndicator; window.toggleIndicator = addIndicator; -window.StrategyParams = StrategyParams; -window.SimulationStorage = SimulationStorage; window.IndicatorRegistry = IndicatorRegistry; document.addEventListener('DOMContentLoaded', async () => { @@ -88,11 +37,6 @@ document.addEventListener('DOMContentLoaded', async () => { restoreSidebarState(); restoreSidebarTabState(); initSidebarTabs(); - setDefaultStartDate(); - updateTimeframeDisplay(); - renderSavedSimulations(); - - await loadStrategies(); // Initialize indicator panel window.initIndicatorPanel(); diff --git a/src/api/dashboard/static/js/strategies/config.js b/src/api/dashboard/static/js/strategies/config.js deleted file mode 100644 index 0beea5b..0000000 --- a/src/api/dashboard/static/js/strategies/config.js +++ /dev/null @@ -1,12 +0,0 @@ -export const StrategyParams = { - hts_trend: [ - { name: 'shortPeriod', label: 'Fast Period', type: 'number', default: 33, min: 5, max: 200 }, - { name: 'longPeriod', label: 'Slow Period', type: 'number', default: 144, min: 10, max: 500 }, - { name: 'maType', label: 'MA Type', type: 'select', options: ['RMA', 'SMA', 'EMA', 'WMA', 'VWMA'], default: 'RMA' }, - { name: 'useAutoHTS', label: 'Auto HTS (TF/4)', type: 'boolean', default: false }, - { name: 'use1HFilter', label: '1H Red Zone Filter', type: 'boolean', default: true } - ], - ma_trend: [ - { name: 'period', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 } - ] -}; diff --git a/src/api/dashboard/static/js/strategies/engine.js b/src/api/dashboard/static/js/strategies/engine.js deleted file mode 100644 index cfa47cf..0000000 --- a/src/api/dashboard/static/js/strategies/engine.js +++ /dev/null @@ -1,167 +0,0 @@ -import { IndicatorRegistry } from '../indicators/index.js'; -import { RiskManager } from './risk-manager.js'; - -export class ClientStrategyEngine { - constructor() { - this.indicatorTypes = IndicatorRegistry; - } - - run(candlesMap, strategyConfig, riskConfig, simulationStart) { - const primaryTF = strategyConfig.timeframes?.primary || '1d'; - const candles = candlesMap[primaryTF]; - if (!candles) return { error: `No candles for primary timeframe ${primaryTF}` }; - - const indicatorResults = {}; - console.log('Calculating indicators for timeframes:', Object.keys(candlesMap)); - for (const tf in candlesMap) { - indicatorResults[tf] = {}; - const tfCandles = candlesMap[tf]; - const tfIndicators = (strategyConfig.indicators || []).filter(ind => (ind.timeframe || primaryTF) === tf); - - console.log(` TF ${tf}: ${tfIndicators.length} indicators to calculate`); - - for (const ind of tfIndicators) { - const IndicatorClass = this.indicatorTypes[ind.type]; - if (IndicatorClass) { - const instance = new IndicatorClass(ind); - indicatorResults[tf][ind.name] = instance.calculate(tfCandles); - const validValues = indicatorResults[tf][ind.name].filter(v => v !== null).length; - console.log(` Calculated ${ind.name} on ${tf}: ${validValues} valid values`); - } - } - } - - const risk = new RiskManager(riskConfig); - const trades = []; - let position = null; - const startTimeSec = Math.floor(new Date(simulationStart).getTime() / 1000); - console.log('Simulation start (seconds):', startTimeSec, 'Date:', simulationStart); - console.log('Total candles available:', candles.length); - console.log('First candle time:', candles[0].time, 'Last candle time:', candles[candles.length - 1].time); - - const pointers = {}; - for (const tf in candlesMap) pointers[tf] = 0; - - let processedCandles = 0; - - for (let i = 1; i < candles.length; i++) { - const time = candles[i].time; - const price = candles[i].close; - - if (time < startTimeSec) { - for (const tf in candlesMap) { - while (pointers[tf] < candlesMap[tf].length - 1 && - candlesMap[tf][pointers[tf] + 1].time <= time) { - pointers[tf]++; - } - } - continue; - } - - processedCandles++; - - for (const tf in candlesMap) { - while (pointers[tf] < candlesMap[tf].length - 1 && - candlesMap[tf][pointers[tf] + 1].time <= time) { - pointers[tf]++; - } - } - - const signal = this.evaluate(i, pointers, candles, candlesMap, indicatorResults, strategyConfig, position); - - if (signal === 'BUY' && !position) { - const size = risk.calculateSize(price); - position = { type: 'long', entryPrice: price, entryTime: candles[i].time, size }; - } else if (signal === 'SELL' && position) { - const pnl = (price - position.entryPrice) * position.size; - trades.push({ ...position, exitPrice: price, exitTime: candles[i].time, pnl, pnlPct: (pnl / (position.entryPrice * position.size)) * 100 }); - risk.balance += pnl; - position = null; - } - } - - console.log(`Simulation complete: ${processedCandles} candles processed after start date, ${trades.length} trades`); - - return { - total_trades: trades.length, - win_rate: (trades.filter(t => t.pnl > 0).length / (trades.length || 1)) * 100, - total_pnl: risk.balance - 1000, - trades - }; - } - - evaluate(index, pointers, candles, candlesMap, indicatorResults, config, position) { - const primaryTF = config.timeframes?.primary || '1d'; - - const getVal = (indName, tf) => { - const tfValues = indicatorResults[tf]?.[indName]; - if (!tfValues) return null; - return tfValues[pointers[tf]]; - }; - - const getPrice = (tf) => { - const tfCandles = candlesMap[tf]; - if (!tfCandles) return null; - return tfCandles[pointers[tf]].close; - }; - - if (config.id === 'ma_trend') { - const period = config.params?.period || 44; - - if (index === 1) { - console.log('First candle time:', candles[index].time, 'Date:', new Date(candles[index].time * 1000)); - console.log(`MA${period} value:`, getVal(`ma${period}`, primaryTF)); - } - const maValue = getVal(`ma${period}`, primaryTF); - const price = candles[index].close; - - const secondaryTF = config.timeframes?.secondary?.[0]; - let secondaryBullish = true; - let secondaryBearish = true; - if (secondaryTF) { - const secondaryPrice = getPrice(secondaryTF); - const secondaryMA = getVal(`ma${period}_${secondaryTF}`, secondaryTF); - if (secondaryPrice !== null && secondaryMA !== null) { - secondaryBullish = secondaryPrice > secondaryMA; - secondaryBearish = secondaryPrice < secondaryMA; - } - if (index === 1) { - console.log(`Trend check: ${secondaryTF} price=${secondaryPrice}, MA=${secondaryMA}, bullish=${secondaryBullish}, bearish=${secondaryBearish}`); - } - } - - if (maValue) { - if (price > maValue && secondaryBullish) return 'BUY'; - if (price < maValue && secondaryBearish) return 'SELL'; - } - } - - const evaluateConditions = (conds) => { - if (!conds || !conds.conditions) return false; - const results = conds.conditions.map(c => { - const targetTF = c.timeframe || primaryTF; - const leftVal = c.indicator === 'price' ? getPrice(targetTF) : getVal(c.indicator, targetTF); - const rightVal = typeof c.value === 'number' ? c.value : (c.value === 'price' ? getPrice(targetTF) : getVal(c.value, targetTF)); - - if (leftVal === null || rightVal === null) return false; - - switch(c.operator) { - case '>': return leftVal > rightVal; - case '<': return leftVal < rightVal; - case '>=': return leftVal >= rightVal; - case '<=': return leftVal <= rightVal; - case '==': return leftVal == rightVal; - default: return false; - } - }); - - if (conds.logic === 'OR') return results.some(r => r); - return results.every(r => r); - }; - - if (evaluateConditions(config.entryLong)) return 'BUY'; - if (evaluateConditions(config.exitLong)) return 'SELL'; - - return 'HOLD'; - } -} diff --git a/src/api/dashboard/static/js/strategies/hts-engine.js b/src/api/dashboard/static/js/strategies/hts-engine.js deleted file mode 100644 index ec66191..0000000 --- a/src/api/dashboard/static/js/strategies/hts-engine.js +++ /dev/null @@ -1,423 +0,0 @@ -import { MA } from '../indicators/ma.js'; - -/** - * HTS (Higher Timeframe Trend System) Strategy Engine - * Computes trading signals based on HTS indicator rules: - * 1. Trend detection using fast/slow channels - * 2. Entry rules on channel breakouts - * 3. 1H timeframe filter (optional) - * 4. Stop loss based on opposite channel - */ - -export class HTSStrategyEngine { - constructor(config = {}) { - this.config = { - shortPeriod: config.shortPeriod || 33, - longPeriod: config.longPeriod || 144, - maType: config.maType || 'RMA', - useAutoHTS: config.useAutoHTS || false, - use1HFilter: config.use1HFilter !== false - }; - } - - calculateHTSCandles(candles, periodMult = 1) { - if (!candles || candles.length < this.config.longPeriod) return []; - - const shortPeriod = this.config.shortPeriod; - const longPeriod = this.config.longPeriod; - const maType = this.config.maType; - - const shortHigh = MA.get(maType, candles, shortPeriod, 'high'); - const shortLow = MA.get(maType, candles, shortPeriod, 'low'); - const longHigh = MA.get(maType, candles, longPeriod, 'high'); - const longLow = MA.get(maType, candles, longPeriod, 'low'); - - const results = []; - for (let i = 0; i < candles.length; i++) { - if (!shortHigh[i] || !longLow[i]) continue; - - results.push({ - time: candles[i].time, - fastHigh: shortHigh[i], - fastLow: shortLow[i], - slowHigh: longHigh[i], - slowLow: longLow[i], - price: candles[i].close, - fastMidpoint: (shortHigh[i] + shortLow[i]) / 2, - slowMidpoint: (longHigh[i] + longLow[i]) / 2 - }); - } - - return results; - } - - computeAutoHTS(oneMinCandles, targetTF) { - if (!oneMinCandles || oneMinCandles.length < this.config.longPeriod) return []; - - const tfMultipliers = { - '5m': 5, - '15m': 15, - '30m': 30, - '37m': 37, - '1h': 60, - '4h': 240 - }; - - const tfGroup = tfMultipliers[targetTF] || 5; - - const grouped = []; - let currentGroup = []; - for (let i = 0; i < oneMinCandles.length; i++) { - currentGroup.push(oneMinCandles[i]); - if (currentGroup.length >= tfGroup) { - grouped.push(currentGroup[tfGroup - 1]); - currentGroup = []; - } - } - - return this.calculateHTSCandles(grouped); - } - - check1HFilter(price, h1hHTS) { - if (!this.config.use1HFilter || !h1hHTS || h1hHTS.length === 0) { - return { confirmed: true, reasoning: '1H filter disabled or no data' }; - } - - const latest = h1hHTS[h1hHTS.length - 1]; - const slowLow = latest?.slowLow; - - if (slowLow === null || slowLow === undefined) { - return { confirmed: true, reasoning: '1H HTS not ready' }; - } - - if (price < slowLow) { - return { - confirmed: false, - reasoning: `1H Filter: Long rejected - price ${price.toFixed(2)} below 1H slow channel (${slowLow.toFixed(2)})` - }; - } - - return { - confirmed: true, - reasoning: `1H Filter: Confirmed - price ${price.toFixed(2)} above 1H slow channel (${slowLow.toFixed(2)})` - }; - } - - calculateEntrySignal(primaryHTS, index, position, h1hHTS = null, prevHTS = null) { - if (!primaryHTS || index >= primaryHTS.length || index < 1) { - return { signal: 'HOLD', confidence: 0, reasoning: 'Insufficient data' }; - } - - const current = primaryHTS[index]; - const currentPrice = current?.price; - - if (!currentPrice) { - return { signal: 'HOLD', confidence: 0, reasoning: 'No price data' }; - } - - const prev = primaryHTS[index - 1]; - - const h1hCheck = this.check1HFilter(currentPrice, h1hHTS); - if (!h1hCheck.confirmed) { - return { - signal: 'HOLD', - confidence: 0, - reasoning: h1hCheck.reasoning, - filterStatus: '1H_REJECTED' - }; - } - - const fastLow = current?.fastLow; - const fastHigh = current?.fastHigh; - const slowLow = current?.slowLow; - const slowHigh = current?.slowHigh; - - const prevFastLow = prev?.fastLow; - const prevSlowLow = prev?.slowLow; - const prevFastHigh = prev?.fastHigh; - const prevSlowHigh = prev?.slowHigh; - - if (!fastLow || !slowLow) { - return { signal: 'HOLD', confidence: 0, reasoning: 'HTS data not ready' }; - } - - const inPositionLong = position?.type === 'long'; - const inPositionShort = position?.type === 'short'; - - if (inPositionLong) { - if (currentPrice < slowLow || fastLow < slowLow) { - return { - signal: 'CLOSE_LONG', - confidence: 90, - reasoning: `Stop Loss: Price ${currentPrice.toFixed(2)} broke below slow channel (${slowLow.toFixed(2)})`, - stopPrice: slowLow - }; - } - return { - signal: 'HOLD', - confidence: 50, - reasoning: `Long open, holding - price ${currentPrice.toFixed(2)} above stop at ${slowLow.toFixed(2)}`, - stopPrice: slowLow - }; - } - - if (inPositionShort) { - if (currentPrice > slowHigh || fastHigh > slowHigh) { - return { - signal: 'CLOSE_SHORT', - confidence: 90, - reasoning: `Stop Loss: Price ${currentPrice.toFixed(2)} broke above slow channel (${slowHigh.toFixed(2)})`, - stopPrice: slowHigh - }; - } - return { - signal: 'HOLD', - confidence: 50, - reasoning: `Short open, holding - price ${currentPrice.toFixed(2)} below stop at ${slowHigh.toFixed(2)}`, - stopPrice: slowHigh - }; - } - - if (prevFastLow <= prevSlowLow && fastLow > slowLow) { - return { - signal: 'BUY', - confidence: 75, - reasoning: `Long Entry: Fast Low (${fastLow.toFixed(2)}) crossed above Slow Low (${slowLow.toFixed(2)})`, - entryType: 'CROSSOVER' - }; - } - - if (prevFastHigh >= prevSlowHigh && fastHigh < slowHigh) { - return { - signal: 'SELL', - confidence: 75, - reasoning: `Short Entry: Fast High (${fastHigh.toFixed(2)}) crossed below Slow High (${slowHigh.toFixed(2)})`, - entryType: 'CROSSUNDER' - }; - } - - const bullAlignment = slowLow < slowHigh && fastLow > slowLow; - const bearAlignment = slowLow < slowHigh && fastHigh < slowHigh; - - if (bullAlignment && currentPrice > slowLow) { - return { - signal: 'BUY', - confidence: 50, - reasoning: `Long Entry: Price ${currentPrice.toFixed(2)} above Slow Low with bullish channel alignment`, - entryType: 'ALIGNED' - }; - } - - if (bearAlignment && currentPrice < slowHigh) { - return { - signal: 'SELL', - confidence: 50, - reasoning: `Short Entry: Price ${currentPrice.toFixed(2)} below Slow High with bearish channel alignment`, - entryType: 'ALIGNED' - }; - } - - return { - signal: 'HOLD', - confidence: 10, - reasoning: 'No clear signal - waiting for crossover' - }; - } - - computeStopLoss(position, currentHTS, index) { - if (!position || !currentHTS || index >= currentHTS.length) return null; - - const current = currentHTS[index]; - if (!current) return null; - - if (position.type === 'long') { - return current.slowLow; - } - - if (position.type === 'short') { - return current.slowHigh; - } - - return null; - } - - calculateHTSCandles(candles, periodMult = 1) { - if (!candles || candles.length < this.config.longPeriod) return []; - - const shortPeriod = this.config.shortPeriod; - const longPeriod = this.config.longPeriod; - const maType = this.config.maType; - - const shortHigh = MA.get(maType, candles, shortPeriod, 'high'); - const shortLow = MA.get(maType, candles, shortPeriod, 'low'); - const longHigh = MA.get(maType, candles, longPeriod, 'high'); - const longLow = MA.get(maType, candles, longPeriod, 'low'); - - const results = []; - for (let i = 0; i < candles.length; i++) { - if (!shortHigh[i] || !longLow[i]) continue; - - results.push({ - time: candles[i].time, - fastHigh: shortHigh[i], - fastLow: shortLow[i], - slowHigh: longHigh[i], - slowLow: longLow[i], - price: candles[i].close, - fastMidpoint: ((shortHigh[i] + shortLow[i]) / 2), - slowMidpoint: ((longHigh[i] + longLow[i]) / 2), - open: candles[i].open, - high: candles[i].high, - low: candles[i].low, - close: candles[i].close, - volume: candles[i].volume - }); - } - - return results; - } - - runSimulation(candles, oneMinCandles, h1hCandles) { - const primaryHTS = this.calculateHTSCandles(candles); - - console.log('[HTS] Starting simulation'); - console.log('[HTS] Primary HTS data length:', primaryHTS.length); - console.log('[HTS] Original candles length:', candles.length); - - let h1hHTS = null; - if (h1hCandles && h1hCandles.length > 0) { - h1hHTS = this.calculateHTSCandles(h1hCandles); - console.log('[HTS] 1H HTS data length:', h1hHTS.length); - } - - let position = null; - const trades = []; - let balance = 1000; - let rejectionsCount = 0; - let buySignals = 0; - let sellSignals = 0; - - for (let i = 1; i < primaryHTS.length; i++) { - if (!primaryHTS[i]) continue; - - const htsData = primaryHTS[i]; - const signal = this.calculateEntrySignal(primaryHTS, i, position, h1hHTS, primaryHTS[i - 1]); - - if (signal.filterStatus === '1H_REJECTED') { - rejectionsCount++; - continue; - } - - if (signal.signal === 'BUY' && !position) { - buySignals++; - console.log('[HTS] BUY SIGNAL:', signal.reasoning); - position = { - type: 'long', - entryPrice: htsData.price, - entryTime: htsData.time, - size: 1, - stopLoss: signal.stopPrice || primaryHTS[i].slowLow - }; - console.log('[HTS] LONG entry at', htsData.price, 'stop at', position.stopLoss); - } - - if (signal.signal === 'SELL' && !position) { - sellSignals++; - console.log('[HTS] SELL SIGNAL:', signal.reasoning); - position = { - type: 'short', - entryPrice: htsData.price, - entryTime: htsData.time, - size: 1, - stopLoss: signal.stopPrice || primaryHTS[i].slowHigh - }; - console.log('[HTS] SHORT entry at', htsData.price, 'stop at', position.stopLoss); - } - - if ((signal.signal === 'CLOSE_LONG' || signal.signal === 'CLOSE_SHORT') && position) { - const pnl = position.type === 'long' - ? (htsData.price - position.entryPrice) - : (position.entryPrice - htsData.price); - - trades.push({ - ...position, - exitPrice: htsData.price, - exitTime: htsData.time, - pnl, - pnlPct: (pnl / position.entryPrice) * 100 - }); - - balance += pnl; - console.log('[HTS] Position closed:', signal.signal, 'PnL:', pnl.toFixed(2)); - position = null; - } - - if (position) { - const sl = this.computeStopLoss(position, primaryHTS, i); - - if (sl !== null) { - const slHit = position.type === 'long' ? htsData.price < sl : htsData.price > sl; - - if (slHit) { - const pnl = position.type === 'long' - ? (sl - position.entryPrice) - : (position.entryPrice - sl); - - trades.push({ - ...position, - exitPrice: sl, - exitTime: htsData.time, - pnl, - pnlPct: (pnl / position.entryPrice) * 100, - exitReason: 'STOP_LOSS' - }); - - balance += pnl; - console.log('[HTS] Stop Loss hit, PnL:', pnl.toFixed(2)); - position = null; - } - } - } - } - - const winRate = trades.length > 0 - ? (trades.filter(t => t.pnl > 0).length / trades.length) * 100 - : 0; - - console.log('[HTS] Simulation complete:'); - console.log('[HTS] - Buy signals:', buySignals); - console.log('[HTS] - Sell signals:', sellSignals); - console.log('[HTS] - 1H filter rejections:', rejectionsCount); - console.log('[HTS] - Total HTS candles processed:', primaryHTS.length); - console.log('[HTS] - Total trades:', trades.length); - console.log('[HTS] - Final balance:', balance.toFixed(2)); - console.log('[HTS] - Win rate:', winRate.toFixed(1) + '%'); - - if (trades.length === 0) { - console.warn('[HTS] No trades generated! Possible reasons:'); - if (rejectionsCount > 0) { - console.warn(` - 1H filter rejected ${rejectionsCount} potential trades`); - } - if (buySignals === 0 && sellSignals === 0) { - console.warn(' - No crossover or alignment signals detected'); - console.warn(' - Market may be in ranging/sideways mode'); - } - console.warn(' - Consider:'); - console.warn(' * Reducing 1H filter strictness or disabling it'); - console.warn(' * Adjusting short/long periods for current timeframe'); - console.warn(' * Checking if data has sufficient price movement'); - } - - return { - trades, - balance, - totalTrades: trades.length, - winRate, - finalPnL: balance - 1000, - total_trades: trades.length, - win_rate: winRate, - total_pnl: balance - 1000, - htsData: primaryHTS - }; - } -} \ No newline at end of file diff --git a/src/api/dashboard/static/js/strategies/index.js b/src/api/dashboard/static/js/strategies/index.js deleted file mode 100644 index a14f5b4..0000000 --- a/src/api/dashboard/static/js/strategies/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { StrategyParams } 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 deleted file mode 100644 index 11c66dc..0000000 --- a/src/api/dashboard/static/js/strategies/risk-manager.js +++ /dev/null @@ -1,17 +0,0 @@ -export class RiskManager { - constructor(config, initialBalance = 1000) { - this.config = config || { - positionSizing: { method: 'percent', value: 0.1 }, - stopLoss: { enabled: true, method: 'percent', value: 0.02 }, - takeProfit: { enabled: true, method: 'percent', value: 0.04 } - }; - this.balance = initialBalance; - this.equity = initialBalance; - } - calculateSize(price) { - if (this.config.positionSizing.method === 'percent') { - return (this.balance * this.config.positionSizing.value) / price; - } - return this.config.positionSizing.value / price; - } -} diff --git a/src/api/dashboard/static/js/ui/export.js b/src/api/dashboard/static/js/ui/export.js deleted file mode 100644 index 42dfc63..0000000 --- a/src/api/dashboard/static/js/ui/export.js +++ /dev/null @@ -1,140 +0,0 @@ -import { downloadFile } from '../utils/index.js'; - -export function showExportDialog() { - if (!window.lastSimulationResults) { - alert('Please run a simulation first'); - return; - } - - const overlay = document.createElement('div'); - overlay.className = 'dialog-overlay'; - overlay.onclick = () => closeExportDialog(); - document.body.appendChild(overlay); - - const dialog = document.createElement('div'); - dialog.className = 'export-dialog'; - dialog.id = 'exportDialog'; - dialog.innerHTML = ` -
📥 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/hts-visualizer.js b/src/api/dashboard/static/js/ui/hts-visualizer.js index 9baacc3..bebc3dc 100644 --- a/src/api/dashboard/static/js/ui/hts-visualizer.js +++ b/src/api/dashboard/static/js/ui/hts-visualizer.js @@ -1,5 +1,3 @@ -import { HTSStrategyEngine } from '../strategies/hts-engine.js'; - const HTS_COLORS = { fastHigh: '#00bcd4', fastLow: '#00bcd4', diff --git a/src/api/dashboard/static/js/ui/index.js b/src/api/dashboard/static/js/ui/index.js index 99eae98..a007e51 100644 --- a/src/api/dashboard/static/js/ui/index.js +++ b/src/api/dashboard/static/js/ui/index.js @@ -1,28 +1,5 @@ export { TradingDashboard, refreshTA, openAIAnalysis } from './chart.js'; export { toggleSidebar, restoreSidebarState } from './sidebar.js'; -export { SimulationStorage } from './storage.js'; -export { showExportDialog, closeExportDialog, performExport, exportSavedSimulation } from './export.js'; -export { - runSimulation, - displayEnhancedResults, - showSimulationMarkers, - clearSimulationMarkers, - clearSimulationResults, - getLastResults, - setLastResults -} from './simulation.js'; -export { - renderStrategies, - selectStrategy, - renderStrategyParams, - loadStrategies, - saveSimulation, - renderSavedSimulations, - loadSavedSimulation, - deleteSavedSimulation, - getCurrentStrategy, - setCurrentStrategy -} from './strategies-panel.js'; export { renderIndicatorList, addNewIndicator, diff --git a/src/api/dashboard/static/js/ui/indicators-panel-new.js b/src/api/dashboard/static/js/ui/indicators-panel-new.js index 40031e1..2feb93a 100644 --- a/src/api/dashboard/static/js/ui/indicators-panel-new.js +++ b/src/api/dashboard/static/js/ui/indicators-panel-new.js @@ -295,9 +295,50 @@ function renderIndicatorConfig(indicator, meta) { ${indicator.params._lineWidth || 2} - ` : ''} +` : ''} + ${window.dashboard?.indicatorSignals ? ` +
+
Signal Status
+ ${(() => { + const indSignal = window.dashboard.indicatorSignals.find(s => s.id === indicator.id); + if (!indSignal) { + return `
No signal data available
`; + } + + const signalType = indSignal.signal.toUpperCase(); + const signalIcon = signalType === 'BUY' ? '🟢' : signalType === 'SELL' ? '🔴' : '⚪'; + const signalColor = indSignal.color || 'var(--tv-text-secondary)'; + const signalDate = indSignal.lastSignalDate ? new Date(indSignal.lastSignalDate * 1000).toLocaleString() : 'Waiting for crossover...'; + + return ` +
+ + + ${signalIcon} ${signalType} + +
+
+ + ${signalDate} +
+
+ +
+ + ${indSignal.strength}% +
+
+
+ + ${indSignal.reasoning} +
+ `; + })()} +
+ ` : ''} + ${meta?.inputs && meta.inputs.length > 0 ? `
Parameters
diff --git a/src/api/dashboard/static/js/ui/signals-calculator.js b/src/api/dashboard/static/js/ui/signals-calculator.js index 75c1589..7e17e24 100644 --- a/src/api/dashboard/static/js/ui/signals-calculator.js +++ b/src/api/dashboard/static/js/ui/signals-calculator.js @@ -236,18 +236,16 @@ function calculateMASignal(indicator, lastCandle, prevCandle, values) { let signalType, strength, reasoning; - if (close > ma * 1.02) { + if (close > ma) { signalType = SIGNAL_TYPES.BUY; strength = Math.min(60 + ((close - ma) / ma) * 500, 100); - reasoning = `Price (${close.toFixed(2)}) is strongly above ${maLabel} (${ma.toFixed(2)}), bullish trend`; - } else if (close < ma * 0.98) { + reasoning = `Price (${close.toFixed(2)}) is above ${maLabel} (${ma.toFixed(2)})`; + } else if (close < ma) { signalType = SIGNAL_TYPES.SELL; strength = Math.min(60 + ((ma - close) / ma) * 500, 100); - reasoning = `Price (${close.toFixed(2)}) is strongly below ${maLabel} (${ma.toFixed(2)}), bearish trend`; + reasoning = `Price (${close.toFixed(2)}) is below ${maLabel} (${ma.toFixed(2)})`; } else { - signalType = SIGNAL_TYPES.HOLD; - strength = 30; - reasoning = `Price (${close.toFixed(2)}) is near ${maLabel} (${ma.toFixed(2)}), sideways/consolidating`; + return null; } console.log('[calculateMASignal] Result:', signalType, strength); @@ -406,6 +404,36 @@ export function calculateAllIndicatorSignals() { const signal = calculateIndicatorSignal(indicator, candles, values); + let currentSignal = signal; + let lastSignalDate = indicator.lastSignalTimestamp || null; + let lastSignalType = indicator.lastSignalType || null; + + if (!currentSignal || !currentSignal.type) { + console.log('[Signals] No valid signal for', indicator.type, '- Using last signal if available'); + + if (lastSignalType && lastSignalDate) { + currentSignal = { + type: lastSignalType, + strength: 50, + value: candles[candles.length - 1]?.close, + reasoning: `No crossover (price equals MA)` + }; + } else { + console.log('[Signals] No previous signal available - Skipping'); + continue; + } + } else { + const currentCandleTimestamp = candles[candles.length - 1].time; + + if (currentSignal.type !== lastSignalType || !lastSignalType) { + console.log('[Signals] Signal changed for', indicator.type, ':', lastSignalType, '->', currentSignal.type); + lastSignalDate = currentCandleTimestamp; + lastSignalType = currentSignal.type; + indicator.lastSignalTimestamp = lastSignalDate; + indicator.lastSignalType = lastSignalType; + } + } + const label = indicator.type?.toUpperCase(); const params = indicator.params && typeof indicator.params === 'object' ? Object.entries(indicator.params) @@ -420,11 +448,12 @@ export function calculateAllIndicatorSignals() { label: label, params: params || null, type: indicator.type, - signal: signal.type, - strength: Math.round(signal.strength), - value: signal.value, - reasoning: signal.reasoning, - color: SIGNAL_COLORS[signal.type] + signal: currentSignal.type, + strength: Math.round(currentSignal.strength), + value: currentSignal.value, + reasoning: currentSignal.reasoning, + color: SIGNAL_COLORS[currentSignal.type], + lastSignalDate: lastSignalDate }); } diff --git a/src/api/dashboard/static/js/ui/simulation.js b/src/api/dashboard/static/js/ui/simulation.js deleted file mode 100644 index 201b111..0000000 --- a/src/api/dashboard/static/js/ui/simulation.js +++ /dev/null @@ -1,567 +0,0 @@ -import { ClientStrategyEngine } from '../strategies/index.js'; -import { HTSStrategyEngine } from '../strategies/hts-engine.js'; -import { addHTSVisualization } from './hts-visualizer.js'; -import { SimulationStorage } from './storage.js'; -import { downloadFile } from '../utils/index.js'; -import { showExportDialog, closeExportDialog, performExport } from './export.js'; - -let lastSimulationResults = null; - -export function getLastResults() { - return lastSimulationResults; -} - -export function setLastResults(results) { - lastSimulationResults = results; - window.lastSimulationResults = results; -} - -export async function runSimulation() { - const strategyConfig = getStrategyConfig(); - if (!strategyConfig) { - alert('Please select a strategy'); - return; - } - - const startDateInput = document.getElementById('simStartDate').value; - if (!startDateInput) { - alert('Please select a start date'); - return; - } - - const runBtn = document.getElementById('runSimBtn'); - runBtn.disabled = true; - runBtn.textContent = '⏳ Running...'; - - try { - const start = new Date(startDateInput); - const fetchStart = new Date(start.getTime() - 200 * 24 * 60 * 60 * 1000); - - if (!window.dashboard) { - throw new Error('Dashboard not initialized'); - } - const interval = window.dashboard.currentInterval; - const secondaryTF = document.getElementById('simSecondaryTF').value; - const riskPercent = parseFloat(document.getElementById('simRiskPercent').value); - const stopLossPercent = parseFloat(document.getElementById('simStopLoss').value); - - const timeframes = [interval]; - if (secondaryTF && secondaryTF !== '') { - timeframes.push(secondaryTF); - } - - const query = new URLSearchParams({ symbol: 'BTC', start: fetchStart.toISOString() }); - timeframes.forEach(tf => query.append('timeframes', tf)); - - console.log('Fetching candles with query:', query.toString()); - - const response = await fetch(`/api/v1/candles/bulk?${query.toString()}`); - - if (!response.ok) { - throw new Error(`API error: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - console.log('Candle data received:', data); - console.log('Looking for interval:', interval); - console.log('Available timeframes:', Object.keys(data)); - - if (!data[interval] || data[interval].length === 0) { - throw new Error(`No candle data available for ${interval} timeframe. Check if data exists in database.`); - } - - const candlesMap = { - [interval]: data[interval].map(c => ({ - time: Math.floor(new Date(c.time).getTime() / 1000), - open: parseFloat(c.open), - high: parseFloat(c.high), - low: parseFloat(c.low), - close: parseFloat(c.close) - })) - }; - - if (secondaryTF && data[secondaryTF]) { - candlesMap[secondaryTF] = data[secondaryTF].map(c => ({ - time: Math.floor(new Date(c.time).getTime() / 1000), - open: parseFloat(c.open), - high: parseFloat(c.high), - low: parseFloat(c.low), - close: parseFloat(c.close) - })); - } - - const engineConfig = { - id: strategyConfig.id, - params: strategyConfig.params, - timeframes: { primary: interval, secondary: secondaryTF ? [secondaryTF] : [] }, - indicators: [] - }; - - console.log('Building strategy config:'); - console.log(' Primary TF:', interval); - console.log(' Secondary TF:', secondaryTF); - console.log(' Available candles:', Object.keys(candlesMap)); - - let results; - - if (strategyConfig.id === 'hts_trend') { - console.log('Running HTS strategy simulation...'); - const htsEngine = new HTSStrategyEngine(strategyConfig.params); - - let oneMinCandles = null; - let h1hCandles = null; - - if (strategyConfig.params?.useAutoHTS) { - console.log('Fetching 1m candles for Auto HTS...'); - const oneMinQuery = `symbol=BTC&interval=1m&start=${fetchStart.toISOString()}&limit=10000`; - const oneMinResponse = await fetch(`/api/v1/candles?${oneMinQuery}`); - if (oneMinResponse.ok) { - const oneMinData = await oneMinResponse.json(); - if (oneMinData.candles && oneMinData.candles.length > 0) { - oneMinCandles = oneMinData.candles.map(c => ({ - time: Math.floor(new Date(c.time).getTime() / 1000), - open: parseFloat(c.open), - high: parseFloat(c.high), - low: parseFloat(c.low), - close: parseFloat(c.close), - volume: parseFloat(c.volume) - })); - console.log(`Got ${oneMinCandles.length} 1m candles`); - } - } - } - - if (strategyConfig.params?.use1HFilter) { - console.log('Fetching 1H candles for HTS 1H filter...'); - const h1hQuery = `symbol=BTC&interval=1h&start=${fetchStart.toISOString()}&limit=500`; - const h1hResponse = await fetch(`/api/v1/candles?${h1hQuery}`); - if (h1hResponse.ok) { - const h1hData = await h1hResponse.json(); - if (h1hData.candles && h1hData.candles.length > 0) { - h1hCandles = h1hData.candles.map(c => ({ - time: Math.floor(new Date(c.time).getTime() / 1000), - open: parseFloat(c.open), - high: parseFloat(c.high), - low: parseFloat(c.low), - close: parseFloat(c.close), - volume: parseFloat(c.volume) - })); - console.log(`Got ${h1hCandles.length} 1H candles`); - } - } - } - - results = htsEngine.runSimulation(candlesMap[interval], oneMinCandles, h1hCandles); - } else { - if (strategyConfig.id === 'ma_trend') { - const period = strategyConfig.params?.period || 44; - engineConfig.indicators.push({ - name: `ma${period}`, - type: 'sma', - params: { period: period }, - timeframe: interval - }); - if (secondaryTF) { - engineConfig.indicators.push({ - name: `ma${period}_${secondaryTF}`, - type: 'sma', - params: { period: period }, - timeframe: secondaryTF - }); - } - } - - console.log(' Indicators configured:', engineConfig.indicators.map(i => `${i.name} on ${i.timeframe}`)); - - const riskConfig = { - positionSizing: { method: 'percent', value: riskPercent }, - stopLoss: { enabled: true, method: 'percent', value: stopLossPercent } - }; - - const engine = new ClientStrategyEngine(); - results = engine.run(candlesMap, engineConfig, riskConfig, start); - } - - if (results.error) throw new Error(results.error); - - setLastResults({ - ...results, - config: { - strategyId: strategyConfig.id, - strategyName: window.availableStrategies?.find(s => s.id === strategyConfig.id)?.name || strategyConfig.id, - timeframe: interval, - secondaryTimeframe: secondaryTF, - startDate: startDateInput, - riskPercent: riskPercent, - stopLossPercent: stopLossPercent, - params: strategyConfig.params - }, - runAt: new Date().toISOString() - }); - - displayEnhancedResults(lastSimulationResults); - - document.getElementById('resultsSection').style.display = 'block'; - - if (window.dashboard && candlesMap[interval]) { - const chartData = candlesMap[interval].map(c => ({ - time: c.time, - open: c.open, - high: c.high, - low: c.low, - close: c.close - })); - window.dashboard.candleSeries.setData(chartData); - window.dashboard.allData.set(interval, chartData); - console.log(`Chart updated with ${chartData.length} candles from simulation range`); - } - - showSimulationMarkers(); - - if (strategyConfig.id === 'hts_trend' && results.htsData && window.dashboard) { - try { - console.log('Visualizing HTS channels...'); - const candles = window.dashboard.allData.get(interval) || candlesMap[interval]; - htsVisualizer = addHTSVisualization( - window.dashboard.chart, - window.dashboard.candleSeries, - results.htsData, - candles, - strategyConfig.params?.useAutoHTS - ); - console.log('HTS channels visualized'); - } catch (e) { - console.error('Error visualizing HTS channels:', e); - } - } - - } catch (error) { - console.error('Simulation error:', error); - alert('Simulation error: ' + error.message); - } finally { - runBtn.disabled = false; - runBtn.textContent = '▶ Run Simulation'; - } -} - -export function displayEnhancedResults(simulation) { - const results = simulation.results || simulation; - - document.getElementById('simTrades').textContent = results.total_trades || '0'; - document.getElementById('simWinRate').textContent = (results.win_rate || 0).toFixed(1) + '%'; - - const pnl = results.total_pnl || 0; - const pnlElement = document.getElementById('simPnL'); - pnlElement.textContent = (pnl >= 0 ? '+' : '') + '$' + pnl.toFixed(2); - pnlElement.style.color = pnl >= 0 ? '#4caf50' : '#f44336'; - - let grossProfit = 0; - let grossLoss = 0; - (results.trades || []).forEach(trade => { - if (trade.pnl > 0) grossProfit += trade.pnl; - else grossLoss += Math.abs(trade.pnl); - }); - const profitFactor = grossLoss > 0 ? (grossProfit / grossLoss).toFixed(2) : grossProfit > 0 ? '∞' : '0'; - document.getElementById('simProfitFactor').textContent = profitFactor; - - drawEquitySparkline(results); -} - -function drawEquitySparkline(results) { - const container = document.getElementById('equitySparkline'); - if (!container || !results.trades || results.trades.length === 0) { - container.innerHTML = '
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 = []; -let htsVisualizer = null; -let tradeMarkerSeries = []; - -export function showSimulationMarkers() { - const results = getLastResults(); - - if (!results || !window.dashboard) { - console.warn('Cannot show markers: no results or dashboard'); - return; - } - - const trades = results.trades || results.results?.trades || []; - const markers = []; - - clearSimulationMarkers(); - - console.log('Plotting trades:', trades.length); - console.log('Dashboard check:', { - dashboard: !!window.dashboard, - chart: !!window.dashboard?.chart, - candleSeries: !!window.dashboard?.candleSeries, - setMarkers: typeof window.dashboard?.candleSeries?.setMarkers - }); - -trades.forEach((trade, i) => { - let entryTime, exitTime; - - if (typeof trade.entryTime === 'number') { - entryTime = trade.entryTime; - } else { - entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000); - } - - if (typeof trade.exitTime === 'number') { - exitTime = trade.exitTime; - } else { - exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000); - } - - console.log(`[Trade ${i}] Entry: ${new Date(entryTime * 1000).toISOString()}, Exit: ${new Date(exitTime * 1000).toISOString()}`); - console.log(` Entry Price: $${trade.entryPrice.toFixed(2)} | Exit Price: $${trade.exitPrice.toFixed(2)} | PnL: ${trade.pnl > 0 ? '+' : ''}$${trade.pnl.toFixed(2)} (${trade.pnlPct.toFixed(1)}%)`); - - if (window.dashboard && window.dashboard.chart && window.dashboard.candleSeries) { - try { - const data = window.dashboard.candleSeries.data(); - if (!data || data.length === 0) { - console.warn('No chart data available for markers'); - return; - } - - const chartTimeRange = { - min: Math.min(...data.map(c => c.time)), - max: Math.max(...data.map(c => c.time)) - }; - - if (entryTime < chartTimeRange.min || entryTime > chartTimeRange.max) { - console.log(`Skipping trade ${i} - entry time outside chart range`); - return; - } - - if (exitTime < chartTimeRange.min || exitTime > chartTimeRange.max) { - console.log(`Skipping trade ${i} - exit time outside chart range`); - return; - } - - const buyMarkerSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, { - color: '#2196f3', - lineWidth: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false - }); - - const entryPrice = trade.entryPrice; - buyMarkerSeries.setData([ - { time: entryTime, value: entryPrice }, - { time: entryTime + 1, value: entryPrice } - ]); - - tradeMarkerSeries.push({ series: buyMarkerSeries }); - - const pnlColor = trade.pnl > 0 ? '#4caf50' : '#f44336'; - - const sellMarkerSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, { - color: pnlColor, - lineWidth: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false - }); - - const exitPrice = trade.exitPrice; - sellMarkerSeries.setData([ - { time: exitTime, value: exitPrice }, - { time: exitTime + 1, value: exitPrice } - ]); - - tradeMarkerSeries.push({ series: sellMarkerSeries }); - - const lineSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, { - color: 'rgba(33, 150, 243, 0.3)', - lineWidth: 1, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false - }); - - lineSeries.setData([ - { time: entryTime, value: trade.entryPrice }, - { time: exitTime, value: trade.exitPrice } - ]); - - tradeLineSeries.push(lineSeries); - } catch (e) { - console.error(`Error adding marker for trade ${i}:`, e.message); - } - } - }); - - markers.sort((a, b) => a.time - b.time); - - const candleSeries = window.dashboard.candleSeries; - if (candleSeries && typeof candleSeries.setMarkers === 'function') { - try { - console.log('Setting', markers.length, 'markers on candle series'); - - const currentData = candleSeries.data(); - const markerTimes = new Set(markers.map(m => m.time)); - const candleTimes = new Set(currentData.map(c => c.time)); - - const orphanMarkers = markers.filter(m => !candleTimes.has(m.time)); - if (orphanMarkers.length > 0) { - console.warn(`${orphanMarkers.length} markers have timestamps not found in chart data:`); - orphanMarkers.forEach(m => { - console.warn(' - Time:', m.time, '(', new Date(m.time * 1000).toISOString(), ')'); - }); - console.log('First few candle times:', Array.from(candleTimes).slice(0, 5)); - console.log('Last few candle times:', Array.from(candleTimes).slice(-5)); - } - - candleSeries.setMarkers(markers); - console.log('Markers set successfully'); - } catch (e) { - console.error('Error setting markers:', e); - console.error('Marker methods available:', Object.getOwnPropertyNames(Object.getPrototypeOf(candleSeries)).filter(m => m.includes('marker'))); - } - } else { - console.warn('Markers not supported on this chart version - trades will be logged but not visualized on chart'); - } - - console.log(`Plotted ${trades.length} trades with connection lines`); - console.log(`Trade markers created for ${trades.length} positions`); -} - -export function clearSimulationMarkers() { - try { - if (window.dashboard && window.dashboard.candleSeries && typeof window.dashboard.candleSeries.setMarkers === 'function') { - window.dashboard.candleSeries.setMarkers([]); - } - } catch (e) { - // Ignore errors clearing markers - } - - try { - tradeLineSeries.forEach(series => { - try { - if (window.dashboard && window.dashboard.chart) { - window.dashboard.chart.removeSeries(series); - } - } catch (e) { - // Series might already be removed - } - }); - } catch (e) { - // Ignore errors removing series - } - - tradeLineSeries = []; - - try { - if (htsVisualizer) { - htsVisualizer.clear(); - htsVisualizer = null; - } - } catch (e) { - // Ignore errors clearing HTS visualizer - } - - // Clear trade marker series - try { - tradeMarkerSeries.forEach(marker => { - try { - if (window.dashboard && window.dashboard.chart && marker.series) { - window.dashboard.chart.removeSeries(marker.series); - } - } catch (e) { - // Ignore errors - } - }); - tradeMarkerSeries = []; - } catch (e) { - // Ignore errors - } -} - -export function clearSimulationResults() { - clearSimulationMarkers(); - - setLastResults(null); - - const resultsSection = document.getElementById('resultsSection'); - if (resultsSection) { - resultsSection.style.display = 'none'; - } - - const simTrades = document.getElementById('simTrades'); - const simWinRate = document.getElementById('simWinRate'); - const simPnL = document.getElementById('simPnL'); - const simProfitFactor = document.getElementById('simProfitFactor'); - const equitySparkline = document.getElementById('equitySparkline'); - - if (simTrades) simTrades.textContent = '0'; - if (simWinRate) simWinRate.textContent = '0%'; - if (simPnL) { - simPnL.textContent = '$0.00'; - simPnL.style.color = ''; - } - if (simProfitFactor) simProfitFactor.textContent = '0'; - if (equitySparkline) equitySparkline.innerHTML = ''; -} - -function getStrategyConfig() { - const strategyId = window.currentStrategy; - if (!strategyId) return null; - - const params = {}; - const paramDefs = window.StrategyParams?.[strategyId] || []; - - paramDefs.forEach(def => { - const input = document.getElementById(`param_${def.name}`); - if (input) { - params[def.name] = def.type === 'number' ? parseFloat(input.value) : input.value; - } - }); - - return { - id: strategyId, - params: params - }; -} diff --git a/src/api/dashboard/static/js/ui/storage.js b/src/api/dashboard/static/js/ui/storage.js deleted file mode 100644 index 48b2fa3..0000000 --- a/src/api/dashboard/static/js/ui/storage.js +++ /dev/null @@ -1,47 +0,0 @@ -export const SimulationStorage = { - STORAGE_KEY: 'btc_bot_simulations', - - getAll() { - try { - const data = localStorage.getItem(this.STORAGE_KEY); - return data ? JSON.parse(data) : []; - } catch (e) { - console.error('Error reading simulations:', e); - return []; - } - }, - - save(simulation) { - try { - const simulations = this.getAll(); - simulation.id = simulation.id || 'sim_' + Date.now(); - simulation.createdAt = new Date().toISOString(); - simulations.push(simulation); - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(simulations)); - return simulation.id; - } catch (e) { - console.error('Error saving simulation:', e); - return null; - } - }, - - delete(id) { - try { - let simulations = this.getAll(); - simulations = simulations.filter(s => s.id !== id); - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(simulations)); - return true; - } catch (e) { - console.error('Error deleting simulation:', e); - return false; - } - }, - - get(id) { - return this.getAll().find(s => s.id === id); - }, - - clear() { - localStorage.removeItem(this.STORAGE_KEY); - } -}; diff --git a/src/api/dashboard/static/js/ui/strategies-panel.js b/src/api/dashboard/static/js/ui/strategies-panel.js deleted file mode 100644 index 140cfb2..0000000 --- a/src/api/dashboard/static/js/ui/strategies-panel.js +++ /dev/null @@ -1,318 +0,0 @@ -import { StrategyParams } from '../strategies/config.js'; - -let currentStrategy = null; - -export function getCurrentStrategy() { - return currentStrategy; -} - -export function setCurrentStrategy(strategyId) { - currentStrategy = strategyId; - window.currentStrategy = strategyId; -} - -export function renderStrategies(strategies) { - const container = document.getElementById('strategyList'); - - if (!strategies || strategies.length === 0) { - container.innerHTML = '
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}`); - } - - let data = await response.json(); - console.log('Strategies loaded:', data); - - if (!data.strategies) { - throw new Error('Invalid response format: missing strategies array'); - } - - data.strategies = data.strategies.filter(s => s.id !== 'hts_trend'); - - data.strategies.push({ - id: 'hts_trend', - name: 'HTS Trend Strategy', - description: 'Higher Timeframe Trend System with Auto HTS, 1H filters, and channel-based entries/exits', - required_indicators: [] - }); - - window.availableStrategies = data.strategies; - renderStrategies(data.strategies); - } catch (error) { - console.error('Error loading strategies:', error); - - let errorMessage = error.message; - if (error.name === 'AbortError') { - errorMessage = 'Request timeout - API server not responding'; - } else if (error.message.includes('Failed to fetch')) { - errorMessage = 'Cannot connect to API server - is it running?'; - } - - document.getElementById('strategyList').innerHTML = - `
- ${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/server.py b/src/api/server.py index cec7c17..11d2419 100644 --- a/src/api/server.py +++ b/src/api/server.py @@ -25,12 +25,6 @@ from pydantic import BaseModel, Field # Imports for backtest runner from src.data_collector.database import DatabaseManager from src.data_collector.indicator_engine import IndicatorEngine, IndicatorConfig -from src.data_collector.brain import Brain -from src.data_collector.backtester import Backtester - -# Imports for strategy discovery -import importlib -from src.strategies.base import BaseStrategy logging.basicConfig(level=logging.INFO) @@ -103,45 +97,6 @@ async def root(): } -@app.get("/api/v1/strategies") -async def list_strategies(response: Response): - """List all available trading strategies with metadata""" - # Prevent caching - response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" - response.headers["Pragma"] = "no-cache" - response.headers["Expires"] = "0" - - # Strategy registry from brain.py - strategy_registry = { - "ma_trend": "src.strategies.ma_strategy.MAStrategy", - } - - strategies = [] - - for strategy_id, class_path in strategy_registry.items(): - try: - module_path, class_name = class_path.rsplit('.', 1) - module = importlib.import_module(module_path) - strategy_class = getattr(module, class_name) - - # Instantiate to get metadata - strategy_instance = strategy_class() - - strategies.append({ - "id": strategy_id, - "name": strategy_instance.display_name, - "description": strategy_instance.description, - "required_indicators": strategy_instance.required_indicators - }) - except Exception as e: - logger.error(f"Failed to load strategy {strategy_id}: {e}") - - return { - "strategies": strategies, - "count": len(strategies) - } - - @app.get("/api/v1/candles") async def get_candles( symbol: str = Query("BTC", description="Trading pair symbol"), @@ -417,50 +372,6 @@ async def list_backtests(symbol: Optional[str] = None, limit: int = 20): return [dict(row) for row in rows] -class BacktestRequest(BaseModel): - symbol: str = "BTC" - intervals: list[str] = ["37m"] - start_date: str = "2025-01-01" # ISO date - end_date: Optional[str] = None - - -async def run_backtest_task(req: BacktestRequest): - """Background task to run backtest""" - db = DatabaseManager( - host=DB_HOST, port=DB_PORT, database=DB_NAME, - user=DB_USER, password=DB_PASSWORD - ) - await db.connect() - - try: - # Load configs (hardcoded for now to match main.py) - configs = [ - IndicatorConfig("ma44", "sma", 44, req.intervals), - IndicatorConfig("ma125", "sma", 125, req.intervals) - ] - - engine = IndicatorEngine(db, configs) - brain = Brain(db, engine) - backtester = Backtester(db, engine, brain) - - start = datetime.fromisoformat(req.start_date).replace(tzinfo=timezone.utc) - end = datetime.fromisoformat(req.end_date).replace(tzinfo=timezone.utc) if req.end_date else datetime.now(timezone.utc) - - await backtester.run(req.symbol, req.intervals, start, end) - - except Exception as e: - logger.error(f"Backtest failed: {e}") - finally: - await db.disconnect() - - -@app.post("/api/v1/backtests") -async def trigger_backtest(req: BacktestRequest, background_tasks: BackgroundTasks): - """Start a backtest in the background""" - background_tasks.add_task(run_backtest_task, req) - return {"message": "Backtest started", "params": req.dict()} - - @app.get("/api/v1/ta") async def get_technical_analysis( symbol: str = Query("BTC", description="Trading pair symbol"), diff --git a/src/data_collector/__init__.py b/src/data_collector/__init__.py index 40f086e..cf2dd9c 100644 --- a/src/data_collector/__init__.py +++ b/src/data_collector/__init__.py @@ -6,7 +6,6 @@ from .backfill import HyperliquidBackfill from .custom_timeframe_generator import CustomTimeframeGenerator from .indicator_engine import IndicatorEngine, IndicatorConfig from .brain import Brain, Decision -from .backtester import Backtester __all__ = [ 'HyperliquidWebSocket', @@ -18,6 +17,5 @@ __all__ = [ 'IndicatorEngine', 'IndicatorConfig', 'Brain', - 'Decision', - 'Backtester' + 'Decision' ] diff --git a/src/data_collector/backtester.py b/src/data_collector/backtester.py deleted file mode 100644 index c3b7b8c..0000000 --- a/src/data_collector/backtester.py +++ /dev/null @@ -1,391 +0,0 @@ -""" -Backtester - Historical replay driver for IndicatorEngine + Brain -Iterates over stored candle data to simulate live trading decisions -""" - -import asyncio -import json -import logging -from datetime import datetime, timezone -from typing import Dict, List, Optional, Any -from uuid import uuid4 - -from .database import DatabaseManager -from .indicator_engine import IndicatorEngine, IndicatorConfig -from .brain import Brain, Decision -from .simulator import Account -from src.strategies.base import SignalType - -logger = logging.getLogger(__name__) - - -class Backtester: - """ - Replays historical candle data through IndicatorEngine and Brain. - Uses Simulator (Account) to track PnL, leverage, and fees. - """ - - def __init__( - self, - db: DatabaseManager, - indicator_engine: IndicatorEngine, - brain: Brain, - ): - self.db = db - self.indicator_engine = indicator_engine - self.brain = brain - self.account = Account(initial_balance=1000.0) - - async def run( - self, - symbol: str, - intervals: List[str], - start: datetime, - end: datetime, - config: Optional[Dict[str, Any]] = None, - ) -> str: - """ - Run a full backtest over the given time range. - """ - backtest_id = str(uuid4()) - - logger.info( - f"Starting backtest {backtest_id}: {symbol} " - f"{intervals} from {start} to {end}" - ) - - # Reset brain state - self.brain.reset_state() - - # Reset account for this run - self.account = Account(initial_balance=1000.0) - - # Store the run metadata - await self._save_run_start( - backtest_id, symbol, intervals, start, end, config - ) - - total_decisions = 0 - - for interval in intervals: - # Only process intervals that have indicators configured - configured = self.indicator_engine.get_configured_intervals() - if interval not in configured: - logger.warning( - f"Skipping interval {interval}: no indicators configured" - ) - continue - - # Get all candle timestamps in range - timestamps = await self._get_candle_timestamps( - symbol, interval, start, end - ) - - if not timestamps: - logger.warning( - f"No candles found for {symbol}/{interval} in range" - ) - continue - - logger.info( - f"Backtest {backtest_id}: processing {len(timestamps)} " - f"{interval} candles..." - ) - - for i, ts in enumerate(timestamps): - # 1. Compute indicators - raw_indicators = await self.indicator_engine.compute_at( - symbol, interval, ts - ) - indicators = {k: v for k, v in raw_indicators.items() if v is not None} - - # 2. Get Current Position info for Strategy - current_pos = self.account.get_position_dict() - - # 3. Brain Evaluate - decision: Decision = await self.brain.evaluate( - symbol=symbol, - interval=interval, - timestamp=ts, - indicators=indicators, - backtest_id=backtest_id, - current_position=current_pos - ) - - # 4. Execute Decision in Simulator - self._execute_decision(decision) - - total_decisions += 1 - - if (i + 1) % 200 == 0: - logger.info( - f"Backtest {backtest_id}: {i + 1}/{len(timestamps)} " - f"{interval} candles processed. Eq: {self.account.equity:.2f}" - ) - await asyncio.sleep(0.01) - - # Compute and store summary results from Simulator - results = self.account.get_stats() - results['total_evaluations'] = total_decisions - - await self._save_run_results(backtest_id, results) - - logger.info( - f"Backtest {backtest_id} complete. Final Balance: {results['final_balance']:.2f}" - ) - - return backtest_id - - def _execute_decision(self, decision: Decision): - """Translate Brain decision into Account action""" - price = decision.price_at_decision - time = decision.time - - # Open Long - if decision.decision_type == SignalType.OPEN_LONG.value: - self.account.open_position(time, 'long', price, leverage=1.0) # Todo: Configurable leverage - - # Open Short - elif decision.decision_type == SignalType.OPEN_SHORT.value: - self.account.open_position(time, 'short', price, leverage=1.0) - - # Close Long (only if we are long) - elif decision.decision_type == SignalType.CLOSE_LONG.value: - if self.account.current_position and self.account.current_position.side == 'long': - self.account.close_position(time, price) - - # Close Short (only if we are short) - elif decision.decision_type == SignalType.CLOSE_SHORT.value: - if self.account.current_position and self.account.current_position.side == 'short': - self.account.close_position(time, price) - - # Update equity mark-to-market - self.account.update_equity(price) - - async def _get_candle_timestamps( - self, - symbol: str, - interval: str, - start: datetime, - end: datetime, - ) -> List[datetime]: - """Get all candle timestamps in a range, ordered chronologically""" - async with self.db.acquire() as conn: - rows = await conn.fetch(""" - SELECT time FROM candles - WHERE symbol = $1 AND interval = $2 - AND time >= $3 AND time <= $4 - ORDER BY time ASC - """, symbol, interval, start, end) - - return [row["time"] for row in rows] - - async def _save_run_start( - self, - backtest_id: str, - symbol: str, - intervals: List[str], - start: datetime, - end: datetime, - config: Optional[Dict[str, Any]], - ) -> None: - """Store backtest run metadata at start""" - async with self.db.acquire() as conn: - await conn.execute(""" - INSERT INTO backtest_runs ( - id, strategy, symbol, start_time, end_time, - intervals, config - ) - VALUES ($1, $2, $3, $4, $5, $6, $7) - """, - backtest_id, - self.brain.strategy_name, - symbol, - start, - end, - intervals, - json.dumps(config) if config else None, - ) - - async def _compute_results(self, backtest_id, symbol): - """Deprecated: Logic moved to Account class""" - return {} - - async def _save_run_results( - self, - backtest_id: str, - results: Dict[str, Any], - ) -> None: - """Update backtest run with final results""" - # Remove trades list from stored results (can be large) - stored_results = {k: v for k, v in results.items() if k != "trades"} - - async with self.db.acquire() as conn: - await conn.execute(""" - UPDATE backtest_runs - SET results = $1 - WHERE id = $2 - """, json.dumps(stored_results), backtest_id) - - async def get_run(self, backtest_id: str) -> Optional[Dict[str, Any]]: - """Get a specific backtest run with results""" - async with self.db.acquire() as conn: - row = await conn.fetchrow(""" - SELECT id, strategy, symbol, start_time, end_time, - intervals, config, results, created_at - FROM backtest_runs - WHERE id = $1 - """, backtest_id) - - return dict(row) if row else None - - async def list_runs( - self, - symbol: Optional[str] = None, - limit: int = 20, - ) -> List[Dict[str, Any]]: - """List recent backtest runs""" - async with self.db.acquire() as conn: - if symbol: - rows = await conn.fetch(""" - SELECT id, strategy, symbol, start_time, end_time, - intervals, results, created_at - FROM backtest_runs - WHERE symbol = $1 - ORDER BY created_at DESC - LIMIT $2 - """, symbol, limit) - else: - rows = await conn.fetch(""" - SELECT id, strategy, symbol, start_time, end_time, - intervals, results, created_at - FROM backtest_runs - ORDER BY created_at DESC - LIMIT $1 - """, limit) - - return [dict(row) for row in rows] - - async def cleanup_run(self, backtest_id: str) -> int: - """Delete all decisions and metadata for a backtest run""" - async with self.db.acquire() as conn: - result = await conn.execute(""" - DELETE FROM decisions WHERE backtest_id = $1 - """, backtest_id) - - await conn.execute(""" - DELETE FROM backtest_runs WHERE id = $1 - """, backtest_id) - - deleted = int(result.split()[-1]) if result else 0 - logger.info( - f"Cleaned up backtest {backtest_id}: " - f"{deleted} decisions deleted" - ) - return deleted - - -async def main(): - """CLI entry point for running backtests""" - import argparse - import os - - parser = argparse.ArgumentParser( - description="Run backtest on historical data" - ) - parser.add_argument( - "--symbol", default="BTC", help="Symbol (default: BTC)" - ) - parser.add_argument( - "--intervals", nargs="+", default=["37m"], - help="Intervals to backtest (default: 37m)" - ) - parser.add_argument( - "--start", required=True, - help="Start date (ISO format, e.g., 2025-01-01)" - ) - parser.add_argument( - "--end", default=None, - help="End date (ISO format, default: now)" - ) - parser.add_argument( - "--db-host", default=os.getenv("DB_HOST", "localhost"), - ) - parser.add_argument( - "--db-port", type=int, default=int(os.getenv("DB_PORT", 5432)), - ) - parser.add_argument( - "--db-name", default=os.getenv("DB_NAME", "btc_data"), - ) - parser.add_argument( - "--db-user", default=os.getenv("DB_USER", "btc_bot"), - ) - parser.add_argument( - "--db-password", default=os.getenv("DB_PASSWORD", ""), - ) - - args = parser.parse_args() - - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - ) - - # Parse dates - start = datetime.fromisoformat(args.start).replace(tzinfo=timezone.utc) - end = ( - datetime.fromisoformat(args.end).replace(tzinfo=timezone.utc) - if args.end - else datetime.now(timezone.utc) - ) - - # Initialize components - db = DatabaseManager( - host=args.db_host, - port=args.db_port, - database=args.db_name, - user=args.db_user, - password=args.db_password, - ) - await db.connect() - - try: - # Default indicator configs (MA44 + MA125 on selected intervals) - configs = [ - IndicatorConfig("ma44", "sma", 44, args.intervals), - IndicatorConfig("ma125", "sma", 125, args.intervals), - ] - - indicator_engine = IndicatorEngine(db, configs) - brain = Brain(db, indicator_engine) - backtester = Backtester(db, indicator_engine, brain) - - # Run the backtest - backtest_id = await backtester.run( - symbol=args.symbol, - intervals=args.intervals, - start=start, - end=end, - ) - - # Print results - run = await backtester.get_run(backtest_id) - if run and run.get("results"): - results = json.loads(run["results"]) if isinstance(run["results"], str) else run["results"] - print("\n=== Backtest Results ===") - print(f"ID: {backtest_id}") - print(f"Strategy: {run['strategy']}") - print(f"Period: {run['start_time']} to {run['end_time']}") - print(f"Intervals: {run['intervals']}") - print(f"Total evaluations: {results.get('total_evaluations', 0)}") - print(f"Total trades: {results.get('total_trades', 0)}") - print(f"Win rate: {results.get('win_rate', 0)}%") - print(f"Total P&L: {results.get('total_pnl_pct', 0)}%") - print(f"Final Balance: {results.get('final_balance', 0)}") - - finally: - await db.disconnect() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/data_collector/brain.py b/src/data_collector/brain.py index 37ee6fe..a8a7816 100644 --- a/src/data_collector/brain.py +++ b/src/data_collector/brain.py @@ -1,49 +1,25 @@ """ -Brain - Strategy evaluation and decision logging -Pure strategy logic separated from DB I/O for testability +Brain - Simplified indicator evaluation """ import json import logging from dataclasses import dataclass from datetime import datetime, timezone -from typing import Dict, Optional, Any, List, Callable +from typing import Dict, Optional, Any, List 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__) -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: - """Load a strategy instance from registry""" - if strategy_name not in STRATEGY_REGISTRY: - logger.warning(f"Strategy {strategy_name} not found, defaulting to ma_trend") - strategy_name = "ma_trend" - - factory = STRATEGY_REGISTRY[strategy_name] - return factory() - @dataclass class Decision: """A single brain evaluation result""" time: datetime symbol: str interval: str - decision_type: str # "buy", "sell", "hold" -> Now maps to SignalType + decision_type: str # "buy", "sell", "hold" strategy: str confidence: float price_at_decision: float @@ -71,21 +47,21 @@ class Decision: class Brain: """ - Evaluates market conditions using a loaded Strategy. + Evaluates market conditions using indicators. + Simplified version without complex strategy plug-ins. """ def __init__( self, db: DatabaseManager, indicator_engine: IndicatorEngine, - strategy: str = "ma44_strategy", + strategy: str = "default", ): self.db = db self.indicator_engine = indicator_engine self.strategy_name = strategy - self.active_strategy: BaseStrategy = load_strategy(strategy) - logger.info(f"Brain initialized with strategy: {self.active_strategy.name}") + logger.info("Brain initialized (Simplified)") async def evaluate( self, @@ -120,23 +96,19 @@ class Brain: "volume": float(candle["volume"]), } - # Delegate to Strategy - signal: StrategySignal = self.active_strategy.analyze( - candle_dict, indicators, current_position - ) - - # Build decision + # Simple crossover logic example if needed, otherwise just return HOLD + # For now, we just return a neutral decision as "Strategies" are removed decision = Decision( time=timestamp, symbol=symbol, interval=interval, - decision_type=signal.type.value, + decision_type="hold", strategy=self.strategy_name, - confidence=signal.confidence, + confidence=0.0, price_at_decision=price, indicator_snapshot=indicators, candle_snapshot=candle_dict, - reasoning=signal.reasoning, + reasoning="Strategy logic removed - Dashboard shows indicators", backtest_id=backtest_id, ) @@ -178,8 +150,6 @@ class Brain: async def _store_decision(self, decision: Decision) -> None: """Write decision to the decisions table""" - # Note: We might want to skip writing every single HOLD to DB to save space if simulating millions of candles - # But keeping it for now for full traceability async with self.db.acquire() as conn: await conn.execute(""" INSERT INTO decisions ( diff --git a/src/data_collector/simulator.py b/src/data_collector/simulator.py deleted file mode 100644 index c02d0ba..0000000 --- a/src/data_collector/simulator.py +++ /dev/null @@ -1,160 +0,0 @@ -""" -Simulator -Handles account accounting, leverage, fees, and position management for backtesting. -""" - -from dataclasses import dataclass -from typing import Optional, List, Dict, Any -from datetime import datetime -from .brain import Decision # We might need to decouple this later, but reusing for now - -@dataclass -class Trade: - entry_time: datetime - exit_time: Optional[datetime] - side: str # 'long' or 'short' - entry_price: float - exit_price: Optional[float] - size: float # Quantity of asset - leverage: float - pnl: float = 0.0 - pnl_percent: float = 0.0 - fees: float = 0.0 - status: str = 'open' # 'open', 'closed' - -class Account: - def __init__(self, initial_balance: float = 1000.0, maker_fee: float = 0.0002, taker_fee: float = 0.0005): - self.initial_balance = initial_balance - self.balance = initial_balance - self.equity = initial_balance - self.maker_fee = maker_fee - self.taker_fee = taker_fee - self.trades: List[Trade] = [] - self.current_position: Optional[Trade] = None - self.margin_used = 0.0 - - def update_equity(self, current_price: float): - """Update equity based on unrealized PnL of current position""" - if not self.current_position: - self.equity = self.balance - return - - trade = self.current_position - if trade.side == 'long': - unrealized_pnl = (current_price - trade.entry_price) * trade.size - else: - unrealized_pnl = (trade.entry_price - current_price) * trade.size - - self.equity = self.balance + unrealized_pnl - - def open_position(self, time: datetime, side: str, price: float, leverage: float = 1.0, portion: float = 1.0): - """ - Open a position. - portion: 0.0 to 1.0 (percentage of available balance to use) - """ - if self.current_position: - # Already have a position, ignore for now (or could add to it) - return - - # Calculate position size - # Margin = (Balance * portion) - # Position Value = Margin * Leverage - # Size = Position Value / Price - - margin_to_use = self.balance * portion - position_value = margin_to_use * leverage - size = position_value / price - - # Fee (Taker) - fee = position_value * self.taker_fee - self.balance -= fee # Deduct fee immediately - - self.current_position = Trade( - entry_time=time, - exit_time=None, - side=side, - entry_price=price, - exit_price=None, - size=size, - leverage=leverage, - fees=fee - ) - self.margin_used = margin_to_use - - def close_position(self, time: datetime, price: float): - """Close the current position""" - if not self.current_position: - return - - trade = self.current_position - position_value = trade.size * price - - # Calculate PnL - if trade.side == 'long': - pnl = (price - trade.entry_price) * trade.size - pnl_pct = (price - trade.entry_price) / trade.entry_price * trade.leverage * 100 - else: - pnl = (trade.entry_price - price) * trade.size - pnl_pct = (trade.entry_price - price) / trade.entry_price * trade.leverage * 100 - - # Fee (Taker) - fee = position_value * self.taker_fee - self.balance -= fee - trade.fees += fee - - # Update Balance - self.balance += pnl - self.margin_used = 0.0 - - # Update Trade Record - trade.exit_time = time - trade.exit_price = price - trade.pnl = pnl - trade.pnl_percent = pnl_pct - trade.status = 'closed' - - self.trades.append(trade) - self.current_position = None - self.equity = self.balance - - def get_position_dict(self) -> Optional[Dict[str, Any]]: - if not self.current_position: - return None - return { - 'type': self.current_position.side, - 'entry_price': self.current_position.entry_price, - 'size': self.current_position.size, - 'leverage': self.current_position.leverage - } - - def get_stats(self) -> Dict[str, Any]: - wins = [t for t in self.trades if t.pnl > 0] - losses = [t for t in self.trades if t.pnl <= 0] - - total_pnl = self.balance - self.initial_balance - total_pnl_pct = (total_pnl / self.initial_balance) * 100 - - return { - "initial_balance": self.initial_balance, - "final_balance": self.balance, - "total_pnl": total_pnl, - "total_pnl_pct": total_pnl_pct, - "total_trades": len(self.trades), - "win_count": len(wins), - "loss_count": len(losses), - "win_rate": (len(wins) / len(self.trades) * 100) if self.trades else 0.0, - "max_drawdown": 0.0, # Todo: implement DD tracking - "trades": [ - { - "entry_time": t.entry_time.isoformat(), - "exit_time": t.exit_time.isoformat() if t.exit_time else None, - "side": t.side, - "entry_price": t.entry_price, - "exit_price": t.exit_price, - "pnl": t.pnl, - "pnl_pct": t.pnl_percent, - "fees": t.fees - } - for t in self.trades - ] - } diff --git a/src/strategies/base.py b/src/strategies/base.py deleted file mode 100644 index dc56986..0000000 --- a/src/strategies/base.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Base Strategy Interface -All strategies must inherit from this class. -""" - -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import Dict, Any, List, Optional -from enum import Enum - -class SignalType(Enum): - OPEN_LONG = "open_long" - OPEN_SHORT = "open_short" - CLOSE_LONG = "close_long" - CLOSE_SHORT = "close_short" - HOLD = "hold" - -@dataclass -class StrategySignal: - type: SignalType - confidence: float - reasoning: str - -class BaseStrategy(ABC): - def __init__(self, config: Optional[Dict[str, Any]] = None): - self.config = config or {} - - @property - @abstractmethod - def name(self) -> str: - """Unique identifier for the strategy""" - pass - - @property - @abstractmethod - def required_indicators(self) -> List[str]: - """List of indicator names required by this strategy (e.g. ['ma44'])""" - pass - - @property - @abstractmethod - def display_name(self) -> str: - """User-friendly name for display in UI (e.g. 'MA44 Crossover')""" - pass - - @property - @abstractmethod - def description(self) -> str: - """Detailed description of how the strategy works""" - pass - - @abstractmethod - def analyze( - self, - candle: Dict[str, Any], - indicators: Dict[str, float], - current_position: Optional[Dict[str, Any]] = None - ) -> StrategySignal: - """ - Analyze market data and return a trading signal. - - Args: - candle: Dictionary containing 'close', 'open', 'high', 'low', 'volume', 'time' - indicators: Dictionary of pre-computed indicator values - current_position: Details about current open position (if any) - {'type': 'long'/'short', 'entry_price': float, 'size': float} - """ - pass diff --git a/src/strategies/ma_strategy.py b/src/strategies/ma_strategy.py deleted file mode 100644 index 8a69ab8..0000000 --- a/src/strategies/ma_strategy.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Moving Average Strategy -Configurable trend following strategy. -- Long when Price > MA(period) -- Short when Price < MA(period) -""" - -from typing import Dict, Any, List, Optional -from .base import BaseStrategy, StrategySignal, SignalType - -class MAStrategy(BaseStrategy): - """ - Configurable Moving Average Strategy. - - Config: - - period: int - MA period (default: 44) - """ - - DEFAULT_PERIOD = 44 - - @property - def name(self) -> str: - return "ma_trend" - - @property - def required_indicators(self) -> List[str]: - # Dynamic based on config - period = self.config.get('period', self.DEFAULT_PERIOD) - return [f"ma{period}"] - - @property - def display_name(self) -> str: - return "MA Strategy" - - @property - def description(self) -> str: - return "Configurable Moving Average strategy. Parameters: period (5-500, default: 44). Goes long when price > MA(period), short when price < MA(period). Optional multi-timeframe trend filter available." - - def analyze( - self, - candle: Dict[str, Any], - indicators: Dict[str, float], - current_position: Optional[Dict[str, Any]] = None - ) -> StrategySignal: - - period = self.config.get('period', self.DEFAULT_PERIOD) - ma_key = f"ma{period}" - - price = candle['close'] - ma_value = indicators.get(ma_key) - - if ma_value is None: - return StrategySignal(SignalType.HOLD, 0.0, f"MA{period} not available") - - # Current position state - is_long = current_position and current_position.get('type') == 'long' - is_short = current_position and current_position.get('type') == 'short' - - # Logic: Price > MA -> Bullish - if price > ma_value: - if is_long: - return StrategySignal(SignalType.HOLD, 1.0, f"Price {price:.2f} > MA{period} {ma_value:.2f}. Stay Long.") - elif is_short: - return StrategySignal(SignalType.CLOSE_SHORT, 1.0, f"Price {price:.2f} crossed above MA{period} {ma_value:.2f}. Close Short.") - else: - return StrategySignal(SignalType.OPEN_LONG, 1.0, f"Price {price:.2f} > MA{period} {ma_value:.2f}. Open Long.") - - # Logic: Price < MA -> Bearish - elif price < ma_value: - if is_short: - return StrategySignal(SignalType.HOLD, 1.0, f"Price {price:.2f} < MA{period} {ma_value:.2f}. Stay Short.") - elif is_long: - return StrategySignal(SignalType.CLOSE_LONG, 1.0, f"Price {price:.2f} crossed below MA{period} {ma_value:.2f}. Close Long.") - else: - return StrategySignal(SignalType.OPEN_SHORT, 1.0, f"Price {price:.2f} < MA{period} {ma_value:.2f}. Open Short.") - - return StrategySignal(SignalType.HOLD, 0.0, f"Price == MA{period}")