Skip to content
Open
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ Manage bot lifecycle and deployment.
- `deploy_v2_script(name, profile, script, config)` - Deploy a script bot
- `deploy_v2_controllers(name, profile, controllers)` - Deploy controller bot
- `get_bot_runs()` - Get bot run history
- `get_rate_oracle_sources()` - List rate oracle sources bots can use
- `get_rate_oracle_config(account_name)` - Get the bot rate oracle config of a credentials profile
- `update_rate_oracle_config(account_name, rate_oracle_source, global_token_name, global_token_symbol)` - Update it (applies on next deploy)

#### 📋 Controllers Router (`client.controllers`)
Manage V2 strategy controllers.
Expand Down Expand Up @@ -234,6 +237,25 @@ Access real-time and historical market data.
- `get_funding_info(connector, pair)` - Get funding rates
- `get_vwap_for_volume(connector, pair, volume, is_buy)` - Calculate VWAP

#### 🖥️ System Router (`client.system`)
Information about the API server itself.

**Key features:**
- hummingbot-api and hummingbot versions of the server you are talking to
- The API's own container: image, digest and compose project
- Whether that image is pinned (built locally, or brought up with a compose override)
- Upgrading the server's own container to the published image, with a preflight that refuses when it is not safe

**Common methods:**
- `get_system_info()` - Versions, own container and image pinning of the API server
- `get_upgrade_preflight()` - Whether the server can replace its own container, and what it would cost
- `start_upgrade(acknowledge_executor_loss)` - Pull the published image and recreate the API container
- `get_upgrade_status()` - Follow a running upgrade, and read its outcome after the restart

**Note:** an upgrade restarts the API, which closes every RUNNING executor as
`SYSTEM_CLEANUP`; bot containers are unaffected. A server whose image is pinned -- built
locally, or brought up with a compose override file -- is refused, and stays an SSH job.

## Examples

