diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index ed6adec..4a0a4cd 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -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) diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 7827b56..9f1aca7 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -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)