3d55aa0975
Use price_to_precision in price_snapshot so live mark price matches entry/SL display instead of fixed 8 decimals.
172 lines
5.3 KiB
Python
172 lines
5.3 KiB
Python
"""实时持仓展示:开仓快照盈亏比、交易所止损是否已保本。"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Callable, Optional
|
|
|
|
|
|
def _positive_float(value: Any) -> Optional[float]:
|
|
try:
|
|
v = float(value)
|
|
return v if v > 0 else None
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def snapshot_stop_loss(initial_stop_loss: Any, stop_loss: Any) -> Optional[float]:
|
|
"""展示盈亏比时优先用开仓时止损快照。"""
|
|
sl = _positive_float(initial_stop_loss)
|
|
if sl is not None:
|
|
return sl
|
|
return _positive_float(stop_loss)
|
|
|
|
|
|
def snapshot_rr(
|
|
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
|
direction: str,
|
|
trigger_price: Any,
|
|
initial_stop_loss: Any,
|
|
stop_loss: Any,
|
|
take_profit: Any,
|
|
) -> Optional[float]:
|
|
entry = _positive_float(trigger_price)
|
|
sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
|
tp = _positive_float(take_profit)
|
|
if entry is None or sl is None or tp is None:
|
|
return None
|
|
return calc_rr_ratio_fn(direction or "long", entry, sl, tp)
|
|
|
|
|
|
def tpsl_slot_trigger_price(slot: Any) -> Optional[float]:
|
|
if not isinstance(slot, dict):
|
|
return None
|
|
for key in ("trigger_price", "trigger_display"):
|
|
v = _positive_float(slot.get(key))
|
|
if v is not None:
|
|
return v
|
|
return None
|
|
|
|
|
|
def stop_is_profit_protecting(direction: str, entry_price: Any, stop_loss: Any) -> bool:
|
|
"""
|
|
止损是否已在盈利侧(保本/锁盈),不再适用「开仓盈亏比」风控。
|
|
做空:止损 < 成交价;做多:止损 > 成交价。
|
|
"""
|
|
entry = _positive_float(entry_price)
|
|
sl = _positive_float(stop_loss)
|
|
if entry is None or sl is None:
|
|
return False
|
|
d = (direction or "long").strip().lower()
|
|
if d == "short":
|
|
return sl < entry
|
|
return sl > entry
|
|
|
|
|
|
def tpsl_update_passes_rr_gate(
|
|
direction: str,
|
|
entry_price: Any,
|
|
stop_loss: Any,
|
|
take_profit: Any,
|
|
min_rr: float,
|
|
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
|
) -> tuple[bool, Optional[str]]:
|
|
"""持仓委托改价:盈利侧止损跳过最低盈亏比;否则按开仓价几何校验。"""
|
|
if stop_is_profit_protecting(direction, entry_price, stop_loss):
|
|
return True, None
|
|
rr = calc_rr_ratio_fn(direction or "long", entry_price, stop_loss, take_profit)
|
|
if rr is not None and rr >= float(min_rr):
|
|
return True, None
|
|
rr_txt = f"{rr:.4f}" if rr is not None else "无法计算"
|
|
return False, f"计划盈亏比 {rr_txt}:1 低于最低要求 {min_rr}:1(盈利侧保本止损不受此限)"
|
|
|
|
|
|
def is_sl_breakeven_secured(direction: str, entry_price: Any, exchange_sl_price: Any) -> bool:
|
|
"""
|
|
交易所当前止损相对开仓成交价是否已保本。
|
|
做多:止损 >= 成交价;做空:止损 <= 成交价。
|
|
"""
|
|
entry = _positive_float(entry_price)
|
|
sl = _positive_float(exchange_sl_price)
|
|
if entry is None or sl is None:
|
|
return False
|
|
d = (direction or "long").strip().lower()
|
|
if d == "short":
|
|
return sl <= entry
|
|
return sl >= entry
|
|
|
|
|
|
def sl_breakeven_from_exchange_tpsl(
|
|
direction: str,
|
|
entry_price: Any,
|
|
exchange_tpsl: Any,
|
|
) -> bool:
|
|
if not isinstance(exchange_tpsl, dict):
|
|
return False
|
|
sl_px = tpsl_slot_trigger_price(exchange_tpsl.get("sl"))
|
|
if sl_px is None:
|
|
return False
|
|
return is_sl_breakeven_secured(direction, entry_price, sl_px)
|
|
|
|
|
|
def enrich_order_display_fields(item: dict[str, Any], calc_rr_ratio_fn: Callable[..., Optional[float]]) -> dict[str, Any]:
|
|
item["rr_ratio"] = snapshot_rr(
|
|
calc_rr_ratio_fn,
|
|
item.get("direction") or "long",
|
|
item.get("trigger_price"),
|
|
item.get("initial_stop_loss"),
|
|
item.get("stop_loss"),
|
|
item.get("take_profit"),
|
|
)
|
|
return item
|
|
|
|
|
|
def apply_order_live_price_display(
|
|
payload: dict[str, Any],
|
|
symbol: Any,
|
|
ticker_price: Any,
|
|
exchange_mark_price: Any,
|
|
format_price_fn: Callable[[Any, Any], str],
|
|
) -> dict[str, Any]:
|
|
"""标记价/现价展示:与交易所 price_to_precision 对齐,避免前端 toFixed(8)。"""
|
|
px_for_fmt = ticker_price
|
|
mark_raw = exchange_mark_price
|
|
if mark_raw is not None:
|
|
try:
|
|
px_for_fmt = float(mark_raw)
|
|
except (TypeError, ValueError):
|
|
pass
|
|
px_disp = format_price_fn(symbol, px_for_fmt)
|
|
payload["price_display"] = px_disp
|
|
if mark_raw is not None:
|
|
try:
|
|
payload["exchange_mark_price_display"] = format_price_fn(symbol, float(mark_raw))
|
|
except (TypeError, ValueError):
|
|
payload["exchange_mark_price_display"] = px_disp
|
|
else:
|
|
payload["exchange_mark_price_display"] = None
|
|
return payload
|
|
|
|
|
|
def apply_order_price_display_fields(
|
|
payload: dict[str, Any],
|
|
*,
|
|
direction: str,
|
|
entry_price: Any,
|
|
initial_stop_loss: Any,
|
|
stop_loss: Any,
|
|
take_profit: Any,
|
|
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
|
exchange_tpsl: Any = None,
|
|
) -> dict[str, Any]:
|
|
payload["rr_ratio"] = snapshot_rr(
|
|
calc_rr_ratio_fn,
|
|
direction,
|
|
entry_price,
|
|
initial_stop_loss,
|
|
stop_loss,
|
|
take_profit,
|
|
)
|
|
payload["sl_breakeven_secured"] = sl_breakeven_from_exchange_tpsl(
|
|
direction, entry_price, exchange_tpsl
|
|
)
|
|
return payload
|