Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions hummingbot-api-openapi.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion hummingbot_api_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
from .sync_client import SyncHummingbotAPIClient
from .ws import MarketDataWebSocket, ExecutorsWebSocket, WebSocketRouter

__version__ = "1.5.7"
__version__ = "1.5.8"
__all__ = ["HummingbotAPIClient", "SyncHummingbotAPIClient", "MarketDataWebSocket", "ExecutorsWebSocket", "WebSocketRouter"]
11 changes: 0 additions & 11 deletions hummingbot_api_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
GatewayCLMMRouter,
MarketDataRouter,
PortfolioRouter,
RateOracleRouter,
ScriptsRouter,
TradingRouter
)
Expand Down Expand Up @@ -49,7 +48,6 @@ def __init__(
self._gateway_amm: Optional[GatewayAMMRouter] = None
self._market_data: Optional[MarketDataRouter] = None
self._portfolio: Optional[PortfolioRouter] = None
self._rate_oracle: Optional[RateOracleRouter] = None
self._scripts: Optional[ScriptsRouter] = None
self._trading: Optional[TradingRouter] = None
self._ws: Optional[WebSocketRouter] = None
Expand Down Expand Up @@ -77,7 +75,6 @@ async def init(self) -> None:
self._gateway_amm = GatewayAMMRouter(self._session, self.base_url)
self._market_data = MarketDataRouter(self._session, self.base_url)
self._portfolio = PortfolioRouter(self._session, self.base_url)
self._rate_oracle = RateOracleRouter(self._session, self.base_url)
self._scripts = ScriptsRouter(self._session, self.base_url)
self._trading = TradingRouter(self._session, self.base_url)
self._ws = WebSocketRouter(self._session, self.base_url, self._username, self._password)
Expand All @@ -101,7 +98,6 @@ async def close(self) -> None:
self._gateway_amm = None
self._market_data = None
self._portfolio = None
self._rate_oracle = None
self._scripts = None
self._trading = None
self._ws = None
Expand Down Expand Up @@ -204,13 +200,6 @@ def portfolio(self) -> PortfolioRouter:
raise RuntimeError("Client not initialized. Call await client.init() first.")
return self._portfolio

@property
def rate_oracle(self) -> RateOracleRouter:
"""Access the rate oracle router."""
if self._rate_oracle is None:
raise RuntimeError("Client not initialized. Call await client.init() first.")
return self._rate_oracle

@property
def scripts(self) -> ScriptsRouter:
"""Access the scripts router."""
Expand Down
2 changes: 0 additions & 2 deletions hummingbot_api_client/routers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from .gateway_amm import GatewayAMMRouter
from .market_data import MarketDataRouter
from .portfolio import PortfolioRouter
from .rate_oracle import RateOracleRouter
from .scripts import ScriptsRouter
from .trading import TradingRouter

Expand All @@ -33,7 +32,6 @@
"GatewayAMMRouter",
"MarketDataRouter",
"PortfolioRouter",
"RateOracleRouter",
"ScriptsRouter",
"TradingRouter"
]
6 changes: 4 additions & 2 deletions hummingbot_api_client/routers/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import Optional
from typing import Optional, Union
import aiohttp

QueryParams = Union[dict, list]


class BaseRouter:
def __init__(self, session: aiohttp.ClientSession, base_url: str):
Expand Down Expand Up @@ -57,7 +59,7 @@ async def _get(self, path: str, params: Optional[dict] = None):
)
return await response.json()

async def _post(self, path: str, json: Optional[dict] = None, params: Optional[dict] = None) -> dict:
async def _post(self, path: str, json: Optional[dict] = None, params: Optional[QueryParams] = None) -> dict:
"""Perform a POST request and return JSON response."""
url = f"{self.base_url}/{path.lstrip('/')}"
async with self.session.post(url, json=json, params=params) as response:
Expand Down
30 changes: 0 additions & 30 deletions hummingbot_api_client/routers/bot_orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,36 +88,6 @@ async def stop_bot(
}
return await self._post("/bot-orchestration/stop-bot", json=stop_bot_action)

