Files
trumpsignal-backend/app/services/tp_sl_monitor.py
T
2026-04-21 19:33:24 +08:00

98 lines
3.0 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] = {}
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)
asyncio.create_task(_fire_close(wt, reason))
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)