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>
This commit is contained in:
2026-02-24 23:25:51 +01:00
commit 4df8d53b1a
58 changed files with 7484 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
"""
Gestion des positions : sizing, validation, suivi.
"""
import logging
from typing import Optional
from app.core.config import settings
from app.core.exchange.base import AbstractExchange, AccountInfo, OrderResult
from app.core.strategy.base import TradeSignal
logger = logging.getLogger(__name__)
class TradeManager:
def __init__(self, exchange: AbstractExchange) -> None:
self._exchange = exchange
self._risk_percent = settings.bot_risk_percent
self._pip_value = 0.0001
async def execute_signal(self, signal: TradeSignal) -> Optional[OrderResult]:
"""
Calcule le sizing et place l'ordre sur l'exchange.
Retourne None si le signal est invalide ou si le sizing est trop petit.
"""
account = await self._exchange.get_account_info()
risk_pips = abs(signal.entry_price - signal.stop_loss) / self._pip_value
if risk_pips < 1:
logger.warning("Signal ignoré : risque en pips trop faible (%s)", risk_pips)
return None
units = self._calculate_units(account, risk_pips, signal)
if abs(units) < 1000:
logger.warning("Signal ignoré : taille de position trop petite (%s)", units)
return None
logger.info(
"Exécution signal %s %s — entry=%.5f SL=%.5f TP=%.5f units=%d",
signal.direction, signal.signal_type,
signal.entry_price, signal.stop_loss, signal.take_profit, units,
)
return await self._exchange.place_order(
instrument=signal.signal_type.split("+")[0] if "+" in signal.signal_type else "EUR_USD",
units=units,
stop_loss=signal.stop_loss,
take_profit=signal.take_profit,
)
def _calculate_units(
self,
account: AccountInfo,
risk_pips: float,
signal: TradeSignal,
) -> float:
"""
Calcule la taille de position basée sur le risque défini.
Formula : units = (balance * risk%) / (risk_pips * pip_value_per_unit)
Pour EUR/USD standard : pip_value_per_unit = $0.0001
"""
risk_amount = account.balance * (self._risk_percent / 100)
# Pour simplification : 1 pip = 0.0001, valeur pip sur mini-lot = $1
pip_value_per_unit = self._pip_value
raw_units = risk_amount / (risk_pips * pip_value_per_unit)
# Arrondir à la centaine inférieure
units = (int(raw_units) // 100) * 100
return float(units) if signal.direction == "buy" else float(-units)