fix(sync-close): reject pre-open fills when backfilling trade records

False external-close sync could match historical closing trades before opened_at, producing stop-loss results with closed_at earlier than opened_at. Only use fills at or after open time.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-16 17:03:51 +08:00
parent 6287ca9129
commit ca1e25888d
2 changed files with 42 additions and 28 deletions
+23 -16
View File
@@ -4088,7 +4088,7 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
since_ms = _to_ms_with_fallback(opened_at_ms, opened_at_str)
close_side = "sell" if direction == "long" else "buy"
def pick_from_trades(trades):
def pick_from_trades(trades, min_ts=None):
if not trades:
return None
candidates = []
@@ -4105,6 +4105,12 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
ts = t.get("timestamp")
if ts is None:
continue
try:
ts_i = int(ts)
except (TypeError, ValueError):
continue
if min_ts and ts_i < int(min_ts):
continue
candidates.append(t)
if not candidates:
return None
@@ -4112,11 +4118,7 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
try:
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=100)
hit = pick_from_trades(trades)
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
return pick_from_trades(trades, since_ms)
except Exception:
return None
@@ -4139,11 +4141,6 @@ def fetch_closing_fills_for_record(exchange_symbol, direction, opened_at_str, cl
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=200)
except Exception:
trades = []
if not trades and since_ms:
try:
trades = exchange.fetch_my_trades(exchange_symbol, since=None, limit=200)
except Exception:
trades = []
for t in trades or []:
if (t.get("side") or "").lower() != close_side:
continue
@@ -4227,6 +4224,9 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
take_profit = row["take_profit"]
exchange_symbol = row["exchange_symbol"] or normalize_exchange_symbol(sym)
open_ms = _to_ms_with_fallback(
row["opened_at_ms"] if "opened_at_ms" in row.keys() else None, opened_at_str
)
closed_at_str = app_now_str()
closed_at_ms = None
closing_trades = fetch_closing_fills_for_record(
@@ -4245,12 +4245,19 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
if closing_trades:
last_ts = closing_trades[-1].get("timestamp")
if last_ts:
closed_at_str = ms_to_app_local_str(int(last_ts))
closed_at_ms = int(last_ts)
try:
last_ts_i = int(last_ts)
except (TypeError, ValueError):
last_ts_i = None
if last_ts_i is not None and open_ms and last_ts_i < int(open_ms):
closing_trades = []
exit_px = None
closed_at_str = app_now_str()
closed_at_ms = None
elif last_ts_i is not None:
closed_at_str = ms_to_app_local_str(last_ts_i)
closed_at_ms = last_ts_i
open_ms = _to_ms_with_fallback(
row["opened_at_ms"] if "opened_at_ms" in row.keys() else None, opened_at_str
)
close_ms = _to_ms_with_fallback(closed_at_ms, closed_at_str)
pnl, exit_px2, _, _, _ = resolve_trade_pnl_amount(
row,
+19 -12
View File
@@ -3206,7 +3206,7 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
since_ms = _to_ms_with_fallback(opened_at_ms, opened_at_str)
close_side = "sell" if direction == "long" else "buy"
def pick_from_trades(trades):
def pick_from_trades(trades, min_ts=None):
if not trades:
return None
candidates = []
@@ -3223,6 +3223,12 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
ts = t.get("timestamp")
if ts is None:
continue
try:
ts_i = int(ts)
except (TypeError, ValueError):
continue
if min_ts and ts_i < int(min_ts):
continue
candidates.append(t)
if not candidates:
return None
@@ -3230,11 +3236,7 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
try:
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=100)
hit = pick_from_trades(trades)
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
return pick_from_trades(trades, since_ms)
except Exception:
return None
@@ -3259,11 +3261,6 @@ def fetch_closing_fills_for_record(exchange_symbol, direction, opened_at_str, cl
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=200)
except Exception:
trades = []
if not trades and since_ms:
try:
trades = exchange.fetch_my_trades(exchange_symbol, since=None, limit=200)
except Exception:
trades = []
for t in trades or []:
if (t.get("side") or "").lower() != close_side:
continue
@@ -3357,6 +3354,9 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
leverage = row["leverage"] or infer_leverage(sym)
exchange_symbol = row["exchange_symbol"] or normalize_okx_symbol(sym)
open_ms = _to_ms_with_fallback(
row["opened_at_ms"] if "opened_at_ms" in row.keys() else None, opened_at_str
)
trade = fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_at_ms=opened_at_ms)
exit_px = None
closed_at_str = app_now_str()
@@ -3367,7 +3367,14 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
exit_px = None
ts = trade.get("timestamp")
if ts:
closed_at_str = ms_to_app_local_str(int(ts))
try:
ts_i = int(ts)
except (TypeError, ValueError):
ts_i = None
if ts_i is not None and open_ms and ts_i < int(open_ms):
exit_px = None
elif ts_i is not None:
closed_at_str = ms_to_app_local_str(ts_i)
if exit_px is None or exit_px <= 0:
p = get_price(sym)