Files
crypto_okx/lib/trade/order_monitor_display_lib.py
dekun a1abe159fa Initial standalone crypto_okx with one-click deploy.
Add deploy/manage.sh bootstrap for git.bz121.com/dekun/crypto_okx and point docs at this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 20:00:59 +08:00

462 lines
16 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 monitor_open_stop_loss(row: Any) -> Optional[float]:
"""从 order_monitors 行取开仓止损快照."""
try:
keys = row.keys() if hasattr(row, "keys") else ()
except Exception:
keys = ()
init = row["initial_stop_loss"] if "initial_stop_loss" in keys else None
cur = row["stop_loss"] if "stop_loss" in keys else None
if init is None and isinstance(row, dict):
init = row.get("initial_stop_loss")
cur = row.get("stop_loss")
return snapshot_stop_loss(init, cur)
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 resolve_breakeven_entry_price(entry_price: Any, avg_entry_price: Any = None) -> Optional[float]:
"""保本判断基准价:有持仓加权均价时优先(滚仓后),否则用首仓成交价."""
avg = _positive_float(avg_entry_price)
if avg is not None:
return avg
return _positive_float(entry_price)
def stale_breakeven_armed(direction: str, entry_price: Any, stop_loss: Any, breakeven_armed: Any) -> bool:
"""止损已回到亏损侧时 breakeven_armed 视为过期(如滚仓下移止损)."""
try:
armed = int(breakeven_armed or 0) != 0
except (TypeError, ValueError):
return False
if not armed:
return False
return not stop_is_profit_protecting(direction, entry_price, stop_loss)
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 resolve_live_tpsl_prices(
plan_sl: Any,
plan_tp: Any,
exchange_tpsl: Any,
) -> tuple[Optional[float], Optional[float], Optional[float], Optional[float]]:
"""返回 (展示用止损, 展示用止盈, 交易所止损, 交易所止盈)."""
ex_sl = ex_tp = None
if isinstance(exchange_tpsl, dict):
ex_sl = tpsl_slot_trigger_price(exchange_tpsl.get("sl"))
ex_tp = tpsl_slot_trigger_price(exchange_tpsl.get("tp"))
disp_sl = ex_sl if ex_sl is not None else _positive_float(plan_sl)
disp_tp = ex_tp if ex_tp is not None else _positive_float(plan_tp)
return disp_sl, disp_tp, ex_sl, ex_tp
def calc_risk_fraction(direction: str, entry_price: Any, stop_loss: Any) -> Optional[float]:
"""|入场-止损|/入场;盈利侧止损返回 0."""
entry = _positive_float(entry_price)
sl = _positive_float(stop_loss)
if entry is None or sl is None:
return None
d = (direction or "long").strip().lower()
if d == "short":
risk = sl - entry
else:
risk = entry - sl
if risk <= 0:
return 0.0
return risk / entry
def calc_latest_risk_amount(
direction: str,
entry_price: Any,
stop_loss: Any,
*,
margin_capital: Any = None,
leverage: Any = None,
exchange_notional: Any = None,
contracts: Any = None,
contract_size: Any = None,
mark_price: Any = None,
funds_decimals: int = 2,
) -> Optional[float]:
"""按当前止损与持仓名义价值估算最新风险(U)."""
rf = calc_risk_fraction(direction, entry_price, stop_loss)
if rf is None:
return None
if rf <= 0:
return 0.0
notional = _positive_float(exchange_notional)
if notional is None:
try:
mc = float(margin_capital or 0)
lev = float(leverage or 0)
if mc > 0 and lev > 0:
notional = mc * lev
except (TypeError, ValueError):
pass
if notional is None:
try:
c = abs(float(contracts or 0))
cs = float(contract_size or 1)
if cs <= 0:
cs = 1.0
px = _positive_float(mark_price) or _positive_float(entry_price)
if c > 0 and px is not None:
notional = c * cs * px
except (TypeError, ValueError):
pass
if notional is None or notional <= 0:
return None
return round(notional * rf, funds_decimals)
def order_monitor_tpsl_needs_sync(
plan_sl: Any,
plan_tp: Any,
exchange_tpsl: Any,
*,
eps: float = 1e-12,
) -> tuple[Optional[float], Optional[float], bool]:
"""若交易所 TP/SL 与库中不一致,返回应写回的 (sl, tp) 及是否需更新."""
_, _, ex_sl, ex_tp = resolve_live_tpsl_prices(plan_sl, plan_tp, exchange_tpsl)
try:
cur_sl = float(plan_sl or 0)
cur_tp = float(plan_tp or 0)
except (TypeError, ValueError):
cur_sl, cur_tp = 0.0, 0.0
new_sl = ex_sl if ex_sl is not None else cur_sl
new_tp = ex_tp if ex_tp is not None else cur_tp
changed = (
(ex_sl is not None and abs(new_sl - cur_sl) > eps)
or (ex_tp is not None and abs(new_tp - cur_tp) > eps)
)
return new_sl, new_tp, changed
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,
format_price_fn: Optional[Callable[[Any, Any], str]] = None,
symbol: Any = None,
margin_capital: Any = None,
leverage: Any = None,
exchange_notional: Any = None,
contracts: Any = None,
contract_size: Any = None,
mark_price: Any = None,
avg_entry_price: Any = None,
funds_decimals: int = 2,
) -> dict[str, Any]:
disp_sl, disp_tp, _, _ = resolve_live_tpsl_prices(stop_loss, take_profit, exchange_tpsl)
payload["stop_loss_raw"] = _positive_float(stop_loss)
payload["take_profit_raw"] = _positive_float(take_profit)
payload["rr_ratio"] = snapshot_rr(
calc_rr_ratio_fn,
direction,
entry_price,
initial_stop_loss,
stop_loss,
take_profit,
)
risk_entry = resolve_breakeven_entry_price(entry_price, avg_entry_price)
payload["avg_entry_price"] = risk_entry
payload["sl_breakeven_secured"] = sl_breakeven_from_exchange_tpsl(
direction, risk_entry, exchange_tpsl
)
payload["stop_loss"] = disp_sl
payload["take_profit"] = disp_tp
if disp_sl is not None and disp_tp is not None:
payload["display_rr_ratio"] = calc_rr_ratio_fn(
direction or "long", entry_price, disp_sl, disp_tp
)
else:
payload["display_rr_ratio"] = None
if contracts is not None:
try:
from lib.market.position_metrics_lib import normalize_contracts_qty
c = normalize_contracts_qty(contracts)
if c > 0:
payload["contracts"] = c
except (TypeError, ValueError):
pass
payload["latest_risk_amount"] = calc_latest_risk_amount(
direction,
risk_entry,
disp_sl if disp_sl is not None else stop_loss,
margin_capital=margin_capital,
leverage=leverage,
exchange_notional=exchange_notional,
contracts=payload.get("contracts") if payload.get("contracts") is not None else contracts,
contract_size=contract_size,
mark_price=mark_price,
funds_decimals=funds_decimals,
)
tp_for_reward = disp_tp if disp_tp is not None else _positive_float(take_profit)
qty_for_reward = payload.get("contracts")
if qty_for_reward is None and contracts is not None:
try:
qty_for_reward = abs(float(contracts))
except (TypeError, ValueError):
qty_for_reward = None
if risk_entry is not None and tp_for_reward is not None and qty_for_reward:
try:
def reward_at_tp_usdt(direction, entry, tp, contracts, contract_size, fee_rate=0.0):
try:
d = (direction or "long").lower()
e, t, c, cs = float(entry), float(tp), float(contracts), float(contract_size or 1)
if e <= 0 or t <= 0 or c <= 0:
return None
raw = (t - e) * c * cs if d == "long" else (e - t) * c * cs
return round(raw, 2)
except Exception:
return None
reward = reward_at_tp_usdt(
direction,
risk_entry,
tp_for_reward,
float(qty_for_reward),
contract_size=float(contract_size or 1.0),
)
payload["reward_at_tp_usdt"] = (
round(reward, funds_decimals) if reward is not None else None
)
except Exception:
payload["reward_at_tp_usdt"] = None
else:
payload["reward_at_tp_usdt"] = None
if format_price_fn is not None and symbol is not None:
payload["stop_loss_display"] = (
format_price_fn(symbol, disp_sl) if disp_sl is not None else ""
)
payload["take_profit_display"] = (
format_price_fn(symbol, disp_tp) if disp_tp is not None else ""
)
mark_raw = mark_price if mark_price is not None else None
if mark_raw is not None and format_price_fn is not None and symbol is not None:
try:
payload["exchange_mark_price_display"] = format_price_fn(symbol, float(mark_raw))
except (TypeError, ValueError):
payload["exchange_mark_price_display"] = None
return payload
def enrich_active_monitor_tpsl_json(
row: Any,
stop_loss: Any,
take_profit: Any,
exchange_tpsl: Any,
*,
position_row: Any = None,
exchange_notional: Any = None,
contracts: Any = None,
contract_size: float = 1.0,
mark_price: Any = None,
calc_rr_ratio_fn: Callable[..., Optional[float]],
format_price_fn: Optional[Callable[[Any, Any], str]] = None,
symbol: Any = None,
funds_decimals: int = 2,
) -> dict[str, Any]:
"""place_tpsl 响应:展示用 TP/SL,最新风险,当前盈亏比."""
def _row_val(key: str, default=None):
try:
if hasattr(row, "keys") and key in row.keys():
return row[key]
except Exception:
pass
if isinstance(row, dict):
return row.get(key, default)
return default
direction = _row_val("direction") or "long"
entry = _row_val("trigger_price")
init_sl = _row_val("initial_stop_loss")
margin = _row_val("margin_capital")
leverage = _row_val("leverage")
if position_row is not None:
from lib.market.position_metrics_lib import parse_position_entry_price, position_contracts
live_c = position_contracts(position_row)
if abs(live_c) >= 1e-12:
contracts = abs(live_c)
avg_entry = parse_position_entry_price(position_row)
else:
avg_entry = None
payload: dict[str, Any] = {
"stop_loss": stop_loss,
"take_profit": take_profit,
}
apply_order_price_display_fields(
payload,
direction=direction,
entry_price=entry,
initial_stop_loss=init_sl,
stop_loss=stop_loss,
take_profit=take_profit,
calc_rr_ratio_fn=calc_rr_ratio_fn,
exchange_tpsl=exchange_tpsl,
format_price_fn=format_price_fn,
symbol=symbol or _row_val("symbol"),
margin_capital=margin,
leverage=leverage,
exchange_notional=exchange_notional,
contracts=contracts,
contract_size=contract_size,
mark_price=mark_price,
avg_entry_price=avg_entry,
funds_decimals=funds_decimals,
)
return payload