fix(reconcile): guard Binance/OKX restart false flat sync

Add startup grace and consecutive flat polls before external-close reconcile, matching Gate, to avoid stopping monitors and canceling TP/SL when positions API is not ready after restart.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-16 16:34:17 +08:00
parent 58e940629a
commit c1ee0dae25
2 changed files with 52 additions and 0 deletions
+26
View File
@@ -324,6 +324,10 @@ POSITION_SIZING_MODE = load_position_sizing_mode()
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
RECONCILE_STARTUP_GRACE_SEC = int(os.getenv("RECONCILE_STARTUP_GRACE_SEC", "90"))
RECONCILE_FLAT_CONFIRM_POLLS = max(1, int(os.getenv("RECONCILE_FLAT_CONFIRM_POLLS", "3")))
_APP_STARTED_AT = time.time()
_RECONCILE_FLAT_STREAK = {}
KLINE_TIMEFRAME = os.getenv("KLINE_TIMEFRAME", "5m")
FULL_MARGIN_BUFFER_RATIO = float(os.getenv("FULL_MARGIN_BUFFER_RATIO", "0.98"))
TRANSFER_CCY = os.getenv("TRANSFER_CCY", "USDT")
@@ -4110,6 +4114,11 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
def reconcile_external_closes(conn, days=None):
global _RECONCILE_FLAT_STREAK
if not exchange_private_api_configured():
return 0
if time.time() - _APP_STARTED_AT < RECONCILE_STARTUP_GRACE_SEC:
return 0
synced_count = 0
cutoff_ms = None
if days is not None:
@@ -4143,9 +4152,26 @@ def reconcile_external_closes(conn, days=None):
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:
_RECONCILE_FLAT_STREAK.pop(oid, None)
continue
if live_contracts > 0:
_RECONCILE_FLAT_STREAK.pop(oid, None)
continue
if r["status"] != "error":
streak = int(_RECONCILE_FLAT_STREAK.get(oid, 0)) + 1
_RECONCILE_FLAT_STREAK[oid] = streak
if streak < RECONCILE_FLAT_CONFIRM_POLLS:
continue
_RECONCILE_FLAT_STREAK.pop(oid, None)
print(
f"[reconcile_external_closes] {r['symbol']} id={oid} "
f"flat x{streak} polls -> sync close"
)
else:
_RECONCILE_FLAT_STREAK.pop(oid, None)
print(
f"[reconcile_external_closes] error recovery {r['symbol']} id={oid} flat -> sync close"
)
cancel_binance_futures_open_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)
+26
View File
@@ -290,6 +290,10 @@ POSITION_SIZING_MODE = load_position_sizing_mode()
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
RECONCILE_STARTUP_GRACE_SEC = int(os.getenv("RECONCILE_STARTUP_GRACE_SEC", "90"))
RECONCILE_FLAT_CONFIRM_POLLS = max(1, int(os.getenv("RECONCILE_FLAT_CONFIRM_POLLS", "3")))
_APP_STARTED_AT = time.time()
_RECONCILE_FLAT_STREAK = {}
BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC = max(
15, int(os.getenv("BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC", "60"))
)
@@ -3402,6 +3406,11 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
def reconcile_external_closes(conn, days=None):
global _RECONCILE_FLAT_STREAK
if not exchange_private_api_configured():
return 0
if time.time() - _APP_STARTED_AT < RECONCILE_STARTUP_GRACE_SEC:
return 0
synced_count = 0
cutoff_ms = None
if days is not None:
@@ -3435,9 +3444,26 @@ def reconcile_external_closes(conn, days=None):
exchange_symbol = r["exchange_symbol"] or normalize_okx_symbol(r["symbol"])
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
if live_contracts is None:
_RECONCILE_FLAT_STREAK.pop(oid, None)
continue
if live_contracts > 0:
_RECONCILE_FLAT_STREAK.pop(oid, None)
continue
if r["status"] != "error":
streak = int(_RECONCILE_FLAT_STREAK.get(oid, 0)) + 1
_RECONCILE_FLAT_STREAK[oid] = streak
if streak < RECONCILE_FLAT_CONFIRM_POLLS:
continue
_RECONCILE_FLAT_STREAK.pop(oid, None)
print(
f"[reconcile_external_closes] {r['symbol']} id={oid} "
f"flat x{streak} polls -> sync close"
)
else:
_RECONCILE_FLAT_STREAK.pop(oid, None)
print(
f"[reconcile_external_closes] error recovery {r['symbol']} id={oid} flat -> sync close"
)
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)