Skip to content

Commit 9a08b23

Browse files
authored
Merge pull request #168 from hummingbot/feat/improve_executors_shutdown
(feat) add order reconcile
2 parents 14fb537 + 6a62762 commit 9a08b23

2 files changed

Lines changed: 103 additions & 0 deletions

File tree

main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ async def lifespan(app: FastAPI):
247247
logging.info("Initializing all trading connectors...")
248248
await connector_service.initialize_all_trading_connectors()
249249

250+
# Reconcile persisted active orders against the exchange (e.g. after an API
251+
# restart/crash that lost in-memory references). Confirmed-closed orders are
252+
# marked terminal; still-open orders are re-tracked so they stay cancelable.
253+
# Runs after connectors reload their persisted in-flight orders.
254+
await connector_service.reconcile_active_orders()
255+
250256
bots_orchestrator.start()
251257
market_data_service.start()
252258
await market_data_service.warmup_rate_oracle()

services/unified_connector_service.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,103 @@ async def _sync_orders_to_database(
914914
for order_id in orders_to_remove:
915915
connector.in_flight_orders.pop(order_id, None)
916916

917+
@staticmethod
918+
def _supports_order_status_query(connector: ConnectorBase) -> bool:
919+
"""Whether a connector can be asked the real state of a single order."""
920+
return (
921+
hasattr(connector, "_request_order_status")
922+
and hasattr(connector, "_is_order_not_found_during_status_update_error")
923+
and hasattr(connector, "in_flight_orders")
924+
)
925+
926+
async def reconcile_active_orders(self) -> Dict[str, int]:
927+
"""Reconcile persisted active orders against the exchange on startup.
928+
929+
Must be called AFTER ``initialize_all_trading_connectors`` so that each
930+
connector's persisted active orders have been reloaded into
931+
``in_flight_orders``. For every tracked order we ask the exchange for its
932+
real state (``_request_order_status``) and:
933+
934+
- **Confirmed terminal** (filled/cancelled/failed) -> update the DB to the
935+
real status and drop it from tracking.
936+
- **Confirmed not found** on the exchange -> mark it CANCELLED in the DB
937+
(it no longer exists) and drop it from tracking.
938+
- **Still open** -> sync the DB to the live state and KEEP it tracked, so
939+
it remains visible and cancelable via the trading endpoints.
940+
- **Unverifiable** (transient error, or the connector isn't available)
941+
-> leave the order untouched. We never mark an order terminal unless
942+
the exchange confirms it, so a connector that failed to start can never
943+
cause a live order to be falsely reported as cancelled.
944+
945+
Returns a summary dict with counts for logging/telemetry.
946+
"""
947+
summary = {"reconciled_terminal": 0, "still_open": 0, "unverified": 0, "skipped_connectors": 0}
948+
if not self.db_manager:
949+
return summary
950+
951+
terminal_states = {
952+
OrderState.FILLED, OrderState.CANCELED,
953+
OrderState.FAILED, OrderState.COMPLETED,
954+
}
955+
956+
from database import OrderRepository
957+
958+
for account_name, connectors in self._trading_connectors.items():
959+
for connector_name, connector in connectors.items():
960+
if not self._supports_order_status_query(connector) or not connector.in_flight_orders:
961+
continue
962+
963+
# Snapshot tracked orders (the set was loaded from the DB at init).
964+
tracked_orders = list(connector.in_flight_orders.values())
965+
for order in tracked_orders:
966+
client_order_id = order.client_order_id
967+
note = None
968+
try:
969+
order_update = await connector._request_order_status(order)
970+
new_state = order_update.new_state
971+
except Exception as exc:
972+
if connector._is_order_not_found_during_status_update_error(exc):
973+
# The exchange does not know this order -> it is gone.
974+
new_state = OrderState.CANCELED
975+
note = "Reconciled on startup: order not found on exchange"
976+
else:
977+
# Transient/unknown error - do not touch the order.
978+
logger.warning(
979+
f"Could not verify order {client_order_id} on "
980+
f"{account_name}/{connector_name}: {exc}"
981+
)
982+
summary["unverified"] += 1
983+
continue
984+
985+
db_status = self._map_order_state_to_status(new_state)
986+
try:
987+
async with self.db_manager.get_session_context() as session:
988+
order_repo = OrderRepository(session)
989+
await order_repo.update_order_status(
990+
client_order_id=client_order_id,
991+
status=db_status,
992+
error_message=note,
993+
)
994+
except Exception as exc:
995+
logger.error(f"Failed to persist reconciled order {client_order_id}: {exc}")
996+
summary["unverified"] += 1
997+
continue
998+
999+
if new_state in terminal_states:
1000+
connector.in_flight_orders.pop(client_order_id, None)
1001+
summary["reconciled_terminal"] += 1
1002+
else:
1003+
# Keep tracking so it stays cancelable via the trading endpoints.
1004+
summary["still_open"] += 1
1005+
1006+
logger.info(
1007+
"Order reconciliation complete: "
1008+
f"{summary['reconciled_terminal']} closed, "
1009+
f"{summary['still_open']} still open (re-tracked), "
1010+
f"{summary['unverified']} unverified"
1011+
)
1012+
return summary
1013+
9171014
async def sync_all_orders_to_database(self):
9181015
"""
9191016
Sync connector's in_flight_orders state to database for all trading connectors.

0 commit comments

Comments
 (0)