feat: 持仓快照盈亏比与交易所止损已保本标识
盈亏比固定用开仓 initial_stop_loss 计算,人工改委托后不变化;轮询交易所止损触发价相对成交价判定已保本,四所实例与中控统一显示绿色标识。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -37,6 +37,10 @@ if _REPO_ROOT not in sys.path:
|
||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
||||
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
||||
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||
from order_monitor_display_lib import (
|
||||
apply_order_price_display_fields,
|
||||
enrich_order_display_fields,
|
||||
)
|
||||
from journal_chart_lib import (
|
||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||
JOURNAL_CHART_DEFAULT_TF1,
|
||||
@@ -2299,12 +2303,7 @@ def enrich_order_item(raw_item, current_capital):
|
||||
ratio = round(margin / current_capital * 100, 2) if current_capital else 0
|
||||
item["notional_value"] = notional
|
||||
item["position_ratio"] = ratio
|
||||
item["rr_ratio"] = calc_rr_ratio(
|
||||
item.get("direction") or "long",
|
||||
item.get("trigger_price"),
|
||||
item.get("initial_stop_loss") or item.get("stop_loss"),
|
||||
item.get("take_profit"),
|
||||
)
|
||||
enrich_order_display_fields(item, calc_rr_ratio)
|
||||
try:
|
||||
be = item.get("breakeven_enabled")
|
||||
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
|
||||
@@ -3218,6 +3217,143 @@ def cancel_gate_swap_trigger_orders(exchange_symbol):
|
||||
pass
|
||||
|
||||
|
||||
def _gate_list_trigger_open_orders(exchange_symbol):
|
||||
params = _gate_swap_trigger_order_params()
|
||||
try:
|
||||
return exchange.fetch_open_orders(exchange_symbol, params=params) or []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _gate_order_trigger_price(order):
|
||||
for key in ("stopPrice", "triggerPrice", "price"):
|
||||
try:
|
||||
v = float(order.get(key) or 0)
|
||||
if v > 0:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
info = order.get("info") or {}
|
||||
if isinstance(info, dict):
|
||||
trig = info.get("trigger")
|
||||
if isinstance(trig, dict):
|
||||
try:
|
||||
v = float(trig.get("price") or 0)
|
||||
if v > 0:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
for key in ("trigger_price", "triggerPrice", "stopPrice", "price"):
|
||||
try:
|
||||
v = float(info.get(key) or 0)
|
||||
if v > 0:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _gate_tpsl_role_from_order(order, direction):
|
||||
info = order.get("info") or {}
|
||||
if not isinstance(info, dict):
|
||||
info = {}
|
||||
ot = str(info.get("order_type") or info.get("orderType") or order.get("type") or "").lower()
|
||||
if "take" in ot and "profit" in ot:
|
||||
return "tp"
|
||||
if "stop" in ot and "loss" in ot:
|
||||
return "sl"
|
||||
trig = info.get("trigger")
|
||||
rule = None
|
||||
if isinstance(trig, dict) and trig.get("rule") is not None:
|
||||
try:
|
||||
rule = int(trig["rule"])
|
||||
except Exception:
|
||||
rule = None
|
||||
if rule is None:
|
||||
try:
|
||||
rule = int(info.get("rule"))
|
||||
except Exception:
|
||||
rule = None
|
||||
if rule is not None:
|
||||
if direction == "long":
|
||||
return "sl" if rule == 2 else ("tp" if rule == 1 else None)
|
||||
return "sl" if rule == 1 else ("tp" if rule == 2 else None)
|
||||
if order.get("stopLossPrice"):
|
||||
return "sl"
|
||||
if order.get("takeProfitPrice"):
|
||||
return "tp"
|
||||
typ = str(order.get("type") or "").upper()
|
||||
if "TAKE" in typ:
|
||||
return "tp"
|
||||
if "STOP" in typ:
|
||||
return "sl"
|
||||
return None
|
||||
|
||||
|
||||
def _gate_tpsl_slot_from_order(order, exchange_symbol):
|
||||
trig = _gate_order_trigger_price(order)
|
||||
try:
|
||||
amt = float(order.get("amount") or order.get("remaining") or 0)
|
||||
except Exception:
|
||||
amt = None
|
||||
if amt is not None and amt <= 0:
|
||||
amt = None
|
||||
oid = order.get("id")
|
||||
if oid is None and isinstance(order.get("info"), dict):
|
||||
oid = order["info"].get("id") or order["info"].get("order_id")
|
||||
disp = format_price_for_symbol(exchange_symbol, trig) if trig else "-"
|
||||
return {
|
||||
"order_id": str(oid) if oid is not None else "",
|
||||
"channel": "gate_trigger",
|
||||
"trigger_price": trig,
|
||||
"trigger_display": disp,
|
||||
"amount": amt,
|
||||
"type": str(order.get("type") or ""),
|
||||
}
|
||||
|
||||
|
||||
def fetch_exchange_tpsl_slots(exchange_symbol, direction, plan_sl=None, plan_tp=None):
|
||||
slots = {"sl": None, "tp": None}
|
||||
if not exchange_symbol:
|
||||
return slots
|
||||
ok, _ = ensure_exchange_live_ready()
|
||||
if not ok:
|
||||
return slots
|
||||
try:
|
||||
ensure_markets_loaded()
|
||||
ambiguous = []
|
||||
for order in _gate_list_trigger_open_orders(exchange_symbol):
|
||||
role = _gate_tpsl_role_from_order(order, direction)
|
||||
slot = _gate_tpsl_slot_from_order(order, exchange_symbol)
|
||||
if role in ("sl", "tp"):
|
||||
if slots[role] is None:
|
||||
slots[role] = slot
|
||||
continue
|
||||
ambiguous.append(slot)
|
||||
for slot in ambiguous:
|
||||
trig = slot.get("trigger_price")
|
||||
if trig is None:
|
||||
continue
|
||||
try:
|
||||
plan_sl_f = float(plan_sl) if plan_sl is not None else None
|
||||
plan_tp_f = float(plan_tp) if plan_tp is not None else None
|
||||
except Exception:
|
||||
plan_sl_f = plan_tp_f = None
|
||||
if plan_sl_f is not None and plan_tp_f is not None:
|
||||
role = "sl" if abs(trig - plan_sl_f) <= abs(trig - plan_tp_f) else "tp"
|
||||
elif plan_sl_f is not None:
|
||||
role = "sl"
|
||||
elif plan_tp_f is not None:
|
||||
role = "tp"
|
||||
else:
|
||||
continue
|
||||
if slots[role] is None:
|
||||
slots[role] = slot
|
||||
except Exception:
|
||||
pass
|
||||
return slots
|
||||
|
||||
|
||||
def cancel_all_open_orders_for_symbol(exchange_symbol):
|
||||
"""策略结束时:尽量撤掉该合约下条件单与普通挂单。"""
|
||||
cancel_gate_swap_trigger_orders(exchange_symbol)
|
||||
@@ -5626,7 +5762,7 @@ def api_price_snapshot():
|
||||
entry = float(r["trigger_price"] or 0)
|
||||
pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0
|
||||
pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0
|
||||
rr_ratio = calc_rr_ratio(r["direction"], entry, r["initial_stop_loss"] or r["stop_loss"], r["take_profit"])
|
||||
exchange_tpsl = {"sl": None, "tp": None}
|
||||
ex_sym = resolve_monitor_exchange_symbol(r)
|
||||
prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"])
|
||||
lev_row = r["leverage"] if "leverage" in r.keys() else None
|
||||
@@ -5637,7 +5773,6 @@ def api_price_snapshot():
|
||||
"price": round(price, 6),
|
||||
"float_pnl": round(pnl, 6),
|
||||
"float_pct": pnl_pct,
|
||||
"rr_ratio": rr_ratio,
|
||||
"plan_margin": round(margin, 4) if margin else None,
|
||||
"exchange_initial_margin": None,
|
||||
"exchange_notional": None,
|
||||
@@ -5658,6 +5793,27 @@ def api_price_snapshot():
|
||||
payload["float_pct"] = (
|
||||
round((payload["float_pnl"] / float(denom)) * 100, 4) if denom and float(denom) > 0 else pnl_pct
|
||||
)
|
||||
if exchange_private_api_configured():
|
||||
try:
|
||||
exchange_tpsl = fetch_exchange_tpsl_slots(
|
||||
ex_sym,
|
||||
r["direction"],
|
||||
plan_sl=r["stop_loss"],
|
||||
plan_tp=r["take_profit"],
|
||||
)
|
||||
except Exception:
|
||||
exchange_tpsl = {"sl": None, "tp": None}
|
||||
payload["exchange_tpsl"] = exchange_tpsl
|
||||
apply_order_price_display_fields(
|
||||
payload,
|
||||
direction=r["direction"],
|
||||
entry_price=entry,
|
||||
initial_stop_loss=r["initial_stop_loss"],
|
||||
stop_loss=r["stop_loss"],
|
||||
take_profit=r["take_profit"],
|
||||
calc_rr_ratio_fn=calc_rr_ratio,
|
||||
exchange_tpsl=exchange_tpsl,
|
||||
)
|
||||
order_prices.append(payload)
|
||||
|
||||
return jsonify({
|
||||
|
||||
Reference in New Issue
Block a user