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:
@@ -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)
|
since_ms = _to_ms_with_fallback(opened_at_ms, opened_at_str)
|
||||||
close_side = "sell" if direction == "long" else "buy"
|
close_side = "sell" if direction == "long" else "buy"
|
||||||
|
|
||||||
def pick_from_trades(trades):
|
def pick_from_trades(trades, min_ts=None):
|
||||||
if not trades:
|
if not trades:
|
||||||
return None
|
return None
|
||||||
candidates = []
|
candidates = []
|
||||||
@@ -4105,6 +4105,12 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
|
|||||||
ts = t.get("timestamp")
|
ts = t.get("timestamp")
|
||||||
if ts is None:
|
if ts is None:
|
||||||
continue
|
continue
|
||||||
|
try:
|
||||||
|
ts_i = int(ts)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if min_ts and ts_i < int(min_ts):
|
||||||
|
continue
|
||||||
candidates.append(t)
|
candidates.append(t)
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return None
|
return None
|
||||||
@@ -4112,11 +4118,7 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=100)
|
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=100)
|
||||||
hit = pick_from_trades(trades)
|
return pick_from_trades(trades, since_ms)
|
||||||
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
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
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)
|
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=200)
|
||||||
except Exception:
|
except Exception:
|
||||||
trades = []
|
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 []:
|
for t in trades or []:
|
||||||
if (t.get("side") or "").lower() != close_side:
|
if (t.get("side") or "").lower() != close_side:
|
||||||
continue
|
continue
|
||||||
@@ -4227,6 +4224,9 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
|||||||
take_profit = row["take_profit"]
|
take_profit = row["take_profit"]
|
||||||
exchange_symbol = row["exchange_symbol"] or normalize_exchange_symbol(sym)
|
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_str = app_now_str()
|
||||||
closed_at_ms = None
|
closed_at_ms = None
|
||||||
closing_trades = fetch_closing_fills_for_record(
|
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:
|
if closing_trades:
|
||||||
last_ts = closing_trades[-1].get("timestamp")
|
last_ts = closing_trades[-1].get("timestamp")
|
||||||
if last_ts:
|
if last_ts:
|
||||||
closed_at_str = ms_to_app_local_str(int(last_ts))
|
try:
|
||||||
closed_at_ms = int(last_ts)
|
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)
|
close_ms = _to_ms_with_fallback(closed_at_ms, closed_at_str)
|
||||||
pnl, exit_px2, _, _, _ = resolve_trade_pnl_amount(
|
pnl, exit_px2, _, _, _ = resolve_trade_pnl_amount(
|
||||||
row,
|
row,
|
||||||
|
|||||||
+19
-12
@@ -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)
|
since_ms = _to_ms_with_fallback(opened_at_ms, opened_at_str)
|
||||||
close_side = "sell" if direction == "long" else "buy"
|
close_side = "sell" if direction == "long" else "buy"
|
||||||
|
|
||||||
def pick_from_trades(trades):
|
def pick_from_trades(trades, min_ts=None):
|
||||||
if not trades:
|
if not trades:
|
||||||
return None
|
return None
|
||||||
candidates = []
|
candidates = []
|
||||||
@@ -3223,6 +3223,12 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
|
|||||||
ts = t.get("timestamp")
|
ts = t.get("timestamp")
|
||||||
if ts is None:
|
if ts is None:
|
||||||
continue
|
continue
|
||||||
|
try:
|
||||||
|
ts_i = int(ts)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if min_ts and ts_i < int(min_ts):
|
||||||
|
continue
|
||||||
candidates.append(t)
|
candidates.append(t)
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return None
|
return None
|
||||||
@@ -3230,11 +3236,7 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=100)
|
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=100)
|
||||||
hit = pick_from_trades(trades)
|
return pick_from_trades(trades, since_ms)
|
||||||
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
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
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)
|
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=200)
|
||||||
except Exception:
|
except Exception:
|
||||||
trades = []
|
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 []:
|
for t in trades or []:
|
||||||
if (t.get("side") or "").lower() != close_side:
|
if (t.get("side") or "").lower() != close_side:
|
||||||
continue
|
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)
|
leverage = row["leverage"] or infer_leverage(sym)
|
||||||
exchange_symbol = row["exchange_symbol"] or normalize_okx_symbol(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)
|
trade = fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_at_ms=opened_at_ms)
|
||||||
exit_px = None
|
exit_px = None
|
||||||
closed_at_str = app_now_str()
|
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
|
exit_px = None
|
||||||
ts = trade.get("timestamp")
|
ts = trade.get("timestamp")
|
||||||
if ts:
|
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:
|
if exit_px is None or exit_px <= 0:
|
||||||
p = get_price(sym)
|
p = get_price(sym)
|
||||||
|
|||||||
Reference in New Issue
Block a user