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:
67
backend/app/services/trade_manager.py
Normal file
67
backend/app/services/trade_manager.py
Normal 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)
|
||||
Reference in New Issue
Block a user