Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions services/accounts_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,10 +950,25 @@ async def add_credentials(self, account_name: str, connector_name: str, credenti
raise HTTPException(status_code=500, detail="Connector service not initialized")

try:
# Update the connector keys (this saves the credentials to file and validates them)
connector = await self._connector_service.update_connector_keys(account_name, connector_name, credentials)

await self.update_account_state()
# Saves the credentials to file + fast balances-only validation, and caches the connector
# so its balances are available immediately; the full bring-up runs in the background.
await self._connector_service.update_connector_keys(account_name, connector_name, credentials)

# Surface the new connector's balances in the portfolio right away — scoped to just this
# connector and skipping Gateway, so it's fast (no all-connectors / unreachable-Gateway
# refresh). The full bring-up finishes in the background. A failure here is display-only
# and must NOT delete the just-validated credential.
try:
await self.update_account_state(
account_names=[account_name],
connector_names=[connector_name],
skip_gateway=True,
)
except Exception as refresh_err:
logger.warning(
f"Initial account-state refresh for {connector_name} failed (will refresh on the "
f"next loop): {refresh_err}"
)
except Exception as e:
logger.error(f"Error adding connector credentials for account {account_name}: {e}")
await self.delete_credentials(account_name, connector_name)
Expand Down
5 changes: 5 additions & 0 deletions services/market_data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,11 @@ async def validate_trading_pair(connector_name: str, trading_pair: str, interval
max_records=10,
))
try:
# Some feeds (e.g. Hyperliquid spot) resolve exchange-specific symbol data in
# initialize_exchange_data() that fetch_candles' REST payload depends on; without it
# the payload dereferences uninitialized state. get_historical_candles initializes the
# same way, so mirror it here in the validation fetch.
await feed.initialize_exchange_data()
end_time = int(_time.time())
candles = await feed.fetch_candles(end_time=end_time, limit=1)
if candles is None or len(candles) == 0:
Expand Down
64 changes: 58 additions & 6 deletions services/unified_connector_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@

logger = logging.getLogger(__name__)

# Strong refs to fire-and-forget background connector-init tasks so they aren't GC'd mid-run.
_BACKGROUND_INIT_TASKS: set = set()


class UnifiedConnectorService:
"""
Expand Down Expand Up @@ -581,10 +584,27 @@ async def _create_and_initialize_trading_connector(
# Authenticate and create connector
connector = self._create_trading_connector(account_name, connector_name)

# Fetch balances first (fast, account-level) so the connector is usable for the portfolio
# before the heavy bring-up, then finish the rest.
await connector._update_balances()
await self._finish_trading_connector_init(connector, account_name, connector_name)

logger.info(f"Initialized trading connector {connector_name} for {account_name}")
return connector

async def _finish_trading_connector_init(
self,
connector: ConnectorBase,
account_name: str,
connector_name: str,
) -> None:
"""Heavy part of trading-connector bring-up: symbol map, trading rules, positions, recorders,
metrics, and network tasks. Split out so it can run in the background after a fast
balances-only init, upgrading an already-cached connector in place without blocking
add-credential or flickering the portfolio."""
# Initialize symbol map and trading rules
await connector._initialize_trading_pair_symbol_map()
await connector._update_trading_rules()
await connector._update_balances()

# Perpetual-specific setup
if self._is_perpetual_connector(connector):
Expand Down Expand Up @@ -625,9 +645,6 @@ async def _create_and_initialize_trading_connector(
except Exception as e:
logger.error(f"Error updating initial order status for {connector_name}: {e}")

logger.info(f"Initialized trading connector {connector_name} for {account_name}")
return connector

def _create_trading_connector(
self,
account_name: str,
Expand Down Expand Up @@ -1061,8 +1078,43 @@ async def update_connector_keys(
# Properly stop old connector (stops recorders, network tasks, cleans up caches)
await self.stop_trading_connector(account_name, connector_name)

# Create new connector with fresh recorders
return await self.get_trading_connector(account_name, connector_name)
# Fast path: create the connector and fetch balances only (mirrors the Hummingbot CLI
# `connect`), then CACHE it so the new connector's balances surface in the portfolio
# immediately via a scoped update_account_state — instead of blocking ~85s on the full
# bring-up (which for Hyperliquid perp fans out a metaAndAssetCtxs call per HIP-3 DEX plus
# order-book/websocket startup waits). Bad keys still fail here and the caller deletes the
# credential.
connector = self._create_trading_connector(account_name, connector_name)
await connector._update_balances()
self._trading_connectors.setdefault(account_name, {})[connector_name] = connector

# Finish the heavy bring-up (symbol map, trading rules, HIP-3 markets, network, recorders) in
# the background, IN PLACE on the cached connector, so it becomes trade-ready without blocking
# the response or flickering the portfolio.
task = asyncio.create_task(self._finish_init_background(account_name, connector_name, connector))
_BACKGROUND_INIT_TASKS.add(task)
task.add_done_callback(_BACKGROUND_INIT_TASKS.discard)

return connector

async def _finish_init_background(self, account_name: str, connector_name: str, connector: ConnectorBase) -> None:
"""Finish the heavy trading-connector bring-up in the background, upgrading the already-cached
balances-only connector in place so it becomes trade-ready — without blocking add-credential."""
try:
# Skip if the credential was already removed before this ran.
if connector_name not in self.list_available_credentials(account_name):
self.clear_trading_connector(account_name, connector_name)
return
await self._finish_trading_connector_init(connector, account_name, connector_name)
# If the credential was deleted WHILE finishing, tear the connector back down so a removed
# key doesn't linger in the cache / account state / portfolio.
if connector_name not in self.list_available_credentials(account_name):
await self.stop_trading_connector(account_name, connector_name)
self.clear_trading_connector(account_name, connector_name)
return
logger.info(f"Background-finished trading connector {connector_name} for {account_name}")
except Exception as e:
logger.error(f"Background finish failed for {account_name}/{connector_name}: {e}")

def clear_trading_connector(
self,
Expand Down