### Jupyter Notebooks
Expand Down
16,674 changes: 16,673 additions & 1 deletion 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.8"
__version__ = "1.5.10"
__all__ = ["HummingbotAPIClient", "SyncHummingbotAPIClient", "MarketDataWebSocket", "ExecutorsWebSocket", "WebSocketRouter"]
11 changes: 11 additions & 0 deletions hummingbot_api_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
MarketDataRouter,
PortfolioRouter,
ScriptsRouter,
SystemRouter,
TradingRouter
)
from .ws import WebSocketRouter
Expand Down Expand Up @@ -49,6 +50,7 @@ def __init__(
self._market_data: Optional[MarketDataRouter] = None
self._portfolio: Optional[PortfolioRouter] = None
self._scripts: Optional[ScriptsRouter] = None
self._system: Optional[SystemRouter] = None
self._trading: Optional[TradingRouter] = None
self._ws: Optional[WebSocketRouter] = None
self._username = username
Expand Down Expand Up @@ -76,6 +78,7 @@ async def init(self) -> None:
self._market_data = MarketDataRouter(self._session, self.base_url)
self._portfolio = PortfolioRouter(self._session, self.base_url)
self._scripts = ScriptsRouter(self._session, self.base_url)
self._system = SystemRouter(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 @@ -99,6 +102,7 @@ async def close(self) -> None:
self._market_data = None
self._portfolio = None
self._scripts = None
self._system = None
self._trading = None
self._ws = None

Expand Down Expand Up @@ -207,6 +211,13 @@ def scripts(self) -> ScriptsRouter:
raise RuntimeError("Client not initialized. Call await client.init() first.")
return self._scripts

@property
def system(self) -> SystemRouter:
"""Access the system router."""
if self._system is None:
raise RuntimeError("Client not initialized. Call await client.init() first.")
return self._system

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

__all__ = [
Expand All @@ -33,5 +34,6 @@
"MarketDataRouter",
"PortfolioRouter",
"ScriptsRouter",
"SystemRouter",
"TradingRouter"
]
67 changes: 67 additions & 0 deletions hummingbot_api_client/routers/bot_orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,70 @@ async def delete_bot_run(self, bot_run_id: int) -> Dict[str, Any]:
bot_run_id: ID of the bot run to delete
"""
return await self._delete(f"/bot-orchestration/bot-runs/{bot_run_id}")

# Bot Rate Oracle Configuration
async def get_rate_oracle_sources(self) -> List[str]:
"""
List the rate oracle sources a bot can be configured with.

This configures deployed bots, not the API's own pricing.
"""
return await self._get("/bot-orchestration/rate-oracle/sources")

async def get_rate_oracle_config(self, account_name: str = "master_account") -> Dict[str, Any]:
"""
Get the rate oracle configuration that bots deployed with a credentials profile will use.

Args:
account_name: Credentials profile whose conf_client.yml to read

Returns:
account_name, rate_oracle_source ({name}), global_token
({global_token_name, global_token_symbol}), rate_limits_share_pct and
available_sources
"""
params = {"account_name": account_name}
return await self._get("/bot-orchestration/rate-oracle/config", params=params)

async def update_rate_oracle_config(
self,
account_name: str = "master_account",
rate_oracle_source: Optional[str] = None,
global_token_name: Optional[str] = None,
global_token_symbol: Optional[str] = None,
rate_limits_share_pct: Optional[float] = None
) -> Dict[str, Any]:
"""
Update the client defaults in a credentials profile's conf_client.yml.

Bots pick the change up on their next deploy (running bots keep their copied config).
Changing master_account's global token also switches the API's own valuation quote token.
Fields left as None are not changed.

Args:
account_name: Credentials profile whose conf_client.yml to update
rate_oracle_source: Rate oracle source name (see get_rate_oracle_sources())
global_token_name: Token to use as global quote (e.g., USDT, USD, BTC); must not be blank
global_token_symbol: Symbol to display for the global token (e.g., "$")
rate_limits_share_pct: Share of each exchange's API rate limit one bot instance may
spend, 0 < pct <= 100. The API rejects anything outside that with a 422, the same
bound hummingbot's own ClientConfigMap enforces when the bot reads the file back

Returns:
success, message and the updated config
"""
params = {"account_name": account_name}
update_request = {}
if rate_oracle_source is not None:
update_request["rate_oracle_source"] = {"name": rate_oracle_source}
global_token = {}
if global_token_name is not None:
global_token["global_token_name"] = global_token_name
if global_token_symbol is not None:
global_token["global_token_symbol"] = global_token_symbol
if global_token:
update_request["global_token"] = global_token
if rate_limits_share_pct is not None:
update_request["rate_limits_share_pct"] = rate_limits_share_pct

return await self._put("/bot-orchestration/rate-oracle/config", json=update_request, params=params)
81 changes: 81 additions & 0 deletions hummingbot_api_client/routers/system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import Any, Dict

from .base import BaseRouter


class SystemRouter(BaseRouter):
"""System router for information about the API server itself."""

async def get_system_info(self) -> Dict[str, Any]:
"""
Get the versions this API server runs and how its own container was deployed.

Read-only. When docker.sock is not mounted or the API cannot identify its own
container, docker_available is False and container is None, but both versions
are still reported.

Returns:
api_version, hummingbot_version, market_data (the MARKET_DATA_* tunables as
this API process resolved them -- reported, never writable, since the API
cannot rewrite its own .env), docker_available, container
({id, name, image, image_id, digest, compose_project, compose_working_dir,
compose_config_files} or None), and pinned / pinned_reason / override_file
saying whether the image is the published one (pinned is None when the
container could not be read)
"""
return await self._get("/system/info")

async def get_upgrade_preflight(self) -> Dict[str, Any]:
"""
Ask whether this API server can replace its own container with the published image.

Read-only and never fails on a daemon problem: an unreachable Docker daemon, a
container the API cannot place, missing compose labels and a registry that cannot
be read all come back as can_upgrade False with a blocked_reason, because the
caller of a destructive action needs the reason more than it needs an error.

Returns:
image_ref, current_digest, available_digest, up_to_date, the pinning verdict
(pinned / pinned_reason / override_file), compose ({project, working_dir,
config_files} or None), running_executors, running_bots, and can_upgrade with
its blocked_reason -- None only when the upgrade may actually be started.
"""
return await self._get("/system/upgrade/preflight")

async def start_upgrade(self, acknowledge_executor_loss: bool = False) -> Dict[str, Any]:
"""
Pull the published image and hand the container recreate to a helper container.

The server re-runs its own preflight, so an upgrade that has become unsafe since
you last looked is refused rather than started on a stale reading.

Args:
acknowledge_executor_loss: Consent to every RUNNING executor being closed as
SYSTEM_CLEANUP and not restored. Required when the server has any running
executor; it defaults to False, so an unwitting caller is refused.

Returns:
run_id and the initial phase. The API is replaced part-way through the run, so
read the outcome back from get_upgrade_status() once it answers again.

Raises:
aiohttp.ClientResponseError: 409 when the upgrade is refused, with the reason
as its message. Nothing was pulled and nothing on the server was changed.
"""
return await self._post(
"/system/upgrade",
json={"acknowledge_executor_loss": acknowledge_executor_loss},
)

async def get_upgrade_status(self) -> Dict[str, Any]:
"""
Get the current or last self-upgrade of this API server.

Phases: idle, pulling, recreating (the API is about to be replaced, so the next
answer comes from the new container), done, failed. After a recreate the record
is the one the new API collected from the helper container on boot.

Returns:
run_id, phase, detail, previous_digest, new_digest, exit_code and log_tail.
"""
return await self._get("/system/upgrade/status")
4 changes: 3 additions & 1 deletion hummingbot_api_client/sync_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .routers.market_data import MarketDataRouter
from .routers.portfolio import PortfolioRouter
from .routers.scripts import ScriptsRouter
from .routers.system import SystemRouter
from .routers.trading import TradingRouter

T = TypeVar('T')
Expand Down Expand Up @@ -84,6 +85,7 @@ def __init__(
self.market_data: MarketDataRouter
self.portfolio: PortfolioRouter
self.scripts: ScriptsRouter
self.system: SystemRouter
self.trading: TradingRouter

def __enter__(self) -> 'SyncHummingbotAPIClient':
Expand Down Expand Up @@ -161,7 +163,7 @@ def _wrap_routers(self):
'accounts', 'archived_bots', 'backtesting', 'bot_orchestration',
'connectors', 'controllers', 'docker', 'executors', 'gateway',
'gateway_swap', 'gateway_clmm', 'gateway_amm', 'market_data',
'portfolio', 'scripts', 'trading'
'portfolio', 'scripts', 'system', 'trading'
]

for router_name in router_attrs:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "hummingbot-api-client"
version = "1.5.9"
version = "1.5.10"
description = "An async Python client for Hummingbot API"
readme = "README.md"
requires-python = ">=3.8"
Expand Down