|
@@ -5,6 +5,7 @@ import logging
|
|
|
import os
|
|
import os
|
|
|
import time
|
|
import time
|
|
|
import threading
|
|
import threading
|
|
|
|
|
+from concurrent.futures import ThreadPoolExecutor
|
|
|
from datetime import datetime, timezone, timedelta
|
|
from datetime import datetime, timezone, timedelta
|
|
|
from decimal import Decimal, ROUND_DOWN
|
|
from decimal import Decimal, ROUND_DOWN
|
|
|
from uuid import uuid4
|
|
from uuid import uuid4
|
|
@@ -22,9 +23,30 @@ FINISHED_ORDER_STATUS = "finished"
|
|
|
_CANCEL_BREAKER_LOCK = threading.Lock()
|
|
_CANCEL_BREAKER_LOCK = threading.Lock()
|
|
|
_CANCEL_BREAKER_NEXT_ALLOWED: dict[str, float] = {}
|
|
_CANCEL_BREAKER_NEXT_ALLOWED: dict[str, float] = {}
|
|
|
_CANCEL_BREAKER_SECONDS = 3.0
|
|
_CANCEL_BREAKER_SECONDS = 3.0
|
|
|
|
|
+_SNAPSHOT_POOL = ThreadPoolExecutor(max_workers=1, thread_name_prefix="balance-snapshot")
|
|
|
logger = logging.getLogger("exec_mcp")
|
|
logger = logging.getLogger("exec_mcp")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _fire_balance_snapshot(account_id: str, source_ref: str) -> None:
|
|
|
|
|
+ """Schedule a balance snapshot in a background thread.
|
|
|
|
|
+
|
|
|
|
|
+ This avoids blocking the WebSocket event loop while the snapshot
|
|
|
|
|
+ capture (which makes HTTP calls and price lookups) runs. The
|
|
|
|
|
+ single-worker pool serialises snapshots so they don't pile up.
|
|
|
|
|
+ """
|
|
|
|
|
+ try:
|
|
|
|
|
+ _SNAPSHOT_POOL.submit(_capture_snapshot_task, account_id, source_ref)
|
|
|
|
|
+ except RuntimeError:
|
|
|
|
|
+ pass # pool already shut down
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _capture_snapshot_task(account_id: str, source_ref: str) -> None:
|
|
|
|
|
+ try:
|
|
|
|
|
+ capture_account_balance_snapshot(account_id, source_kind="order_finished", source_ref=source_ref)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ logger.debug("background balance snapshot failed for account_id=%s source_ref=%s", account_id, source_ref, exc_info=True)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _cancel_breaker_is_open(account_id: str) -> bool:
|
|
def _cancel_breaker_is_open(account_id: str) -> bool:
|
|
|
with _CANCEL_BREAKER_LOCK:
|
|
with _CANCEL_BREAKER_LOCK:
|
|
|
return _CANCEL_BREAKER_NEXT_ALLOWED.get(account_id, 0.0) > time.monotonic()
|
|
return _CANCEL_BREAKER_NEXT_ALLOWED.get(account_id, 0.0) > time.monotonic()
|
|
@@ -103,6 +125,12 @@ def _encode_raw_json(raw_json) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
def _record_order_status_update(*, bitstamp_order_id: str, status: str, raw_json, account_id: str | None = None) -> dict:
|
|
def _record_order_status_update(*, bitstamp_order_id: str, status: str, raw_json, account_id: str | None = None) -> dict:
|
|
|
|
|
+ # ``account_id is None`` signals a WebSocket-driven call (the WS handler
|
|
|
|
|
+ # has no account context and looks it up from the order record below).
|
|
|
|
|
+ # Tool-driven calls (query_order, cancel_order, get_open_orders) always
|
|
|
|
|
+ # pass account_id explicitly.
|
|
|
|
|
+ from_ws = account_id is None
|
|
|
|
|
+
|
|
|
record = _load_order_record(bitstamp_order_id=bitstamp_order_id)
|
|
record = _load_order_record(bitstamp_order_id=bitstamp_order_id)
|
|
|
if record is None:
|
|
if record is None:
|
|
|
raise HTTPException(status_code=404, detail="order not found")
|
|
raise HTTPException(status_code=404, detail="order not found")
|
|
@@ -121,10 +149,13 @@ def _record_order_status_update(*, bitstamp_order_id: str, status: str, raw_json
|
|
|
conn.commit()
|
|
conn.commit()
|
|
|
|
|
|
|
|
if previous_status != FINISHED_ORDER_STATUS and normalized_status == FINISHED_ORDER_STATUS:
|
|
if previous_status != FINISHED_ORDER_STATUS and normalized_status == FINISHED_ORDER_STATUS:
|
|
|
- try:
|
|
|
|
|
- capture_account_balance_snapshot(account_id, source_kind="order_finished", source_ref=bitstamp_order_id)
|
|
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ if from_ws:
|
|
|
|
|
+ _fire_balance_snapshot(account_id, bitstamp_order_id)
|
|
|
|
|
+ else:
|
|
|
|
|
+ try:
|
|
|
|
|
+ capture_account_balance_snapshot(account_id, source_kind="order_finished", source_ref=bitstamp_order_id)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
|
|
|
invalidate_account_cache(account_id)
|
|
invalidate_account_cache(account_id)
|
|
|
return {
|
|
return {
|