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:
dekun
2026-06-09 18:01:59 +08:00
parent 02d2a6c70b
commit 59a45ed027
11 changed files with 449 additions and 40 deletions
+130 -6
View File
@@ -1967,10 +1967,13 @@ 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"))
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", base_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"))
item["effective_miss_reason"] = get_effective_trade_field(row, "reviewed_miss_reason", "miss_reason", item.get("miss_reason"))
@@ -2283,10 +2286,13 @@ 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)
er = (entry_reason or "").strip() or None
from order_monitor_display_lib import snapshot_stop_loss
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
conn.execute(
"INSERT INTO trade_records (symbol,monitor_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id,entry_reason,trend_plan_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss, take_profit,
symbol, monitor_type, direction, trigger_price, snap_sl, snap_sl, take_profit,
margin_capital, leverage, pnl_amount, hold_seconds,
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id, er,
@@ -4064,9 +4070,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):
@@ -4173,7 +4202,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)
@@ -4198,6 +4227,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)
@@ -4220,6 +4255,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,
@@ -4235,6 +4277,87 @@ 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),
).fetchone()
if existing:
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (oid,))
synced += 1
continue
exchange_symbol = r["exchange_symbol"] or normalize_exchange_symbol(r["symbol"])
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
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),
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=r["margin_capital"],
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"],))
synced += 1
try:
sync_trend_trade_records_from_exchange(conn)
except Exception:
pass
return {"ok": True, "synced": synced}
def reconcile_external_closes(conn, days=None):
synced_count = 0
cutoff_ms = None
@@ -7979,6 +8102,7 @@ try:
},
ohlcv_fn=_hub_fetch_ohlcv,
volume_rank_fn=_hub_fetch_volume_rank,
reconcile_hub_flat_fn=reconcile_hub_external_close,
)
from strategy_trend_register import build_trend_config, patch_trend_hub_enrich
+2 -2
View File
@@ -568,14 +568,14 @@
</div>
<div class="table-wrap">
<table>
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓(展示)</th><th>平仓(展示)</th><th>盈亏U(展示)</th><th>结果</th><th>操作</th></tr>
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损(开仓)</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓(展示)</th><th>平仓(展示)</th><th>盈亏U(展示)</th><th>结果</th><th>操作</th></tr>
{% for r in record %}
<tr id="trade-row-{{ r.id }}">
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
{% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td>