113 lines
3.3 KiB
Python
113 lines
3.3 KiB
Python
"""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:
|
|
return "short" if str(side or "").strip().lower() == "short" else "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",
|
|
]
|