"""Side-aware execution math shared by paper and live trading adapters.""" from __future__ import annotations def safe_float(value, default: float = 0.0) -> float: try: if value is None or value == "": return default return float(value) except Exception: return default def normalize_side(side: str | None) -> str: text = str(side or "").strip().lower() if text in {"short", "sell", "空", "空头", "做空", "开空"} or "空" in text: return "short" return "long" def open_price(side: str, current_price: float, slippage_pct: float = 0.0) -> float: price = safe_float(current_price) slip = max(0.0, safe_float(slippage_pct)) if price <= 0: return 0.0 if normalize_side(side) == "short": return round(price * (1 - slip / 100), 12) return round(price * (1 + slip / 100), 12) def close_price(side: str, current_price: float, slippage_pct: float = 0.0) -> float: price = safe_float(current_price) slip = max(0.0, safe_float(slippage_pct)) if price <= 0: return 0.0 if normalize_side(side) == "short": return round(price * (1 + slip / 100), 12) return round(price * (1 - slip / 100), 12) def pnl_pct(side: str, entry_price: float, current_price: float) -> float: entry = safe_float(entry_price) price = safe_float(current_price) if entry <= 0 or price <= 0: return 0.0 if normalize_side(side) == "short": return round((entry / price - 1) * 100, 4) return round((price / entry - 1) * 100, 4) def stop_loss_distance_pct(side: str, entry_price: float, stop_loss: float) -> float: entry = safe_float(entry_price) stop = safe_float(stop_loss) if entry <= 0 or stop <= 0: return 0.0 if normalize_side(side) == "short": return max(0.0, (stop / entry - 1) * 100) return max(0.0, (1 - stop / entry) * 100) def should_stop_loss(side: str, current_price: float, stop_loss: float) -> bool: price = safe_float(current_price) stop = safe_float(stop_loss) if price <= 0 or stop <= 0: return False if normalize_side(side) == "short": return price >= stop return price <= stop def should_take_profit(side: str, current_price: float, target: float) -> bool: price = safe_float(current_price) tp = safe_float(target) if price <= 0 or tp <= 0: return False if normalize_side(side) == "short": return price <= tp return price >= tp def should_hit_trailing_stop(side: str, current_price: float, trailing_stop: float) -> bool: price = safe_float(current_price) stop = safe_float(trailing_stop) if price <= 0 or stop <= 0: return False if normalize_side(side) == "short": return price >= stop return price <= stop def tighter_stop(side: str, current_stop: float, candidate_stop: float) -> float: current = safe_float(current_stop) candidate = safe_float(candidate_stop) if candidate <= 0: return current if current <= 0: return candidate if normalize_side(side) == "short": return min(current, candidate) return max(current, candidate) __all__ = [ "close_price", "normalize_side", "open_price", "pnl_pct", "safe_float", "should_hit_trailing_stop", "should_stop_loss", "should_take_profit", "stop_loss_distance_pct", "tighter_stop", ]