This guide walks through the complete workflow for training and optimizing a strategy in quants-lab, then deploying it as a live bot managed by the hummingbot-api.
The stack has four components that work together:
| Component | Role |
|---|---|
| quants-lab | Research: data collection, feature engineering, backtesting, optimization, autonomous orchestration |
| hummingbot-api | Control plane: REST API that manages Docker containers, credentials, controller configs, and bot lifecycle |
| hummingbot | Execution engine: runs inside Docker containers, executes strategies on exchanges |
| dashboard | Web UI: Streamlit app at port 8501, wraps the hummingbot-api for point-and-click operations |
quants-lab (research + signals)
│
│ REST API calls (BackendAPIClient)
▼
hummingbot-api (FastAPI + PostgreSQL + EMQX/MQTT)
│ │
│ Docker API │ MQTT
▼ ▼
hummingbot containers Bot state / commands
(one per bot instance)
dashboard (Streamlit) ──► hummingbot-api
Before starting, make sure you have:
- Python environment set up with
quants-labinstalled (see its README) - Access to the hummingbot-api (local or remote:
https://api.metallorum.duckdns.org/) - Exchange API keys added under
Accountsin the dashboard or via/accountsendpoint - MongoDB running locally or accessible (for quants-lab features/signals storage)
- Docker daemon running on the machine where hummingbot-api is deployed
Step 1: Collect historical data
Step 2: Engineer features & generate signals [optional for signal-based strategies]
Step 3: Optimize strategy hyperparameters
Step 4: Analyze optimization results
Step 5: Create controller config
Step 6: Validate with a single backtest
Step 7: Deploy to a live bot
Step 8: Monitor and manage the bot lifecycle
All optimization and backtesting requires local OHLCV candle data. Use the candle downloader task or research notebook.
# In quants-lab/config/my_pipeline.yml
tasks:
download_candles:
enabled: true
task_class: app.tasks.data_collection.candles_downloader_task.CandlesDownloaderTask
schedule:
type: frequency
frequency_hours: 6.0
timezone: UTC
config:
connector_name: "kucoin_perpetual" # always use kucoin as candles connector
quote_asset: "USDT"
intervals: ["1m", "3m", "1h"]
days_data_retention: 60Then run the orchestrator:
cd quants-lab
python -m core.tasks.orchestrator --config config/my_pipeline.ymlOpen and run research_notebooks/data_collection/download_candles_all_pairs.ipynb. It downloads 1m candles for all USDT-perpetual pairs and writes them to the local cache (data/candles/).
from core.data_paths import DataPaths
import pandas as pd
paths = DataPaths()
candles = pd.read_parquet(paths.get_candles_path("kucoin_perpetual", "BTC-USDT", "1m"))
print(candles.tail())Skip this step if you are optimizing a standalone indicator-based controller (like
macd_bb_v1orbollinger_v1) that does not rely on pre-computed signals. Go directly to Step 3.
This step is required for signal-driven deployment pipelines (like the trend-follower grid).
Open and run research_notebooks/feature_engineering/trend_follower_grid.ipynb. It:
- Loads candles from the local cache
- Computes an
ema_trendfeature for each trading pair (a float in [-1, +1] representing trend strength and direction) - Writes
FeatureandSignaldocuments to MongoDB
Signal schema:
| Field | Description |
|---|---|
signal_name |
e.g., "ema_trend" |
trading_pair |
e.g., "BTC-USDT" |
category |
"tf" (trend-following), "mr" (mean-reversion), "pt" (pairs-trading) |
value |
Float in [-1.0, +1.0]. Values > 0.7 → strong long. Values < -0.7 → strong short. |
You can query signals in your own notebooks:
from core.features.storage import FeatureStorage
from core.database_manager import DatabaseManager
db = DatabaseManager()
storage = FeatureStorage(db)
signals = await storage.get_latest_signals(
signal_name="ema_trend",
category="tf",
min_value=0.7 # only strong long signals
)This is the core "training" step. The StrategyOptimizer uses Optuna (TPE sampler) to find the controller config that maximizes a performance objective (default: Sharpe ratio) over historical data.
Available controllers by type:
Directional Trading (signal → position with triple-barrier exits)
macd_bb_v1— MACD + Bollinger Bandsbollinger_v1,bollinger_v2— pure Bollingersupertrend_v1— Supertrend indicatordman_v3— Dynamic market-adaptivebollingrid— Bollinger + grid hybrid
Market Making (spread-based)
pmm_simple— Pure market makingpmm_dynamic— Volatility-adjusted spreadsdman_maker_v2— Dynamic market maker
Generic (custom logic)
grid_strike— Price-range grid tradingstat_arb— Statistical arbitragexemm_multiple_levels— Cross-exchange market making
Create a subclass of BaseStrategyConfigGenerator that defines the hyperparameter search space using Optuna's trial API:
# quants-lab/app/tasks/backtesting/my_macd_optimization_task.py
from core.backtesting.optimizer import BaseStrategyConfigGenerator, StrategyOptimizer
from hummingbot.controllers.directional_trading.macd_bb_v1 import MACDBBControllerConfig
class MACDBBConfigGenerator(BaseStrategyConfigGenerator):
def generate_config(self, trial) -> MACDBBControllerConfig:
return MACDBBControllerConfig(
controller_name="macd_bb_v1",
connector_name="kucoin_perpetual",
trading_pair=self.trading_pair,
interval=trial.suggest_categorical("interval", ["3m", "5m", "15m"]),
bb_length=trial.suggest_int("bb_length", 50, 200, step=10),
bb_std=trial.suggest_float("bb_std", 1.5, 3.0, step=0.25),
bb_long_threshold=trial.suggest_float("bb_long_threshold", 0.0, 0.35, step=0.05),
bb_short_threshold=trial.suggest_float("bb_short_threshold", 0.65, 1.0, step=0.05),
macd_fast=trial.suggest_int("macd_fast", 8, 21, step=1),
macd_slow=trial.suggest_int("macd_slow", 21, 55, step=2),
macd_signal=trial.suggest_int("macd_signal", 5, 15, step=1),
total_amount_quote=500,
max_executors_per_side=1,
leverage=10,
triple_barrier_config=TripleBarrierConfig(
stop_loss=trial.suggest_float("stop_loss", 0.005, 0.03, step=0.005),
take_profit=trial.suggest_float("take_profit", 0.005, 0.03, step=0.005),
time_limit=60 * 60 * 24, # 1 day
open_order_type=OrderType.MARKET,
),
)import asyncio
from core.backtesting.optimizer import StrategyOptimizer
optimizer = StrategyOptimizer(
config_generator=MACDBBConfigGenerator(trading_pair="BTC-USDT"),
connector_name="kucoin_perpetual",
trading_pair="BTC-USDT",
start_date="2024-01-01",
end_date="2024-06-30",
n_trials=200, # number of hyperparameter combinations to try
n_jobs=4, # parallel backtests
study_name="macd_bb_btcusdt_v1",
storage="sqlite:///optuna_studies.db", # persistent study storage
)
asyncio.run(optimizer.optimize())Or wrap it in a task YAML (see config/template_1_candles_optimization.yml for a full example) and run it through the orchestrator.
Each Optuna trial:
- Generates a controller config from the search space
- Runs
BacktestingEngine.run_backtesting()on your chosen date range - Returns an objective value (Sharpe ratio by default, but you can override to use net PnL, max drawdown, etc.)
- Optuna's TPE sampler uses the result to guide the next trial toward better regions of the parameter space
This is Bayesian optimization, not neural-network training. The "model" is the parameter set that maximizes your objective on historical data.
Use the analysis notebook to inspect your study:
research_notebooks/optimization_analysis/analyze_optimization_results.ipynb
It provides:
- Objective value distribution across all trials
- Parameter importance plots (which hyperparameters matter most)
- Best trial config
- Equity curve for the best configuration
Retrieve best parameters programmatically:
import optuna
study = optuna.load_study(
study_name="macd_bb_btcusdt_v1",
storage="sqlite:///optuna_studies.db"
)
best_trial = study.best_trial
print(f"Best Sharpe: {best_trial.value:.4f}")
print(f"Best params: {best_trial.params}")Critical checks before deploying:
- Sharpe ratio > 1.0 on the training period
- Walk-forward test: re-run the best config on a held-out date range it has never seen
- The best config is not over-fitted (check that top-10 trials have similar performance)
- Max drawdown is within acceptable bounds for your risk appetite
- The number of trades is large enough (> 50) to be statistically significant
Translate the best parameters into a controller config YAML. This YAML is what the hummingbot-api stores and the hummingbot engine reads.
- Open the dashboard at
http://localhost:8501(or your hosted URL) - Go to Config → [Strategy Name] (e.g., MACD BB v1)
- Fill in the parameters from your optimization results
- Click "Backtest" to verify performance inline
- Click "Save Config" — enter a descriptive name (e.g.,
macd_bb_btcusdt_long_v1)
The config is saved to bots/conf/controllers/macd_bb_btcusdt_long_v1.yml on the API server.
import httpx
import yaml
controller_config = {
"id": "macd_bb_btcusdt_long_v1",
"controller_name": "macd_bb_v1",
"controller_type": "directional_trading",
"connector_name": "kucoin_perpetual",
"trading_pair": "BTC-USDT",
"interval": "5m",
"bb_length": 120,
"bb_std": 2.0,
"bb_long_threshold": 0.1,
"bb_short_threshold": 0.9,
"macd_fast": 12,
"macd_slow": 26,
"macd_signal": 9,
"total_amount_quote": "500",
"max_executors_per_side": 1,
"leverage": 10,
"position_mode": "HEDGE",
"triple_barrier_config": {
"stop_loss": "0.015",
"take_profit": "0.015",
"time_limit": 86400,
"open_order_type": 1, # MARKET = 1
"take_profit_order_type": 2, # LIMIT = 2
"stop_loss_order_type": 1,
}
}
response = httpx.post(
"https://api.metallorum.duckdns.org/controllers/configs/macd_bb_btcusdt_long_v1",
json=controller_config,
auth=("username", "password"),
)
response.raise_for_status()
print("Config saved:", response.json())from core.services.backend_api_client import BackendAPIClient
client = BackendAPIClient(
host="https://api.metallorum.duckdns.org",
username="username",
password="password",
)
await client.save_controller_config(config_dict=controller_config)For the trend-follower pipeline, the grid_strike controller is used. The key fields are price levels derived from the ema_trend feature:
id: tf_btcusdt_long_20250317_1200
controller_name: grid_strike
controller_type: generic
connector_name: kucoin_perpetual
trading_pair: BTC-USDT
side: 1 # 1=long, 2=short
position_mode: HEDGE
leverage: 20
start_price: "42000.0" # grid lower bound
end_price: "45000.0" # grid upper bound
limit_price: "41000.0" # stop loss level (below start for long)
total_amount_quote: "400"
max_open_orders: 2
max_orders_per_batch: 1
min_order_amount_quote: "6"
min_spread_between_orders: "0.002"
activation_bounds: "0.004"
order_frequency: 5
keep_position: false
triple_barrier_config:
open_order_type: 3 # LIMIT_MAKER
take_profit: "0.0016"
take_profit_order_type: 3Before deploying, run a final validation backtest through the API. This confirms the config is valid and gives you one last performance check.
In any strategy Config page, after loading your saved config, click Backtest. The dashboard calls POST /backtesting/run-backtesting and shows results inline.
import httpx
payload = {
"start_time": "2024-07-01 00:00:00",
"end_time": "2024-12-31 00:00:00",
"backtesting_resolution": "1m",
"trade_cost": 0.0006,
"config": {
"controller_name": "macd_bb_v1",
"connector_name": "kucoin_perpetual",
"trading_pair": "BTC-USDT",
# ... all params
}
}
response = httpx.post(
"https://api.metallorum.duckdns.org/backtesting/run-backtesting",
json=payload,
auth=("username", "password"),
timeout=120,
)
result = response.json()
print(f"Sharpe: {result['results']['sharpe_ratio']:.4f}")
print(f"Net PnL: {result['results']['net_pnl_quote']:.2f}")
print(f"Max Drawdown: {result['results']['max_drawdown_usd']:.2f}")Once you're satisfied with the config, deploy it. A single bot instance can run multiple controllers simultaneously (useful for deploying a portfolio of strategies in one container).
- Confirm the controller config YAML exists on the API server (
GET /controllers/configs/) - Confirm exchange credentials are configured (
GET /accounts/) - Check available Docker images (
GET /docker/images)
- Go to Orchestration → Deploy Bot
- Set Instance Name (e.g.,
macd-bb-portfolio-v1) - Select Credentials Profile (e.g.,
master_account) - Select Docker Image (e.g.,
hummingbot/hummingbot:latest) - Check the boxes next to the controller configs you want to include
- Optionally set drawdown limits
- Click Deploy Bot
import httpx
deployment = {
"instance_name": "macd-bb-portfolio-v1",
"credentials_profile": "master_account",
"controllers_config": [
"macd_bb_btcusdt_long_v1",
"macd_bb_ethusdt_long_v1",
],
"image": "hummingbot/hummingbot:latest",
"max_global_drawdown_quote": 1000.0,
"max_controller_drawdown_quote": 200.0,
}
response = httpx.post(
"https://api.metallorum.duckdns.org/bot-orchestration/deploy-v2-controllers",
json=deployment,
auth=("username", "password"),
)
response.raise_for_status()
print("Bot deployed:", response.json())await client.deploy_v2_controllers(
instance_name="macd-bb-portfolio-v1",
credentials_profile="master_account",
controllers_config=["macd_bb_btcusdt_long_v1", "macd_bb_ethusdt_long_v1"],
image="hummingbot/hummingbot:latest",
max_global_drawdown_quote=1000.0,
max_controller_drawdown_quote=200.0,
)- The API generates a script config YAML (
conf/scripts/{instance_name}-{timestamp}.yml):script_file_name: v2_with_controllers.py controllers_config: - macd_bb_btcusdt_long_v1.yml - macd_bb_ethusdt_long_v1.yml max_global_drawdown_quote: 1000.0 max_controller_drawdown_quote: 200.0
- A Docker container is created from the chosen image, with the bot's directory mounted as a volume.
- The hummingbot engine starts inside the container, reads the script config, loads all controller configs, and begins trading.
- The bot connects to the EMQX MQTT broker. The API discovers it and begins tracking its state.
- A
BotRunrecord is written to PostgreSQL with metadata about this deployment.
# All active bots
response = httpx.get(
"https://api.metallorum.duckdns.org/bot-orchestration/status",
auth=("username", "password"),
)
bots = response.json()
for bot_name, status in bots.items():
print(f"{bot_name}: {status['status']}")- Orchestration → Instances — shows all Docker containers and their states
- Performance → Bot Performance — P&L breakdown per controller, per bot, with equity curves
You can update a controller's parameters without restarting the bot:
response = httpx.post(
"https://api.metallorum.duckdns.org/controllers/bots/macd-bb-portfolio-v1/macd_bb_btcusdt_long_v1/config",
json={"take_profit": "0.02", "stop_loss": "0.01"}, # partial update
auth=("username", "password"),
)When a bot has run its course (or you want to replace it with a new version):
response = httpx.post(
"https://api.metallorum.duckdns.org/bot-orchestration/stop-and-archive-bot/macd-bb-portfolio-v1",
auth=("username", "password"),
)This gracefully stops all controllers, archives the bot's data to S3, and removes the Docker container.
The production pipeline in research_notebooks/bot_orchestration/tf_pipeline.ipynb demonstrates the full automated workflow. It runs on a schedule (every 30 minutes via the task orchestrator) and does everything automatically:
- Signal ingestion — reads
ema_trendsignals from MongoDB; selects top-N long (> 0.7) and short (< -0.7) pairs - Bot inventory — queries the API for all running
trend_follower_grid*bots - Stop decisions — stops controllers that have been running long enough AND whose signal has flipped or P&L is below threshold
- Archiving — archives bots where all controllers are stopped
- New deployments — for each new strong signal with no existing position, creates a
grid_strikeconfig and deploys a new bot - Notification — sends a Telegram summary of all actions taken
To build your own automated pipeline, use DeploymentBaseTask as a base:
# quants-lab/app/tasks/deployment/my_strategy_deployment.py
from app.tasks.deployment.deployment_base_task import DeploymentBaseTask
class MyStrategyDeploymentTask(DeploymentBaseTask):
async def get_candidates(self):
# Return list of controller configs to potentially deploy
signals = await self.storage.get_latest_signals(signal_name="my_signal", min_value=0.7)
return [self.signal_to_config(s) for s in signals]
async def should_stop_controller(self, bot_name, controller_name, status):
# Return True if this controller should be stopped
pnl = status.get("net_pnl_quote", 0)
return pnl < -self.max_controller_drawdownConsistent naming makes it easier to track which configs belong to which study and when they were deployed.
| Object | Convention | Example |
|---|---|---|
| Optuna study | {strategy}_{pair}_{version} |
macd_bb_btcusdt_v1 |
| Controller config | {strategy}_{pair}_{direction}_{date} |
macd_bb_btcusdt_long_20250317 |
| Bot instance | {strategy}-{portfolio-id} |
macd-bb-portfolio-v1 |
| Docker image tag | hummingbot/hummingbot:{version} |
hummingbot/hummingbot:latest |
| Action | Method | Endpoint |
|---|---|---|
| List all configs | GET | /controllers/configs/ |
| Get a config | GET | /controllers/configs/{name} |
| Create/update config | POST | /controllers/configs/{name} |
| Delete config | DELETE | /controllers/configs/{name} |
| Get config template | GET | /controllers/{type}/{name}/config/template |
| Validate config | POST | /controllers/{type}/{name}/config/validate |
| Action | Method | Endpoint |
|---|---|---|
| Deploy bot | POST | /bot-orchestration/deploy-v2-controllers |
| All bots status | GET | /bot-orchestration/status |
| Single bot status | GET | /bot-orchestration/{bot_name}/status |
| Bot history | GET | /bot-orchestration/{bot_name}/history |
| Start bot | POST | /bot-orchestration/start-bot |
| Stop bot | POST | /bot-orchestration/stop-bot |
| Stop and archive | POST | /bot-orchestration/stop-and-archive-bot/{bot_name} |
| Bot run history | GET | /bot-orchestration/bot-runs |
| Path | Description |
|---|---|
config/*.yml |
Task pipeline configs |
research_notebooks/data_collection/ |
Candle download notebooks |
research_notebooks/feature_engineering/ |
Feature computation notebooks |
research_notebooks/bot_orchestration/tf_pipeline.ipynb |
Main automated orchestration notebook |
research_notebooks/optimization_analysis/ |
Study analysis notebooks |
core/backtesting/optimizer.py |
StrategyOptimizer class |
core/features/storage.py |
MongoDB feature/signal storage |
core/services/backend_api_client.py |
HTTP client for hummingbot-api |
app/tasks/deployment/deployment_base_task.py |
Base class for autonomous deployment tasks |
Backtest has too few trades
- Widen the indicator thresholds (e.g., lower
bb_long_threshold) or use a shorter interval - Use a longer date range
Optimization runs very slowly
- Increase
n_jobs(parallel trials) — up to the number of available CPU cores - Use a shorter date range for the initial search, then refine on a longer range
- Pre-filter trading pairs to only the most liquid ones
Bot doesn't appear after deploy
- Check the MQTT broker is reachable from the bot container (
GET /bot-orchestration/mqtt) - Inspect the Docker container logs:
GET /docker/containers/{bot_name}/logs - Verify the credentials profile has valid API keys for the target exchange
Controller config validation fails
- Use
POST /controllers/{type}/{name}/config/validateto get a detailed error message - Check that all required fields are present and that numeric fields are strings where Decimal is expected
Live update not reflected
- There is a short polling delay. The bot controller reads its config every few seconds.
- If the bot appears stuck, check its status via
GET /bot-orchestration/{bot_name}/status