4ffcb442fe
- New migrations for daily_budget, active_window, and bottrade snapshot - Add trumpstruth scraper and price_impact_monitor service - Expand bot_engine, hyperliquid, recovery, and tp_sl_monitor logic - Update API/schemas/models for new features Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
103 lines
3.2 KiB
Python
103 lines
3.2 KiB
Python
"""
|
|
Take-profit / stop-loss monitor.
|
|
|
|
Subscribes to the Binance price stream; for every open trade that has TP/SL set,
|
|
closes the HL position as soon as the live mark price crosses the threshold.
|
|
|
|
Lifecycle:
|
|
- bot_engine.open_position calls register_trade(...) after each new trade
|
|
- price callback in binance.py calls on_price_tick() once per second
|
|
- when TP or SL hits, we enqueue close_and_finalize(...) and de-register
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Dict, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class WatchedTrade:
|
|
trade_id: int
|
|
wallet: str
|
|
api_key: str
|
|
leverage: int
|
|
asset: str
|
|
side: str # "long" | "short"
|
|
entry_price: float
|
|
take_profit_pct: Optional[float]
|
|
stop_loss_pct: Optional[float]
|
|
|
|
|
|
# trade_id -> WatchedTrade
|
|
_watched: Dict[int, WatchedTrade] = {}
|
|
|
|
# Strong references to fire-close tasks to prevent GC before completion
|
|
_background_tasks: set = set()
|
|
|
|
|
|
def register_trade(
|
|
trade_id: int,
|
|
wallet: str,
|
|
api_key: str,
|
|
leverage: int,
|
|
asset: str,
|
|
side: str,
|
|
entry_price: float,
|
|
take_profit_pct: Optional[float],
|
|
stop_loss_pct: Optional[float],
|
|
) -> None:
|
|
if take_profit_pct is None and stop_loss_pct is None:
|
|
return # nothing to watch
|
|
_watched[trade_id] = WatchedTrade(
|
|
trade_id=trade_id, wallet=wallet, api_key=api_key, leverage=leverage,
|
|
asset=asset, side=side, entry_price=entry_price,
|
|
take_profit_pct=take_profit_pct, stop_loss_pct=stop_loss_pct,
|
|
)
|
|
logger.info("TP/SL watching trade %d (%s %s @ %.2f, tp=%s, sl=%s)",
|
|
trade_id, side, asset, entry_price, take_profit_pct, stop_loss_pct)
|
|
|
|
|
|
def unregister(trade_id: int) -> None:
|
|
_watched.pop(trade_id, None)
|
|
|
|
|
|
def on_price_tick(asset: str, price: float) -> None:
|
|
"""Called from binance.py on every price update. Fires close for any trade that hit TP/SL."""
|
|
if not _watched:
|
|
return
|
|
triggered = []
|
|
for tid, wt in list(_watched.items()):
|
|
if wt.asset != asset:
|
|
continue
|
|
pct = (price - wt.entry_price) / wt.entry_price
|
|
signed_pct = pct if wt.side == 'long' else -pct # gain in position's direction
|
|
pct_x100 = signed_pct * 100
|
|
|
|
if wt.take_profit_pct is not None and pct_x100 >= wt.take_profit_pct:
|
|
triggered.append((wt, "take_profit"))
|
|
elif wt.stop_loss_pct is not None and pct_x100 <= -wt.stop_loss_pct:
|
|
triggered.append((wt, "stop_loss"))
|
|
|
|
for wt, reason in triggered:
|
|
unregister(wt.trade_id)
|
|
task = asyncio.create_task(_fire_close(wt, reason))
|
|
_background_tasks.add(task)
|
|
task.add_done_callback(_background_tasks.discard)
|
|
|
|
|
|
async def _fire_close(wt: WatchedTrade, reason: str) -> None:
|
|
from app.services.bot_engine import close_and_finalize
|
|
try:
|
|
await close_and_finalize(
|
|
trade_id=wt.trade_id,
|
|
api_key=wt.api_key,
|
|
leverage=wt.leverage,
|
|
asset=wt.asset,
|
|
wallet=wt.wallet,
|
|
reason=reason,
|
|
)
|
|
except Exception as exc:
|
|
logger.error("TP/SL close failed for trade %d: %s", wt.trade_id, exc)
|