From ca1e25888d0ba9251670521d70f791e66268e89d Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 16 Jun 2026 17:03:51 +0800 Subject: [PATCH] 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 --- crypto_monitor_binance/app.py | 39 +++++++++++++++++++++-------------- crypto_monitor_okx/app.py | 31 +++++++++++++++++----------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 3382d17..591dea3 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -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, diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 9f1aca7..d802ee4 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -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)