local strategy

This commit is contained in:
BTC Bot
2026-02-17 10:39:39 +01:00
parent fcac738e64
commit eafba745b1
7 changed files with 218 additions and 203 deletions

View File

@ -41,10 +41,13 @@ src/
│ ├── custom_timeframe_generator.py # 37m, 148m, 1d aggregation │ ├── custom_timeframe_generator.py # 37m, 148m, 1d aggregation
│ ├── indicator_engine.py # SMA/EMA computation & storage │ ├── indicator_engine.py # SMA/EMA computation & storage
│ ├── brain.py # Strategy evaluation & decision logging │ ├── brain.py # Strategy evaluation & decision logging
│ └── backtester.py # Historical replay driver │ └── backtester.py # Historical replay driver (server-side)
── api/ ── api/
├── server.py # FastAPI app, endpoints for data/backtests ├── server.py # FastAPI app, endpoints for data/backtests
└── dashboard/static/index.html # Real-time web dashboard └── 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 config/data_config.yaml # Operational config & indicator settings
docker/ # Docker orchestration & init-scripts docker/ # Docker orchestration & init-scripts
scripts/ # Deploy, backup, & utility scripts scripts/ # Deploy, backup, & utility scripts
@ -52,17 +55,30 @@ scripts/ # Deploy, backup, & utility scripts
## Architecture & Data Flow ## Architecture & Data Flow
### Live Trading (Server-Side)
``` ```
Live: WS -> Buffer -> DB -> CustomTF -> IndicatorEngine -> Brain -> Decisions Live: WS -> Buffer -> DB -> CustomTF -> IndicatorEngine -> Brain -> Decisions
│ │ │ │
Backtest: DB (History) -> Backtester ─────────┴─────────────┘ 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 - **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. 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. - **Consistency**: Indicators are computed exactly the same way for live and backtest.
- **Visualization**: Dashboard queries `indicators` and `decisions` tables directly. - **Visualization**: Dashboard queries `indicators` and `decisions` tables directly.
Decisions contain a JSON snapshot of indicators at the moment of decision. 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 ## Key Dataclasses
@ -109,6 +125,32 @@ class Decision: # Brain output
- **Logging**: Use `logger = logging.getLogger(__name__)`. - **Logging**: Use `logger = logging.getLogger(__name__)`.
- **Config**: Load from `config/data_config.yaml` or env vars. - **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 ## Common Tasks
### Add New Indicator ### Add New Indicator

View File

