fix: Gate hub close sync and trade record open stop-loss snapshot
Sync order monitors from Gate position history after hub flat close. Store and display initial_stop_loss in trade records instead of post-amend exchange stops. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+132
-9
@@ -1953,13 +1953,12 @@ def get_effective_trade_field(row, reviewed_key, base_key, default=None):
|
||||
|
||||
def to_effective_trade_dict(row):
|
||||
item = row_to_dict(row)
|
||||
base_stop = item.get("initial_stop_loss") if item.get("initial_stop_loss") not in (None, "") else item.get("stop_loss")
|
||||
from order_monitor_display_lib import snapshot_stop_loss
|
||||
|
||||
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
||||
item["display_open_stop_loss"] = open_stop
|
||||
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
||||
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
||||
open_stop = item.get("initial_stop_loss")
|
||||
if open_stop in (None, ""):
|
||||
open_stop = base_stop
|
||||
item["display_open_stop_loss"] = open_stop
|
||||
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
||||
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
||||
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
||||
@@ -2259,7 +2258,9 @@ def insert_trade_record(
|
||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||
snap_sl = initial_stop_loss if initial_stop_loss not in (None, "") else stop_loss
|
||||
from order_monitor_display_lib import snapshot_stop_loss
|
||||
|
||||
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
||||
er = (
|
||||
(entry_reason or "").strip()
|
||||
or entry_reason_from_key_signal(kst)
|
||||
@@ -3710,9 +3711,32 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
|
||||
if hit is None and since_ms:
|
||||
trades = exchange.fetch_my_trades(exchange_symbol, since=None, limit=100)
|
||||
hit = pick_from_trades(trades)
|
||||
return hit
|
||||
if hit is not None:
|
||||
return hit
|
||||
except Exception:
|
||||
return None
|
||||
pass
|
||||
try:
|
||||
from gate_position_history_lib import pick_gate_position_close
|
||||
|
||||
pos = pick_gate_position_close(
|
||||
fetch_gate_positions_close_history(),
|
||||
exchange_symbol,
|
||||
direction,
|
||||
opened_at_ms=since_ms,
|
||||
)
|
||||
if pos:
|
||||
return {
|
||||
"price": None,
|
||||
"timestamp": pos["close_ms"],
|
||||
"side": close_side,
|
||||
"_from_position_history": True,
|
||||
"_realized_pnl": pos.get("pnl"),
|
||||
"_sync_key": pos.get("sync_key"),
|
||||
"_open_ms": pos.get("open_ms"),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def fetch_closing_fills_for_record(exchange_symbol, direction, opened_at_str, closed_at_str=None, opened_at_ms=None, closed_at_ms=None):
|
||||
@@ -3819,7 +3843,7 @@ def calc_weighted_exit_price(trades):
|
||||
return weighted_sum / total_amount
|
||||
|
||||
|
||||
def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
||||
def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None, *, prefer_manual=False):
|
||||
"""
|
||||
交易所已无仓、本地仍为 active 时,推断平仓类型/时间/盈亏。
|
||||
返回 (result, pnl_amount, closed_at_str, miss_reason)。
|
||||
@@ -3844,6 +3868,12 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
||||
ts = trade.get("timestamp")
|
||||
if ts:
|
||||
closed_at_str = ms_to_app_local_str(int(ts))
|
||||
if trade.get("_from_position_history"):
|
||||
pnl_hist = trade.get("_realized_pnl")
|
||||
if pnl_hist is not None:
|
||||
note = "中控平仓后按 Gate 平仓历史同步盈亏" if prefer_manual else "按 Gate 平仓历史同步盈亏"
|
||||
res = "手动平仓" if prefer_manual else "外部平仓"
|
||||
return (res, float(pnl_hist), closed_at_str, note)
|
||||
|
||||
if exit_px is None or exit_px <= 0:
|
||||
p = get_price(sym)
|
||||
@@ -3866,6 +3896,13 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
||||
|
||||
result = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_px)
|
||||
pnl = calc_pnl(direction, trigger_price, exit_px, margin_capital, leverage)
|
||||
if prefer_manual:
|
||||
return (
|
||||
"手动平仓",
|
||||
pnl,
|
||||
closed_at_str,
|
||||
"中控平仓后按交易所成交记录同步",
|
||||
)
|
||||
if result:
|
||||
return (
|
||||
result,
|
||||
@@ -3881,6 +3918,91 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
||||
)
|
||||
|
||||
|
||||
def reconcile_hub_external_close(conn, symbol, direction):
|
||||
"""中控市价全平后:立即同步匹配 order_monitor,并读 Gate 平仓历史。"""
|
||||
if not exchange_private_api_configured():
|
||||
return {"ok": False, "msg": "未配置 GATE_API_KEY / GATE_API_SECRET", "synced": 0}
|
||||
from gate_position_history_lib import unified_symbol_for_match
|
||||
|
||||
sym_u = unified_symbol_for_match(symbol)
|
||||
dir_l = (direction or "").strip().lower()
|
||||
if dir_l not in ("long", "short"):
|
||||
return {"ok": False, "msg": "side 须为 long 或 short", "synced": 0}
|
||||
synced = 0
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM order_monitors WHERE status IN ('active', 'error')"
|
||||
).fetchall()
|
||||
for r in rows:
|
||||
if unified_symbol_for_match(r["symbol"]) != sym_u:
|
||||
continue
|
||||
if (r["direction"] or "").strip().lower() != dir_l:
|
||||
continue
|
||||
oid = int(r["id"])
|
||||
if r["status"] == "error":
|
||||
opened_at_chk = get_opened_at_value(r)
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM trade_records WHERE symbol=? AND opened_at=? AND monitor_type=? LIMIT 1",
|
||||
(r["symbol"], opened_at_chk, order_row_monitor_type(r)),
|
||||
).fetchone()
|
||||
if existing:
|
||||
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (oid,))
|
||||
synced += 1
|
||||
continue
|
||||
exchange_symbol = resolve_monitor_exchange_symbol(r)
|
||||
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
|
||||
if live_contracts is None:
|
||||
continue
|
||||
if live_contracts > 0:
|
||||
time.sleep(0.6)
|
||||
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
|
||||
if live_contracts is None or live_contracts > 0:
|
||||
continue
|
||||
global _RECONCILE_FLAT_STREAK
|
||||
_RECONCILE_FLAT_STREAK.pop(oid, None)
|
||||
cancel_gate_swap_trigger_orders(exchange_symbol)
|
||||
opened_at = get_opened_at_value(r)
|
||||
opened_at_ms = _to_ms_with_fallback(r["opened_at_ms"] if "opened_at_ms" in r.keys() else None, opened_at)
|
||||
result, pnl_amount, closed_at, miss_reason = resolve_synced_flat_close(
|
||||
r, opened_at, opened_at_ms=opened_at_ms, prefer_manual=True
|
||||
)
|
||||
closed_at_dt = parse_dt_for_trading_day(closed_at) or app_now()
|
||||
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
|
||||
session_date = r["session_date"] or get_trading_day(closed_at_dt)
|
||||
update_session_capital(conn, session_date, pnl_amount)
|
||||
insert_trade_record(
|
||||
conn,
|
||||
symbol=r["symbol"],
|
||||
monitor_type=trade_record_monitor_type(conn, r),
|
||||
trend_plan_id=trend_plan_id_from_monitor_row(r),
|
||||
key_signal_type=order_row_key_signal_type(r),
|
||||
direction=r["direction"],
|
||||
trigger_price=r["trigger_price"],
|
||||
stop_loss=r["stop_loss"],
|
||||
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
|
||||
take_profit=r["take_profit"],
|
||||
margin_capital=margin_capital_for_trade_record(r),
|
||||
leverage=r["leverage"],
|
||||
pnl_amount=pnl_amount,
|
||||
hold_seconds=hold_seconds,
|
||||
trade_style=r["trade_style"],
|
||||
risk_amount=r["risk_amount"],
|
||||
planned_rr=calc_rr_ratio(r["direction"], r["trigger_price"], r["initial_stop_loss"] or r["stop_loss"], r["take_profit"]),
|
||||
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
|
||||
result=result,
|
||||
miss_reason=handoff_trade_miss_reason(miss_reason, r),
|
||||
opened_at=opened_at,
|
||||
closed_at=closed_at,
|
||||
)
|
||||
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],))
|
||||
clear_key_sizing_snapshot_if_flat(conn, r["session_date"] or get_trading_day())
|
||||
synced += 1
|
||||
try:
|
||||
sync_trade_records_from_exchange(conn, force=True)
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True, "synced": synced}
|
||||
|
||||
|
||||
def reconcile_external_closes(conn, days=None):
|
||||
global _RECONCILE_FLAT_STREAK
|
||||
if not exchange_private_api_configured():
|
||||
@@ -8482,6 +8604,7 @@ try:
|
||||
views={"add_order": add_order, "add_key": add_key},
|
||||
ohlcv_fn=_hub_fetch_ohlcv,
|
||||
volume_rank_fn=_hub_fetch_volume_rank,
|
||||
reconcile_hub_flat_fn=reconcile_hub_external_close,
|
||||
)
|
||||
except Exception as _hub_err:
|
||||
print(f"[hub_bridge] gate: {_hub_err}")
|
||||
|
||||
Reference in New Issue
Block a user