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>
68 lines
2.5 KiB
Python
68 lines
2.5 KiB
Python
"""
|
|
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)
|