@ -1483,54 +1483,37 @@
return tfCandles[pointers[tf]].close; return tfCandles[pointers[tf]].close;
}; };
// Debug logging for first evaluation // Simple logic for MA Trend strategy
if (index === 1) { if (config.id === 'ma_trend') {
console.log('First candle time:', candles[index].time, 'Date:', new Date(candles[index].time * 1000)); const period = config.params?.period || 44;
console.log('MA44 value:', getVal('ma44', primaryTF));
}
// Simple logic for MVP strategies // Debug logging for first evaluation
if (config.id === 'ma44_strategy') { if (index === 1) {
const ma44 = getVal('ma44', primaryTF); 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 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]; const secondaryTF = config.timeframes?.secondary?.[0];
let trendOk = true; let secondaryBullish = true;
let secondaryBearish = true;
if (secondaryTF) { if (secondaryTF) {
const secondaryPrice = getPrice(secondaryTF); const secondaryPrice = getPrice(secondaryTF);
const secondaryMA = getVal(`ma44_${secondaryTF}`, secondaryTF); const secondaryMA = getVal(`ma${period}_${secondaryTF}`, secondaryTF);
trendOk = secondaryPrice > secondaryMA; if (secondaryPrice !== null && secondaryMA !== null) {
secondaryBullish = secondaryPrice > secondaryMA;
secondaryBearish = secondaryPrice < secondaryMA;
}
if (index === 1) { 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 (maValue) {
if (price > ma44 && trendOk) return 'BUY'; if (price > maValue && secondaryBullish) return 'BUY';
if (price < ma44) return 'SELL'; if (price < maValue && secondaryBearish) 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';
} }
} }
@ -2131,6 +2114,9 @@
this.loadInitialData(); this.loadInitialData();
this.loadTA(); this.loadTA();
// Clear simulation results when changing timeframe
clearSimulationResults();
// Update simulation panel timeframe display // Update simulation panel timeframe display
updateTimeframeDisplay(); updateTimeframeDisplay();
} }
@ -2313,6 +2299,36 @@
} }
} }
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) // Set default start date (7 days ago)
function setDefaultStartDate() { function setDefaultStartDate() {
const startDateInput = document.getElementById('simStartDate'); const startDateInput = document.getElementById('simStartDate');
@ -2398,11 +2414,8 @@
// Strategy parameter definitions // Strategy parameter definitions
const StrategyParams = { const StrategyParams = {
ma44_strategy: [ ma_trend: [
{ name: 'maPeriod', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 } { name: 'period', 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 }
] ]
}; };
@ -2462,7 +2475,8 @@
value="${param.default}" value="${param.default}"
${param.min !== undefined ? `min="${param.min}"` : ''} ${param.min !== undefined ? `min="${param.min}"` : ''}
${param.max !== undefined ? `max="${param.max}"` : ''} ${param.max !== undefined ? `max="${param.max}"` : ''}
${param.step !== undefined ? `step="${param.step}"` : ''}> ${param.step !== undefined ? `step="${param.step}"` : ''}
>
</div> </div>
`).join(''); `).join('');
} }
@ -2571,6 +2585,7 @@
// Build strategy config // Build strategy config
const engineConfig = { const engineConfig = {
id: strategyConfig.id, id: strategyConfig.id,
params: strategyConfig.params,
timeframes: { primary: interval, secondary: secondaryTF ? [secondaryTF] : [] }, timeframes: { primary: interval, secondary: secondaryTF ? [secondaryTF] : [] },
indicators: [] indicators: []
}; };
@ -2581,37 +2596,21 @@
console.log(' Available candles:', Object.keys(candlesMap)); console.log(' Available candles:', Object.keys(candlesMap));
// Add indicator based on strategy // Add indicator based on strategy
if (strategyConfig.id === 'ma44_strategy') { if (strategyConfig.id === 'ma_trend') {
const period = strategyConfig.params?.period || 44;
// Primary timeframe indicator // Primary timeframe indicator
engineConfig.indicators.push({ engineConfig.indicators.push({
name: 'ma44', name: `ma${period}`,
type: 'sma', type: 'sma',
params: { period: strategyConfig.params.maPeriod || 44 }, params: { period: period },
timeframe: interval timeframe: interval
}); });
// Confirmation timeframe indicator (for trend filter) // Confirmation timeframe indicator (for trend filter)
if (secondaryTF) { if (secondaryTF) {
engineConfig.indicators.push({ engineConfig.indicators.push({
name: `ma44_${secondaryTF}`, name: `ma${period}_${secondaryTF}`,
type: 'sma', type: 'sma',
params: { period: strategyConfig.params.maPeriod || 44 }, params: { period: period },
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 },
timeframe: secondaryTF timeframe: secondaryTF
}); });
} }
@ -2652,6 +2651,23 @@
// Show results section // Show results section
document.getElementById('resultsSection').style.display = 'block'; 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) { } catch (error) {
console.error('Simulation error:', error); console.error('Simulation error:', error);
alert('Simulation error: ' + error.message); alert('Simulation error: ' + error.message);
@ -3112,7 +3128,7 @@
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); 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 signal: controller.signal
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);

View File

@ -10,7 +10,7 @@ from datetime import datetime, timedelta, timezone
from typing import Optional, List from typing import Optional, List
from contextlib import asynccontextmanager 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.staticfiles import StaticFiles
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -100,12 +100,16 @@ async def root():
@app.get("/api/v1/strategies") @app.get("/api/v1/strategies")
async def list_strategies(): async def list_strategies(response: Response):
"""List all available trading strategies with metadata""" """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 from brain.py
strategy_registry = { strategy_registry = {
"ma44_strategy": "src.strategies.ma44_strategy.MA44Strategy", "ma_trend": "src.strategies.ma_strategy.MAStrategy",
"ma125_strategy": "src.strategies.ma125_strategy.MA125Strategy",
} }
strategies = [] strategies = []

View File

@ -28,7 +28,9 @@ class HyperliquidBackfill:
API_URL = "https://api.hyperliquid.xyz/info" API_URL = "https://api.hyperliquid.xyz/info"
MAX_CANDLES_PER_REQUEST = 500 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 # Standard timeframes supported by Hyperliquid
INTERVALS = [ INTERVALS = [

View File

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

View File

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

View File

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