async def import_strategy_for_bot(
self,
bot_name: str,
strategy: str
) -> Dict[str, Any]:
"""
Import a strategy configuration for a bot.

Args:
bot_name: Name of the bot instance
strategy: Strategy name to import
"""
data = {"strategy": strategy}
return await self._post(f"/bot-orchestration/{bot_name}/import-strategy", json=data)

async def configure_bot(
self,
bot_name: str,
params: Dict[str, Any]
) -> Dict[str, Any]:
"""
Configure bot parameters.

Args:
bot_name: Name of the bot instance
params: Dictionary of configuration parameters
"""
data = {"params": params}
return await self._post(f"/bot-orchestration/{bot_name}/config", json=data)

async def stop_and_archive_bot(
self,
bot_name: str,
Expand Down
40 changes: 26 additions & 14 deletions hummingbot_api_client/routers/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async def is_running(self) -> bool:

async def get_available_images(self, image_name: Optional[str]) -> Dict[str, Any]:
"""Get available Docker images matching the specified name."""
return await self._get(f"/docker/available-images", params={"image_name": image_name})
return await self._get("/docker/available-images/", params={"image_name": image_name})

async def get_active_containers(self, name_filter: Optional[str] = None) -> Dict[str, Any]:
"""Get all currently active (running) Docker containers."""
Expand All @@ -29,27 +29,39 @@ async def clean_exited_containers(self) -> Dict[str, Any]:
return await self._post("/docker/clean-exited-containers")

# Container Management
async def get_container_status(self, container_name: str) -> Dict[str, Any]:
"""Get detailed status information for a specific container."""
return await self._get(f"/docker/container/{container_name}/status")

#
# There is no per-container status route. Read a container's state from
# get_active_containers() / get_exited_containers(), both of which take a
# name_filter.
async def start_container(self, container_name: str) -> Dict[str, Any]:
"""Start a stopped container."""
return await self._post(f"/docker/container/{container_name}/start")
return await self._post(f"/docker/start-container/{container_name}")

async def stop_container(self, container_name: str) -> Dict[str, Any]:
"""Stop a running container."""
return await self._post(f"/docker/container/{container_name}/stop")

async def remove_container(self, container_name: str, force: bool = False) -> Dict[str, Any]:
"""Remove a container."""
params = {"force": force} if force else None
return await self._delete(f"/docker/container/{container_name}", params=params)
return await self._post(f"/docker/stop-container/{container_name}")

async def remove_container(
self,
container_name: str,
archive_locally: bool = True,
s3_bucket: Optional[str] = None,
) -> Dict[str, Any]:
"""Remove a Hummingbot container, archiving its bot data first.

The route only accepts containers named `hummingbot-*`: removal is bound up
with archiving the instance's data, which only those have.
"""
params: Dict[str, Any] = {"archive_locally": archive_locally}
if s3_bucket:
params["s3_bucket"] = s3_bucket
return await self._post(f"/docker/remove-container/{container_name}", params=params)

# Image Management
async def pull_image(self, image_name: str, tag: str = "latest") -> Dict[str, Any]:
"""Pull a Docker image from registry."""
return await self._post("/docker/pull-image/", json={"name": image_name, "tag": tag})
# The route takes one `image_name` carrying the tag, not a name/tag pair.
return await self._post("/docker/pull-image/", json={"image_name": f"{image_name}:{tag}"})

async def get_pull_status(self) -> Dict[str, Any]:
"""Get the status of image pull operations."""
Expand Down
13 changes: 12 additions & 1 deletion hummingbot_api_client/routers/executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,18 @@ async def get_executor(self, executor_id: str) -> Dict[str, Any]:
executor_id: The executor ID to retrieve

Returns:
Executor details
Executor details. `filled_amount_quote` is the volume traded, and means
that on every executor type: for one that places orders the amount it
filled IS its volume, and an LP executor reports the swaps that crossed
its range rather than the capital it deposited — depositing trades
nothing. An LP position derives it from the fees it earned, so it reads
0 until it has earned some.

For a Gateway swap, `custom_info` carries `transaction_hash` (the
on-chain signature; `order_id` is internal and appears nowhere on
chain), `swap_provider`, `wallet_address`, and the LIVE `slippage_pct` —
above the configured start means earlier attempts failed on slippage and
this one paid to get through.

Example:
executor = await client.executors.get_executor("exec_123")
Expand Down
94 changes: 26 additions & 68 deletions hummingbot_api_client/routers/gateway.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Optional, Dict, Any, List
from .base import BaseRouter
from .base import BaseRouter, QueryParams


class GatewayRouter(BaseRouter):
Expand Down Expand Up @@ -160,67 +160,6 @@ async def list_pools(
params={"connector_name": connector_name, "network": network}
)

async def add_pool(
self,
connector_name: str,
pool_type: str,
network: str,
base: str,
quote: str,
address: str
) -> Dict[str, Any]:
"""
Add a custom liquidity pool.

Args:
connector_name: DEX connector name
pool_type: Type of pool
network: Network name
base: Base token symbol
quote: Quote token symbol
address: Pool address
"""
pool_data = {
"connector_name": connector_name,
"type": pool_type,
"network": network,
"base": base,
"quote": quote,
"address": address
}
return await self._post("/gateway/pools", json=pool_data)

async def delete_pool(
self,
connector: str,
network: str,
pool_type: str,
address: str
) -> Dict[str, Any]:
"""
Delete a liquidity pool from Gateway's pool list.

Args:
connector: DEX connector (e.g., 'meteora', 'raydium', 'uniswap')
network: Network name (e.g., 'mainnet-beta', 'mainnet')
pool_type: Pool type (e.g., 'CLMM', 'AMM')
address: Pool contract address to remove

Example:
await client.gateway.delete_pool(
connector='meteora',
network='mainnet-beta',
pool_type='CLMM',
address='2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3'
)
"""
params = {
"connector_name": connector,
"network": network,
"pool_type": pool_type.lower() # Gateway expects lowercase (amm, clmm)
}
return await self._delete(f"/gateway/pools/{address}", params=params)

# ============================================
# Networks (Primary Endpoints)
# ============================================
Expand Down Expand Up @@ -477,22 +416,41 @@ async def delete_network_pool(
async def save_network_pool(
self,
network_id: str,
pool_address: str
pool_address: str,
connector: Optional[str] = None,
type: Optional[str] = None,
) -> Dict[str, Any]:
"""
Save a pool by address - auto-fetches pool info from the blockchain.
Save a pool by address, auto-adding its tokens.

This is the simplest way to add a pool. Just provide the address and
the API will fetch connector, type, tokens, and fees automatically.
The pool's base, quote and fee always come from the connector. Gateway only
needs GeckoTerminal to answer which DEX the address belongs to, and whether it
is amm or clmm. Pass connector and type to answer that directly and skip the
lookup — which also makes this work for a pool GeckoTerminal has not indexed,
the newest ones being exactly the ones it is most likely to be missing.

Args:
network_id: Network ID in format 'chain-network' (e.g., 'solana-mainnet-beta')
pool_address: Pool contract address
connector: DEX connector ('meteora', 'raydium', 'orca', 'uniswap').
Must be given together with type, or both omitted.
type: Pool type, 'amm' or 'clmm'. Must be given together with connector.

Example:
# Caller knows the venue — an LP provider config of 'meteora/clmm' splits
# straight into these two arguments, and no indexer is consulted.
await client.gateway.save_network_pool(
network_id='solana-mainnet-beta',
pool_address='58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2'
pool_address='2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3',
connector='meteora',
type='clmm',
)
"""
return await self._post(f"/gateway/networks/{network_id}/pools/save/{pool_address}")
params: QueryParams = {}
if connector and type:
params["connector"] = connector
params["type"] = type
return await self._post(
f"/gateway/networks/{network_id}/pools/save/{pool_address}",
params=params or None,
)
Loading
Loading