Files
trader-bot/backend/app/core/bot.py
tika 4df8d53b1a feat: trading bot MVP — ICT Order Block + Liquidity Sweep strategy
Full-stack trading bot with:
- FastAPI backend with ICT strategy (Order Block + Liquidity Sweep detection)
- Backtester engine with rolling window, spread simulation, and performance metrics
- Hybrid market data service (yfinance + TwelveData with rate limiting + SQLite cache)
- Simulated exchange for paper trading
- React/TypeScript frontend with TradingView lightweight-charts v5
- Live dashboard with candlestick chart, OHLC legend, trade markers
- Backtest page with configurable parameters, equity curve, and trade table
- WebSocket support for real-time updates
- Bot runner with asyncio loop for automated trading

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:25:51 +01:00

156 lines
5.2 KiB
Python

"""
BotRunner — boucle de trading live asynchrone.
Cycle :
1. Fetch les N dernières bougies
2. Analyser la stratégie
3. Si signal + pas de position ouverte → exécuter
4. Broadcast l'état via WebSocket
5. Attendre le prochain tick
"""
import asyncio
import logging
from datetime import datetime
from typing import Callable, Optional
from app.core.config import settings
from app.core.exchange.base import AbstractExchange
from app.core.strategy.base import AbstractStrategy, AnalysisResult
from app.services.trade_manager import TradeManager
logger = logging.getLogger(__name__)
# Interval en secondes selon la granularité
GRANULARITY_SECONDS = {
"M1": 60, "M5": 300, "M15": 900, "M30": 1800,
"H1": 3600, "H2": 7200, "H4": 14400, "H6": 21600,
"H8": 28800, "H12": 43200, "D": 86400,
}
class BotRunner:
def __init__(
self,
exchange: AbstractExchange,
strategy: AbstractStrategy,
trade_manager: TradeManager,
instrument: str = "",
granularity: str = "",
candles_window: int = 150,
) -> None:
self._exchange = exchange
self._strategy = strategy
self._trade_manager = trade_manager
self.instrument = instrument or settings.bot_default_instrument
self.granularity = granularity or settings.bot_default_granularity
self.candles_window = candles_window
self._running = False
self._task: Optional[asyncio.Task] = None
self._last_analysis: Optional[AnalysisResult] = None
self._started_at: Optional[datetime] = None
# Callback pour broadcaster les événements (WebSocket)
self._on_event: Optional[Callable] = None
@property
def is_running(self) -> bool:
return self._running
def set_event_callback(self, callback: Callable) -> None:
self._on_event = callback
async def start(self) -> None:
if self._running:
logger.warning("Bot déjà en cours d'exécution")
return
self._running = True
self._started_at = datetime.utcnow()
logger.info("Bot démarré — %s %s", self.instrument, self.granularity)
self._task = asyncio.create_task(self._loop())
async def stop(self) -> None:
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
logger.info("Bot arrêté")
async def _loop(self) -> None:
interval = GRANULARITY_SECONDS.get(self.granularity, 3600)
# Décaler d'un peu pour s'assurer que la bougie est fermée
wait_seconds = max(interval // 10, 5)
while self._running:
try:
await self._tick()
except Exception as e:
logger.error("Erreur dans la boucle du bot: %s", e, exc_info=True)
await asyncio.sleep(wait_seconds)
async def _tick(self) -> None:
# 1. Fetch candles
df = await self._exchange.get_candles(
self.instrument, self.granularity, self.candles_window
)
if df.empty:
return
# 2. Analyser
result = self._strategy.analyze(df)
self._last_analysis = result
# 3. Vérifier les positions ouvertes
open_trades = await self._exchange.get_open_trades()
instrument_trades = [t for t in open_trades if t.instrument == self.instrument]
event = {
"type": "tick",
"instrument": self.instrument,
"granularity": self.granularity,
"last_close": float(df.iloc[-1]["close"]),
"last_time": str(df.iloc[-1]["time"]),
"order_blocks": len(result.order_blocks),
"liquidity_levels": len(result.liquidity_levels),
"signals": len(result.signals),
"open_positions": len(instrument_trades),
}
# 4. Exécuter le signal si aucune position ouverte
if result.signals and not instrument_trades:
signal = result.signals[-1]
try:
order = await self._trade_manager.execute_signal(signal)
if order:
logger.info(
"Trade exécuté : %s %s @ %.5f",
signal.direction, self.instrument, order.entry_price,
)
event["new_trade"] = {
"direction": order.direction,
"entry_price": order.entry_price,
"stop_loss": order.stop_loss,
"take_profit": order.take_profit,
}
except Exception as e:
logger.error("Erreur exécution trade: %s", e)
# 5. Broadcast
if self._on_event:
await self._on_event(event)
def get_status(self) -> dict:
return {
"running": self._running,
"instrument": self.instrument,
"granularity": self.granularity,
"started_at": str(self._started_at) if self._started_at else None,
"strategy": self._strategy.__class__.__name__,
"strategy_params": self._strategy.get_params(),
}