done
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user