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
│ ├── 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

View File

@ -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 MA Trend strategy
if (config.id === 'ma_trend') {
const period = config.params?.period || 44;
// Simple logic for MVP strategies
if (config.id === 'ma44_strategy') {
const ma44 = getVal('ma44', primaryTF);
// 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();
}
@ -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)
function setDefaultStartDate() {
const startDateInput = document.getElementById('simStartDate');
@ -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}"` : ''}
>
</div>
`).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);

View File

@ -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 = []

View File

@ -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 = [

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