|
18 | 18 | from pathlib import Path |
19 | 19 |
|
20 | 20 | from src.utils.logger import get_logger |
| 21 | +from src.config.settings import get_settings |
| 22 | +from src.market_data.telemetry import telemetry |
21 | 23 |
|
22 | 24 | logger = get_logger(__name__) |
23 | 25 |
|
@@ -294,6 +296,91 @@ def get_trade_by_market(self, market_id: str) -> Optional[ActiveTrade]: |
294 | 296 | if trade_id: |
295 | 297 | return self.active_trades.get(trade_id) |
296 | 298 | return None |
| 299 | + |
| 300 | + def get_trade_by_token(self, token_id: str) -> Optional[ActiveTrade]: |
| 301 | + """Find active trade by token_id.""" |
| 302 | + for trade in self.active_trades.values(): |
| 303 | + if getattr(trade, "token_id", None) == token_id and not trade.exited: |
| 304 | + return trade |
| 305 | + return None |
| 306 | + |
| 307 | + def evaluate_fast_exit(self, trade: ActiveTrade, best_bid: Optional[float], best_ask: Optional[float], now_monotonic: Optional[float] = None) -> Optional[dict]: |
| 308 | + """ |
| 309 | + Evaluate fast exit rules for a trade given latest market top-of-book. |
| 310 | + Returns exit decision dict if exiting, else None. |
| 311 | + """ |
| 312 | + settings = get_settings() |
| 313 | + now = now_monotonic if now_monotonic is not None else time.monotonic() |
| 314 | + # Determine A/B variant deterministic by token_id |
| 315 | + variant_enabled = False |
| 316 | + try: |
| 317 | + if getattr(settings, "FAST_EXIT_AB_ENABLED", True): |
| 318 | + import hashlib |
| 319 | + h = hashlib.sha256(str(trade.token_id).encode()).digest()[0] |
| 320 | + variant_enabled = (h % 2 == 1) |
| 321 | + except Exception: |
| 322 | + variant_enabled = False |
| 323 | + |
| 324 | + if not variant_enabled: |
| 325 | + return None |
| 326 | + |
| 327 | + # compute hold time |
| 328 | + hold_s = now - float(getattr(trade, "created_at", now)) |
| 329 | + |
| 330 | + min_hold = int(getattr(settings, "FAST_EXIT_MIN_HOLD_S", 10)) |
| 331 | + if hold_s < min_hold: |
| 332 | + # skip early exit |
| 333 | + telemetry.incr("fast_exit_skipped_min_hold_total", 1) |
| 334 | + return None |
| 335 | + |
| 336 | + # pick current exit price |
| 337 | + current_price = None |
| 338 | + side = getattr(trade, "side", "UP") |
| 339 | + if side == "UP": |
| 340 | + # to exit a long, take best_bid (what you'd get selling) |
| 341 | + current_price = best_bid if best_bid is not None else (best_ask or None) |
| 342 | + else: |
| 343 | + # for short, exit at best_ask (buy to cover) |
| 344 | + current_price = best_ask if best_ask is not None else (best_bid or None) |
| 345 | + |
| 346 | + if current_price is None: |
| 347 | + return None |
| 348 | + |
| 349 | + entry_price = float(getattr(trade, "entry_price", 0.0) or 0.0) |
| 350 | + tp = float(getattr(settings, "FAST_EXIT_TAKE_PROFIT_CENTS", 0.07)) |
| 351 | + sl = float(getattr(settings, "FAST_EXIT_STOP_LOSS_CENTS", 0.10)) |
| 352 | + time_stop = int(getattr(settings, "FAST_EXIT_TIME_STOP_S", 90)) |
| 353 | + max_hold = int(getattr(settings, "FAST_EXIT_MAX_HOLD_S", 120)) |
| 354 | + |
| 355 | + # compute profit relative to entry (absolute cents) |
| 356 | + pnl_move = (current_price - entry_price) if side == "UP" else (entry_price - current_price) |
| 357 | + |
| 358 | + # Take profit |
| 359 | + if pnl_move >= tp: |
| 360 | + telemetry.incr("fast_exit_tp_total", 1) |
| 361 | + # perform exit |
| 362 | + res = self.exit_trade(trade.trade_id, current_price, "fast_tp", exit_request_id=f"fast_tp_{int(now*1000)}") |
| 363 | + return {"action": "exit", "reason": "tp", "res": res} |
| 364 | + |
| 365 | + # Stop loss (hard) |
| 366 | + if pnl_move <= -sl: |
| 367 | + telemetry.incr("fast_exit_sl_total", 1) |
| 368 | + res = self.exit_trade(trade.trade_id, current_price, "fast_sl", exit_request_id=f"fast_sl_{int(now*1000)}") |
| 369 | + return {"action": "exit", "reason": "sl", "res": res} |
| 370 | + |
| 371 | + # Time stop |
| 372 | + if hold_s >= time_stop and (trade.unrealized_pnl is None or trade.unrealized_pnl <= 0): |
| 373 | + telemetry.incr("fast_exit_time_stop_total", 1) |
| 374 | + res = self.exit_trade(trade.trade_id, current_price, "fast_time_stop", exit_request_id=f"fast_time_{int(now*1000)}") |
| 375 | + return {"action": "exit", "reason": "time_stop", "res": res} |
| 376 | + |
| 377 | + # Max hold |
| 378 | + if hold_s >= max_hold: |
| 379 | + telemetry.incr("fast_exit_max_hold_total", 1) |
| 380 | + res = self.exit_trade(trade.trade_id, current_price, "fast_max_hold", exit_request_id=f"fast_max_{int(now*1000)}") |
| 381 | + return {"action": "exit", "reason": "max_hold", "res": res} |
| 382 | + |
| 383 | + return None |
297 | 384 |
|
298 | 385 | def check_timeout(self, trade_id: str) -> bool: |
299 | 386 | """Check if trade has timed out.""" |
|
0 commit comments