From eafba745b190f7e64cf06269aef62eed85f121ad Mon Sep 17 00:00:00 2001 From: BTC Bot Date: Tue, 17 Feb 2026 10:39:39 +0100 Subject: [PATCH] local strategy --- AGENTS.md | 50 ++++++++- src/api/dashboard/static/index.html | 152 +++++++++++++++------------- src/api/server.py | 12 ++- src/data_collector/backfill.py | 4 +- src/strategies/ma125_strategy.py | 63 ------------ src/strategies/ma44_strategy.py | 63 ------------ src/strategies/ma_strategy.py | 77 ++++++++++++++ 7 files changed, 218 insertions(+), 203 deletions(-) delete mode 100644 src/strategies/ma125_strategy.py delete mode 100644 src/strategies/ma44_strategy.py create mode 100644 src/strategies/ma_strategy.py diff --git a/AGENTS.md b/AGENTS.md index 4c2bbb4..45428e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,10 +41,13 @@ src/ │ ├── custom_timeframe_generator.py # 37m, 148m, 1d aggregation │ ├── indicator_engine.py # SMA/EMA computation & storage │ ├── brain.py # Strategy evaluation & decision logging -│ └── backtester.py # Historical replay driver -└── api/ - ├── server.py # FastAPI app, endpoints for data/backtests - └── dashboard/static/index.html # Real-time web dashboard +│ └── backtester.py # Historical replay driver (server-side) +├── api/ +│ ├── server.py # FastAPI app, endpoints for data/backtests +│ └── dashboard/static/index.html # Real-time web dashboard with client-side simulation +└── strategies/ + ├── base.py # Base strategy interface + └── ma_strategy.py # Configurable MA strategy (period: 5-500) config/data_config.yaml # Operational config & indicator settings docker/ # Docker orchestration & init-scripts scripts/ # Deploy, backup, & utility scripts @@ -52,17 +55,30 @@ scripts/ # Deploy, backup, & utility scripts ## Architecture & Data Flow +### Live Trading (Server-Side) ``` Live: WS -> Buffer -> DB -> CustomTF -> IndicatorEngine -> Brain -> Decisions │ │ Backtest: DB (History) -> Backtester ─────────┴─────────────┘ ``` +### Client-Side Simulation (Dashboard) +``` +User Input -> API /candles/bulk -> Browser (ClientStrategyEngine) -> Chart Visualization + ↓ ↓ + Historical Data Buy/Sell Markers +``` + - **Stateless Logic**: `IndicatorEngine` and `Brain` are driver-agnostic. They read from DB and write to DB, unaware if the trigger is live WS or backtest replay. - **Consistency**: Indicators are computed exactly the same way for live and backtest. - **Visualization**: Dashboard queries `indicators` and `decisions` tables directly. Decisions contain a JSON snapshot of indicators at the moment of decision. +- **Client-Side Simulation**: All strategy simulations run in the browser using `ClientStrategyEngine`. + The server only provides historical candle data via API. This minimizes server load and allows + interactive backtesting with configurable parameters (MA period 5-500). +- **Full Historical Display**: After simulation, the chart displays the complete date range used, + not just the default 1000 recent candles. ## Key Dataclasses @@ -109,6 +125,32 @@ class Decision: # Brain output - **Logging**: Use `logger = logging.getLogger(__name__)`. - **Config**: Load from `config/data_config.yaml` or env vars. +## Strategy Configuration + +The system uses a single configurable Moving Average strategy (`ma_strategy`) with a dynamic period (5-500). + +### Strategy Files +- `src/strategies/base.py` - Base strategy interface with `SignalType` enum +- `src/strategies/ma_strategy.py` - Configurable MA strategy implementation + +### Client-Side vs Server-Side + +| Feature | Client-Side (Dashboard) | Server-Side (CLI/API) | +|---------|------------------------|----------------------| +| **Purpose** | Interactive simulation | Production backtesting | +| **Strategy** | Single configurable MA (period 5-500) | Configurable via strategy registry | +| **Indicators** | Calculated in browser (SMA, RSI, etc.) | Pre-computed in database | +| **Data Flow** | API → Browser → Chart | DB → Backtester → DB | +| **Performance** | Fast, interactive | Thorough, historical | + +### Simulation Workflow +1. User selects date range and MA period (5-500) in dashboard sidebar +2. Browser fetches full historical data from `/api/v1/candles/bulk` +3. `ClientStrategyEngine` calculates indicators client-side using JavaScript +4. Simulation runs on complete dataset, generating buy/sell signals +5. Chart updates to show full historical range with trade markers +6. Results displayed in sidebar (win rate, P&L, profit factor) + ## Common Tasks ### Add New Indicator diff --git a/src/api/dashboard/static/index.html b/src/api/dashboard/static/index.html index 489bc02..21ce853 100644 --- a/src/api/dashboard/static/index.html +++ b/src/api/dashboard/static/index.html @@ -1483,54 +1483,37 @@ return tfCandles[pointers[tf]].close; }; - // Debug logging for first evaluation - if (index === 1) { - console.log('First candle time:', candles[index].time, 'Date:', new Date(candles[index].time * 1000)); - console.log('MA44 value:', getVal('ma44', primaryTF)); - } - - // Simple logic for MVP strategies - if (config.id === 'ma44_strategy') { - const ma44 = getVal('ma44', primaryTF); + // Simple logic for MA Trend strategy + if (config.id === 'ma_trend') { + const period = config.params?.period || 44; + + // Debug logging for first evaluation + 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; - // Optional: Multi-TF trend filter + // Optional: Multi-TF trend filter (must align for both entry and exit) const secondaryTF = config.timeframes?.secondary?.[0]; - let trendOk = true; + let secondaryBullish = true; + let secondaryBearish = true; if (secondaryTF) { const secondaryPrice = getPrice(secondaryTF); - const secondaryMA = getVal(`ma44_${secondaryTF}`, secondaryTF); - trendOk = secondaryPrice > secondaryMA; + 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}, trendOk=${trendOk}`); + console.log(`Trend check: ${secondaryTF} price=${secondaryPrice}, MA=${secondaryMA}, bullish=${secondaryBullish}, bearish=${secondaryBearish}`); } } - if (ma44) { - if (price > ma44 && trendOk) return 'BUY'; - if (price < ma44) return 'SELL'; - } - } - - if (config.id === 'ma125_strategy') { - const ma125 = getVal('ma125', primaryTF); - const price = candles[index].close; - - // Optional: Multi-TF trend filter - const secondaryTF = config.timeframes?.secondary?.[0]; - let trendOk = true; - if (secondaryTF) { - const secondaryPrice = getPrice(secondaryTF); - const secondaryMA = getVal(`ma125_${secondaryTF}`, secondaryTF); - trendOk = secondaryPrice > secondaryMA; - if (index === 1) { - console.log(`Trend check: ${secondaryTF} price=${secondaryPrice}, MA=${secondaryMA}, trendOk=${trendOk}`); - } - } - - if (ma125) { - if (price > ma125 && trendOk) return 'BUY'; - if (price < ma125) return 'SELL'; + if (maValue) { + if (price > maValue && secondaryBullish) return 'BUY'; + if (price < maValue && secondaryBearish) return 'SELL'; } } @@ -2131,6 +2114,9 @@ this.loadInitialData(); this.loadTA(); + // Clear simulation results when changing timeframe + clearSimulationResults(); + // Update simulation panel timeframe display updateTimeframeDisplay(); } @@ -2312,6 +2298,36 @@ window.tradeLineSeries = []; } } + + function clearSimulationResults() { + // Clear markers from chart + clearSimulationMarkers(); + + // Clear simulation data + window.lastSimulationResults = null; + + // Hide results section + const resultsSection = document.getElementById('resultsSection'); + if (resultsSection) { + resultsSection.style.display = 'none'; + } + + // Reset results display + 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 = ''; + } // Set default start date (7 days ago) function setDefaultStartDate() { @@ -2398,11 +2414,8 @@ // Strategy parameter definitions const StrategyParams = { - ma44_strategy: [ - { name: 'maPeriod', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 } - ], - ma125_strategy: [ - { name: 'maPeriod', label: 'MA Period', type: 'number', default: 125, min: 5, max: 500 } + ma_trend: [ + { name: 'period', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 } ] }; @@ -2462,7 +2475,8 @@ value="${param.default}" ${param.min !== undefined ? `min="${param.min}"` : ''} ${param.max !== undefined ? `max="${param.max}"` : ''} - ${param.step !== undefined ? `step="${param.step}"` : ''}> + ${param.step !== undefined ? `step="${param.step}"` : ''} + > `).join(''); } @@ -2571,6 +2585,7 @@ // Build strategy config const engineConfig = { id: strategyConfig.id, + params: strategyConfig.params, timeframes: { primary: interval, secondary: secondaryTF ? [secondaryTF] : [] }, indicators: [] }; @@ -2581,37 +2596,21 @@ console.log(' Available candles:', Object.keys(candlesMap)); // Add indicator based on strategy - if (strategyConfig.id === 'ma44_strategy') { + if (strategyConfig.id === 'ma_trend') { + const period = strategyConfig.params?.period || 44; // Primary timeframe indicator engineConfig.indicators.push({ - name: 'ma44', + name: `ma${period}`, type: 'sma', - params: { period: strategyConfig.params.maPeriod || 44 }, + params: { period: period }, timeframe: interval }); // Confirmation timeframe indicator (for trend filter) if (secondaryTF) { engineConfig.indicators.push({ - name: `ma44_${secondaryTF}`, + name: `ma${period}_${secondaryTF}`, type: 'sma', - params: { period: strategyConfig.params.maPeriod || 44 }, - timeframe: secondaryTF - }); - } - } else if (strategyConfig.id === 'ma125_strategy') { - // Primary timeframe indicator - engineConfig.indicators.push({ - name: 'ma125', - type: 'sma', - params: { period: strategyConfig.params.maPeriod || 125 }, - timeframe: interval - }); - // Confirmation timeframe indicator (for trend filter) - if (secondaryTF) { - engineConfig.indicators.push({ - name: `ma125_${secondaryTF}`, - type: 'sma', - params: { period: strategyConfig.params.maPeriod || 125 }, + params: { period: period }, timeframe: secondaryTF }); } @@ -2652,6 +2651,23 @@ // Show results section document.getElementById('resultsSection').style.display = 'block'; + // Update chart with full historical data from simulation + 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`); + } + + // Show simulation markers on chart + showSimulationMarkers(); + } catch (error) { console.error('Simulation error:', error); alert('Simulation error: ' + error.message); @@ -3112,7 +3128,7 @@ const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); - const response = await fetch('/api/v1/strategies', { + const response = await fetch('/api/v1/strategies?_=' + Date.now(), { signal: controller.signal }); clearTimeout(timeoutId); diff --git a/src/api/server.py b/src/api/server.py index fe20796..2046a6a 100644 --- a/src/api/server.py +++ b/src/api/server.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta, timezone from typing import Optional, List from contextlib import asynccontextmanager -from fastapi import FastAPI, HTTPException, Query, BackgroundTasks +from fastapi import FastAPI, HTTPException, Query, BackgroundTasks, Response from fastapi.staticfiles import StaticFiles from fastapi.responses import StreamingResponse from fastapi.middleware.cors import CORSMiddleware @@ -100,12 +100,16 @@ async def root(): @app.get("/api/v1/strategies") -async def list_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 = { - "ma44_strategy": "src.strategies.ma44_strategy.MA44Strategy", - "ma125_strategy": "src.strategies.ma125_strategy.MA125Strategy", + "ma_trend": "src.strategies.ma_strategy.MAStrategy", } strategies = [] diff --git a/src/data_collector/backfill.py b/src/data_collector/backfill.py index 92c78e4..532b92c 100644 --- a/src/data_collector/backfill.py +++ b/src/data_collector/backfill.py @@ -28,7 +28,9 @@ class HyperliquidBackfill: API_URL = "https://api.hyperliquid.xyz/info" MAX_CANDLES_PER_REQUEST = 500 - MAX_TOTAL_CANDLES = 5000 + # Hyperliquid API might limit total history, but we'll set a high limit + # and stop when no more data is returned + MAX_TOTAL_CANDLES = 500000 # Standard timeframes supported by Hyperliquid INTERVALS = [ diff --git a/src/strategies/ma125_strategy.py b/src/strategies/ma125_strategy.py deleted file mode 100644 index 6810600..0000000 --- a/src/strategies/ma125_strategy.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -MA125 Strategy -Simple trend following strategy. -- Long when Price > MA125 -- Short when Price < MA125 -""" - -from typing import Dict, Any, List, Optional -from .base import BaseStrategy, StrategySignal, SignalType - -class MA125Strategy(BaseStrategy): - @property - def name(self) -> str: - return "ma125_strategy" - - @property - def required_indicators(self) -> List[str]: - return ["ma125"] - - @property - def display_name(self) -> str: - return "MA125 Strategy" - - @property - def description(self) -> str: - return "Long-term trend following using 125-period moving average. Better for identifying major trends." - - def analyze( - self, - candle: Dict[str, Any], - indicators: Dict[str, float], - current_position: Optional[Dict[str, Any]] = None - ) -> StrategySignal: - - price = candle['close'] - ma125 = indicators.get('ma125') - - if ma125 is None: - return StrategySignal(SignalType.HOLD, 0.0, "MA125 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 > MA125 -> Bullish - if price > ma125: - if is_long: - return StrategySignal(SignalType.HOLD, 1.0, f"Price {price:.2f} > MA125 {ma125:.2f}. Stay Long.") - elif is_short: - return StrategySignal(SignalType.CLOSE_SHORT, 1.0, f"Price {price:.2f} crossed above MA125 {ma125:.2f}. Close Short.") - else: - return StrategySignal(SignalType.OPEN_LONG, 1.0, f"Price {price:.2f} > MA125 {ma125:.2f}. Open Long.") - - # Logic: Price < MA125 -> Bearish - elif price < ma125: - if is_short: - return StrategySignal(SignalType.HOLD, 1.0, f"Price {price:.2f} < MA125 {ma125:.2f}. Stay Short.") - elif is_long: - return StrategySignal(SignalType.CLOSE_LONG, 1.0, f"Price {price:.2f} crossed below MA125 {ma125:.2f}. Close Long.") - else: - return StrategySignal(SignalType.OPEN_SHORT, 1.0, f"Price {price:.2f} < MA125 {ma125:.2f}. Open Short.") - - return StrategySignal(SignalType.HOLD, 0.0, "Price == MA125") diff --git a/src/strategies/ma44_strategy.py b/src/strategies/ma44_strategy.py deleted file mode 100644 index 4b9154a..0000000 --- a/src/strategies/ma44_strategy.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -MA44 Strategy -Simple trend following strategy. -- Long when Price > MA44 -- Short when Price < MA44 -""" - -from typing import Dict, Any, List, Optional -from .base import BaseStrategy, StrategySignal, SignalType - -class MA44Strategy(BaseStrategy): - @property - def name(self) -> str: - return "ma44_strategy" - - @property - def required_indicators(self) -> List[str]: - return ["ma44"] - - @property - def display_name(self) -> str: - return "MA44 Strategy" - - @property - def description(self) -> str: - return "Buy when price crosses above MA44, sell when below. Good for trending markets." - - def analyze( - self, - candle: Dict[str, Any], - indicators: Dict[str, float], - current_position: Optional[Dict[str, Any]] = None - ) -> StrategySignal: - - price = candle['close'] - ma44 = indicators.get('ma44') - - if ma44 is None: - return StrategySignal(SignalType.HOLD, 0.0, "MA44 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 > MA44 -> Bullish - if price > ma44: - if is_long: - return StrategySignal(SignalType.HOLD, 1.0, f"Price {price:.2f} > MA44 {ma44:.2f}. Stay Long.") - elif is_short: - return StrategySignal(SignalType.CLOSE_SHORT, 1.0, f"Price {price:.2f} crossed above MA44 {ma44:.2f}. Close Short.") - else: - return StrategySignal(SignalType.OPEN_LONG, 1.0, f"Price {price:.2f} > MA44 {ma44:.2f}. Open Long.") - - # Logic: Price < MA44 -> Bearish - elif price < ma44: - if is_short: - return StrategySignal(SignalType.HOLD, 1.0, f"Price {price:.2f} < MA44 {ma44:.2f}. Stay Short.") - elif is_long: - return StrategySignal(SignalType.CLOSE_LONG, 1.0, f"Price {price:.2f} crossed below MA44 {ma44:.2f}. Close Long.") - else: - return StrategySignal(SignalType.OPEN_SHORT, 1.0, f"Price {price:.2f} < MA44 {ma44:.2f}. Open Short.") - - return StrategySignal(SignalType.HOLD, 0.0, "Price == MA44") diff --git a/src/strategies/ma_strategy.py b/src/strategies/ma_strategy.py new file mode 100644 index 0000000..8a69ab8 --- /dev/null +++ b/src/strategies/ma_strategy.py @@ -0,0 +1,77 @@ +""" +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}")