Zooni
Zooni is an autonomous algorithmic trading bot built in Rust. It runs grid trading strategies on cryptocurrency exchanges, with built-in market intelligence to pick symbols, size positions, and switch between opportunities automatically.
What Zooni Does
- Analyzes markets using technical indicators (RSI, ADX, ATR, Bollinger Bands, VWAP) to detect whether a market is ranging, trending, or volatile
- Picks the best symbols by scanning all available pairs and ranking them by grid-trading suitability
- Deploys grid orders — layered buy and sell limit orders that capture profit from price oscillations
- Manages risk with per-symbol exposure caps, daily/weekly loss limits, max drawdown protection, and a kill switch
- Monitors and adapts — periodically re-scans the market and switches to better opportunities when they appear
Key Properties
- Written in Rust — single binary, no runtime dependencies, minimal memory footprint
- Persistent — SQLite order journal survives restarts; state auto-recovers
- Observable — web dashboard, terminal TUI, Telegram bot, Prometheus metrics, webhook alerts
- Safe by default — testnet mode, dry-run mode, circuit breakers, rate limiting
- Self-contained — no external services required beyond the exchange API
Binaries
| Binary | Purpose |
|---|---|
trading-bot | Main bot — runs grids, autopilot, dashboard, Telegram |
backtest | Test strategies against historical or synthetic price data |
scan | One-shot market scanner — rank symbols by grid suitability |
Version
Zooni uses date-based versioning (YYYY.M.D). Check your version:
./trading-bot --version
# zooni 2026.4.23 (29db06d)
Installation
Prerequisites
Build from Source
git clone https://github.com/syedhamidali/zooni.git
cd zooni
cargo build --release
This produces three binaries in target/release/:
| Binary | Description |
|---|---|
trading-bot | Main trading bot |
backtest | Strategy backtester |
scan | Market scanner |
Docker
docker build -t zooni .
See Docker deployment for details.
GitHub Releases
Pre-built Linux binaries are attached to every GitHub release. Download and run directly — no Rust toolchain needed.
Verify Installation
./target/release/trading-bot --version
# zooni 2026.4.23 (abc1234)
Configuration
Zooni is configured via a single TOML file. Copy the example and edit:
cp config.example.toml config.toml
Never commit
config.toml— it contains API secrets. It is in.gitignore.
Minimal Config
The smallest working config for testnet dry-run:
[bot]
dry_run = true
[bybit]
api_key = ""
api_secret = ""
testnet = true
[grid]
symbol = "BTCUSDT"
lower_price = 60000.0
upper_price = 70000.0
levels = 10
qty_per_level_base = 0.001
[risk]
starting_equity_quote = 10000.0
Config Sections
| Section | Purpose |
|---|---|
[bot] | Dry-run, database path, retry/circuit-breaker settings |
[bybit] | Exchange API credentials and endpoint settings |
[grid] | Grid strategy parameters (symbol, range, levels, qty) |
[risk] | Exposure limits, loss caps, drawdown thresholds |
[alerts] | Webhook URL and notification filters |
[autopilot] | Autonomous mode settings |
[autopilot.scanner] | Market scanner tuning |
See the Configuration Reference for a complete field-by-field listing.
Environment Variables
| Variable | Purpose |
|---|---|
RUST_LOG | Log verbosity (e.g., info, debug, trading_bot=debug,reqwest=warn) |
TELEGRAM_BOT_TOKEN | Telegram bot token for remote control |
TELEGRAM_CHAT_ID | Telegram chat ID for notifications |
Security Notes
- API keys are stored in plain text in
config.toml. Protect file permissions:chmod 600 config.toml - On a VPS, consider using environment variables or a secrets manager instead of a config file
- The
.gitignoreblocksconfig.toml,.env,*.key,*.pem, and*.secret
First Run
1. Start with Dry-Run on Testnet
Always test on testnet with dry-run first:
./target/release/trading-bot config.toml --dry-run
This simulates the full trading cycle without placing real orders. Watch the logs to verify:
- Grid levels are calculated correctly for your price range
- Risk checks pass for your equity and exposure settings
- Fill detection and order replacement logic works
2. Testnet with Real Orders
Once dry-run looks good, remove --dry-run but keep testnet = true:
./target/release/trading-bot config.toml
Log in to Bybit Testnet to verify orders appear on the exchange.
3. Go Live
When you’re confident:
- Set
testnet = falseinconfig.toml - Replace API keys with your mainnet credentials
- Set
starting_equity_quoteto your actual balance - Start conservatively — fewer levels, smaller
qty_per_level_base
./target/release/trading-bot config.toml
Running Modes
Standard Mode
Trade a single symbol configured in [grid]:
./trading-bot config.toml
TUI Mode
Same as standard, but with a live terminal dashboard:
./trading-bot config.toml --tui
Auto-Pilot Mode
Fully autonomous — scans markets, picks symbols, sizes positions, and switches automatically:
# Enable in config
# [autopilot]
# enabled = true
./trading-bot config.toml --autopilot
See Auto-Pilot Mode for details.
Graceful Shutdown
Press Ctrl+C to shut down. The bot will:
- Stop placing new orders
- Wait for in-flight API calls to complete (configurable via
shutdown_wait_secs) - Cancel remaining open orders on the exchange
- Save final state to the database
Logs
Control verbosity with RUST_LOG:
# Default (info level)
RUST_LOG=info ./trading-bot config.toml
# Debug everything
RUST_LOG=debug ./trading-bot config.toml
# Debug bot logic, quiet HTTP noise
RUST_LOG=trading_bot=debug,reqwest=warn ./trading-bot config.toml
Grid Trading
Grid trading is an automated strategy that profits from price oscillations within a range. It works best in sideways/ranging markets where price bounces between support and resistance.
How It Works
- The bot divides a price range into evenly spaced grid levels
- Buy orders are placed below the current price
- Sell orders are placed above the current price
- When a buy fills, a corresponding sell is placed one level up
- When a sell fills, a corresponding buy is placed one level down
- Each complete buy-sell cycle captures the grid spacing as profit
Price
^
| -------- SELL @ 65,000 (placed after buy fills)
| -------- SELL @ 64,000
| ~~~~~~~~ current price ~63,500
| -------- BUY @ 63,000
| -------- BUY @ 62,000 (placed after sell fills)
|
Profit Per Cycle
Each complete buy-sell round trip earns approximately:
profit = grid_spacing * qty_per_level
For example, with 20 levels across 60,000–70,000 USDT and 0.001 BTC per level:
- Grid spacing = (70,000 - 60,000) / 20 = 500 USDT
- Profit per cycle = 500 * 0.001 = 0.50 USDT (minus fees)
Configuration
[grid]
symbol = "BTCUSDT"
lower_price = 60000.0 # grid bottom
upper_price = 70000.0 # grid top
levels = 20 # number of grid lines
qty_per_level_base = 0.001 # BTC per order
poll_secs = 3 # check for fills every 3 seconds
Key Parameters
Levels
More levels = tighter spacing = more frequent trades = smaller profit per trade. Fewer levels = wider spacing = less frequent but larger profits. Find the sweet spot for your market’s typical oscillation range.
Price Range
- Too narrow: Price breaks out quickly, grid becomes one-sided
- Too wide: Spacing is too large, trades are infrequent
- Right size: Covers the recent support/resistance zone
Auto-Rebalance
When price exits the grid range, you can either:
- Wait for price to return (default behavior)
- Auto-rebalance — shift the entire grid to re-center around current price
auto_rebalance = true
Trailing Profit
Lock in accumulated gains when grid fills exceed a threshold:
trailing_profit_threshold = 50.0 # USDT — take profit above this
When Grid Trading Works
| Market Condition | Grid Performance |
|---|---|
| Ranging/sideways | Excellent — frequent fills, consistent profit |
| Low volatility range | Good — steady but slower |
| Trending up | Poor — all buys fill, sells don’t |
| Trending down | Poor — all sells fill, buys don’t |
| High volatility | Risky — potential for large unrealized losses |
Zooni’s market intelligence detects these regimes and can pause or switch symbols automatically in auto-pilot mode.
Fill Detection
The bot polls open orders every poll_secs seconds. When an order disappears from the open orders list, it’s treated as filled, and a replacement order is placed on the opposite side.
Order Deduplication
Every order gets a unique orderLinkId to prevent duplicate placements during retries or restarts.
Risk Management
Zooni enforces multiple layers of risk protection. Every order passes through the risk engine before reaching the exchange.
Risk Controls
Exposure Limits
Caps how much capital can be deployed at once:
| Limit | Default | Description |
|---|---|---|
max_total_exposure | 80% | Maximum total portfolio exposure |
max_per_market | 40% | Maximum exposure per market type |
max_per_symbol | 20% | Maximum exposure per trading pair |
max_per_strategy | 15% | Maximum exposure per strategy |
All values are fractions of starting_equity_quote.
Loss Limits
Automatic pause when losses exceed thresholds:
| Limit | Default | What Happens |
|---|---|---|
daily_loss_limit | 5% | Pauses new orders for the rest of the day |
weekly_loss_limit | 10% | Pauses new orders for the rest of the week |
max_drawdown | 15% | Pauses until equity recovers above threshold |
Kill Switch
Ctrl+C triggers an atomic kill switch that immediately stops all trading. The bot will:
- Cancel all open orders on the exchange
- Wait for in-flight API calls (up to
shutdown_wait_secs) - Save state to the database
Circuit Breaker
After circuit_breaker_threshold consecutive API failures (default: 5), the bot pauses all API calls for circuit_breaker_cooldown_secs (default: 60). It auto-resets after the cooldown.
Configuration
[risk]
starting_equity_quote = 10000.0 # your actual USDT balance
max_total_exposure = 0.80
max_per_market = 0.40
max_per_symbol = 0.20
max_per_strategy = 0.15
daily_loss_limit = 0.05
weekly_loss_limit = 0.10
max_drawdown = 0.15
How Pre-Trade Checks Work
Before every order placement:
- Kill switch — is the bot shutting down?
- Daily P&L — has the daily loss limit been hit?
- Weekly P&L — has the weekly loss limit been hit?
- Drawdown — is equity too far below peak?
- Symbol exposure — would this order exceed the per-symbol cap?
- Total exposure — would total exposure exceed the portfolio cap?
If any check fails, the order is rejected and the reason is logged. Webhook alerts fire for risk breaches if configured.
P&L Tracking
All fills are recorded in the SQLite database with realized P&L. Query your performance:
# Daily P&L summary
sqlite3 trading-bot.db \
"SELECT date(filled_at) as day, SUM(realised_pnl) as pnl, COUNT(*) as fills
FROM fills GROUP BY day ORDER BY day;"
# Total P&L
sqlite3 trading-bot.db \
"SELECT SUM(realised_pnl) as total_pnl, COUNT(*) as total_fills FROM fills;"
Regime-Aware Pausing
In auto-pilot mode, the bot also considers market regime. If the regime detector classifies the market as strongly trending, the bot can pause grid trading for that symbol and wait for conditions to improve. See Market Intelligence.
Market Intelligence
Zooni uses technical indicators and regime detection to evaluate whether a market is suitable for grid trading, and to size positions appropriately.
Technical Indicators
| Indicator | Function | Used For |
|---|---|---|
| SMA | Simple Moving Average | Trend direction baseline |
| EMA | Exponential Moving Average | Responsive trend detection |
| RSI | Relative Strength Index (0-100) | Overbought/oversold detection |
| ADX | Average Directional Index (0-100) | Trend strength measurement |
| ATR | Average True Range | Volatility measurement |
| Bollinger Bands | Price envelope (upper, mid, lower) | Range/breakout detection |
| VWAP | Volume-Weighted Average Price | Fair value reference |
Regime Detection
The regime detector classifies the market into four states based on indicator values:
| Regime | Conditions | Grid Suitability |
|---|---|---|
| Ranging | Low ADX (< 25), neutral RSI, tight BB | Best — frequent oscillations |
| Trending Up | High ADX (> 25), RSI > 60 | Poor — buys fill, sells don’t |
| Trending Down | High ADX (> 25), RSI < 40 | Poor — sells fill, buys don’t |
| Volatile | Wide Bollinger bandwidth (> 20%) | Risky — large swings |
Classification Logic
if bollinger_bandwidth > 0.20 → Volatile
else if adx > 25 and rsi > 60 → Trending Up
else if adx > 25 and rsi < 40 → Trending Down
else → Ranging
Regime Analysis Output
Each analysis produces:
- Regime classification — Ranging, TrendingUp, TrendingDown, Volatile
- Grid suitability flag — true if conditions favor grid trading
- Suggested price range — derived from Bollinger Bands (lower to upper)
- Suggested levels — based on ATR and range width
- Volatility percentage — ATR as a percentage of price
Scoring
The scanner combines regime analysis into a composite score for ranking symbols:
- Ranging markets score highest
- Low volatility within range is preferred
- High 24h turnover indicates good liquidity
- Tight bid-ask spread means lower execution cost
How It’s Used
Manual Mode
The bot runs regime detection every ~5 minutes and can auto-pause if conditions become unfavorable (trending or volatile).
Auto-Pilot Mode
The scanner uses regime detection to rank all available symbols and deploy grids on the best candidates. See Auto-Pilot Mode.
Backtester
The backtester can use ATR-based adaptive sizing to adjust position size with volatility. See Backtester.
Position Sizing
The sizing engine blends two approaches:
- Kelly Criterion — optimal bet size based on win rate and payoff ratio
- Regime Adjustment — scales position size based on market conditions
kelly_fraction = (win_rate * avg_win - (1 - win_rate) * avg_loss) / avg_win
adjusted_size = base_size * kelly_blend * regime_factor * performance_factor
The performance factor dynamically scales up during profitable streaks and down during losing streaks, providing natural risk adjustment.
Auto-Pilot Mode
Auto-pilot is Zooni’s fully autonomous mode. Instead of trading a single pre-configured symbol, the bot scans the entire market, picks the best opportunities, deploys grids, and switches when conditions change.
How It Works
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐
│ Scan │ ──> │ Score │ ──> │ Deploy │ ──> │ Trade │
│ Market │ │ & Rank │ │ Grid │ │ Loop │
└─────────┘ └──────────┘ └──────────┘ └─────┬─────┘
^ │
│ Re-scan every N minutes │
└────────────────────────────────────────────────────┘
1. Scan
Fetches all trading pairs from the exchange and pre-filters by:
- Minimum 24h turnover (liquidity threshold)
- Maximum bid-ask spread percentage
- Quote currency (default: USDT)
2. Score & Rank
For each candidate, fetches historical klines and runs:
- Regime detection (ADX, RSI, Bollinger Bands)
- Volatility analysis (ATR as % of price)
- Grid suitability scoring
Symbols are ranked by composite score. Ranging markets with moderate volatility and high liquidity score highest.
3. Deploy
For the top-ranked symbol(s):
- Calculates optimal grid range from Bollinger Bands
- Determines position size using Kelly criterion + regime adjustment
- Places the grid orders
4. Trade Loop
Standard grid trading cycle (poll for fills, place replacements). Periodically re-scans and evaluates:
- If current symbol’s score drops significantly
- If a new symbol scores much higher (above
switch_threshold) - If current grid is losing money
5. Switch
When a better opportunity is found:
- Cancel existing grid orders
- Wait for in-flight orders to settle
- Deploy new grid on the better symbol
A hysteresis threshold (switch_threshold = 15.0 by default) prevents excessive switching.
Configuration
[autopilot]
enabled = true
scan_interval_secs = 1800 # re-scan every 30 minutes
switch_threshold = 15.0 # score improvement needed to switch
max_grids = 1 # simultaneous grids (symbols)
[autopilot.scanner]
min_turnover = 1000000.0 # minimum 24h turnover (USDT)
max_spread_pct = 0.2 # maximum bid-ask spread %
quote_currencies = ["USDT"]
max_results = 10 # top N candidates to analyze in depth
kline_interval = "60" # candlestick interval (minutes)
kline_count = 200 # candles to fetch for analysis
Running
# Via config
# Set autopilot.enabled = true in config.toml
./trading-bot config.toml
# Via CLI flag (overrides config)
./trading-bot config.toml --autopilot
Multi-Grid
Set max_grids > 1 to run grids on multiple symbols simultaneously. The bot will deploy grids on the top N ranked symbols and manage them independently.
Multi-grid mode divides your equity across grids. Ensure your
starting_equity_quoteand risk limits account for this.
Notes
- In auto-pilot mode, the
[grid]config section (symbol, lower_price, upper_price) is ignored — these are computed dynamically - Grid parameters (levels, qty) are still respected as base values for sizing
- All risk limits still apply — auto-pilot doesn’t bypass the risk engine
- The scanner only uses public API endpoints (no credentials needed for scanning)
Scanner
The scanner is a standalone tool that analyzes all available trading pairs and ranks them by grid-trading suitability. It’s the same engine used by auto-pilot mode, but runs as a one-shot CLI.
Usage
# Basic scan with defaults
cargo run --release --bin scan -- --config config.toml
# Top 5 results
cargo run --release --bin scan -- --config config.toml --top 5
# Custom filters
cargo run --release --bin scan -- \
--config config.toml \
--quote USDT \
--min-turnover 5000000 \
--top 10
CLI Options
| Flag | Default | Description |
|---|---|---|
--config | config.toml | Config file path (uses [bybit] for API endpoint) |
--top | 10 | Number of top results to show |
--quote | USDT | Quote currency to filter by |
--min-turnover | From config | Minimum 24h turnover in quote currency |
--interval | 60 | Kline interval in minutes |
Output
The scanner prints a formatted report:
=== Market Scan Report ===
Scanned 150 pairs, analyzed top 10
#1 ETHUSDT score: 82.3 regime: Ranging
ADX: 18.2 RSI: 48.7 BB width: 3.2% Vol: 1.8%
Range: 3,200.00 - 3,450.00 Levels: 15
#2 SOLUSDT score: 71.5 regime: Ranging
ADX: 21.4 RSI: 52.1 BB width: 4.1% Vol: 2.3%
Range: 145.00 - 158.00 Levels: 12
...
Followed by a CSV summary for easy import into spreadsheets.
What Gets Scored
- Pre-filter: Remove pairs with low turnover or wide spreads
- Fetch klines: Get recent candlestick data for each candidate
- Regime analysis: Compute ADX, RSI, Bollinger Bands, ATR
- Score: Composite of regime suitability, volatility, and liquidity
- Rank: Sort by score, highest first
Scanner Configuration
Fine-tune via [autopilot.scanner] in your config:
[autopilot.scanner]
min_turnover = 1000000.0 # filter out illiquid pairs
max_spread_pct = 0.2 # filter out wide-spread pairs
quote_currencies = ["USDT"] # which quote currencies
max_results = 10 # analyze top N after pre-filter
kline_interval = "60" # candle interval (minutes)
kline_count = 200 # how many candles to fetch
Notes
- The scanner uses public API endpoints only — no API credentials required for scanning
- Kline fetches are rate-limited to avoid hitting exchange limits
- Scan time depends on
max_results(each candidate requires a kline fetch)
Backtester
The backtester simulates grid trading strategies against historical price data. Use it to find optimal parameters before risking real capital.
Usage
# Backtest against a CSV price file
cargo run --release --bin backtest -- config.toml --data prices.csv
# Against synthetic generated data
cargo run --release --bin backtest -- config.toml --synthetic 5000
# Optimizer sweep — find the best levels/range combination
cargo run --release --bin backtest -- config.toml --optimize --data prices.csv
# Adaptive mode — ATR-based dynamic sizing
cargo run --release --bin backtest -- config.toml --adaptive --data prices.csv
CLI Options
| Flag | Description |
|---|---|
--data <path> | CSV file with historical prices |
--synthetic <N> | Generate N bars of synthetic data instead |
--optimize | Sweep over levels and price ranges, rank results |
--adaptive | Use ATR-based adaptive position sizing |
CSV Format
The price data CSV should have columns for OHLCV data. The backtester parses standard candlestick format.
Output
Standard Backtest
=== Backtest Results ===
Symbol: BTCUSDT
Period: 5000 bars
Levels: 20, Range: 60000.00 - 70000.00
Total PnL: +245.30 USDT
Total Fills: 347
Win Rate: 68.4%
Sharpe Ratio: 1.82
Max Drawdown: -3.2%
Optimizer
The optimizer sweeps multiple configurations and ranks by composite score:
=== Optimization Results ===
Config PnL Sharpe Drawdown WinRate
15 levels +312.40 2.10 -2.8% 72.1%
20 levels +245.30 1.82 -3.2% 68.4%
25 levels +198.70 1.65 -3.5% 65.2%
Best config: 15 levels, range 61000.00-69000.00
Notes
- The backtester uses the same grid logic as the live bot — results are representative
- Synthetic data uses random walk with configurable volatility — useful for stress testing
- Fees are not simulated — subtract exchange fees from reported P&L for realistic estimates
- The backtester does not simulate order book depth or slippage
Web Dashboard
Zooni includes an embedded web dashboard that starts automatically with the bot. No external dependencies — the HTML/CSS/JS is compiled into the binary.
Access
The dashboard runs at http://localhost:9090 by default.
Features
- Stats Cards — current price, equity, daily P&L, total P&L
- Price Chart — canvas-rendered price history with area fill
- Equity Chart — equity curve over time
- Grid Visualization — active grid levels with buy/sell indicators
- Fills Table — recent fills with side, price, quantity, and P&L
- Regime Badge — current market regime (Ranging, Trending, Volatile)
- Auto-refresh — updates every 2 seconds
Design
- Dark theme optimized for monitoring
- No external JavaScript dependencies
- Custom
MiniChartcanvas library for lightweight rendering - Responsive layout
Endpoints
| Path | Content-Type | Description |
|---|---|---|
GET / | text/html | Interactive dashboard |
GET /health | text/plain | Health check ("ok") |
GET /status | application/json | Bot status snapshot |
GET /metrics | text/plain | Prometheus exposition format |
GET /api/live | application/json | Full live data for dashboard |
/api/live Response
{
"price_history": [64500.0, 64520.0, ...],
"equity_history": [10000.0, 10002.5, ...],
"pnl_history": [0.0, 2.5, ...],
"recent_fills": [
{
"time": "2026-04-23T10:15:00Z",
"symbol": "BTCUSDT",
"side": "Buy",
"price": 64500.0,
"qty": 0.001,
"pnl": 0.5
}
],
"grid_levels": [
{ "price": 64000.0, "side": "Buy", "status": "open" },
{ "price": 65000.0, "side": "Sell", "status": "open" }
],
"regime": "Ranging",
"regime_details": "ADX: 18.2, RSI: 48.7"
}
/status Response
{
"orders_placed": 42,
"orders_filled": 28,
"orders_failed": 0,
"daily_pnl": 12.50,
"total_pnl": 45.30,
"equity": 10045.30,
"uptime_secs": 3600
}
TUI Terminal Dashboard
Zooni includes a terminal-based UI built with ratatui for monitoring directly in your terminal.
Usage
./trading-bot config.toml --tui
Display
The TUI shows:
- Header — bot name, symbol, mode (live/dry-run), uptime
- Grid Status — current grid levels with buy/sell prices
- Current Price — live mid-price
- P&L — daily and total realized P&L
- Fill Count — number of fills in current session
- Open Orders — count of active orders on the exchange
- Circuit Breaker — status (closed/open) and failure count
Controls
| Key | Action |
|---|---|
q | Quit (graceful shutdown) |
Ctrl+C | Force quit |
Notes
- The TUI disables the standard log output to avoid display corruption. Logs are still written if a file appender is configured.
- The TUI shares the same trading state as the bot — it’s not a separate process.
- For remote monitoring, use the Web Dashboard or Telegram Bot instead.
Telegram Bot
Zooni includes a Telegram bot for remote monitoring and control. It polls for commands and responds with live bot status.
Setup
1. Create a Telegram Bot
- Message @BotFather on Telegram
- Send
/newbotand follow the prompts - Copy the bot token (e.g.,
123456:ABC-DEF...)
2. Get Your Chat ID
- Message your new bot
- Visit
https://api.telegram.org/bot<TOKEN>/getUpdates - Find your
chat.idin the response
3. Set Environment Variables
export TELEGRAM_BOT_TOKEN="123456:ABC-DEF..."
export TELEGRAM_CHAT_ID="987654321"
The bot starts automatically when both variables are set.
Commands
| Command | Description |
|---|---|
/status | Current bot status (mode, uptime, circuit breaker) |
/pnl | Daily and total P&L |
/grid | Active grid levels and fill count |
/pause | Pause trading (keeps bot running) |
/resume | Resume trading after pause |
/fills | Recent fill history |
How It Works
- The Telegram module polls
getUpdatesevery 5 seconds - Only messages from the configured
TELEGRAM_CHAT_IDare processed - Status data comes from shared
Arc<Metrics>— same data as the web dashboard - The bot runs as a background task and doesn’t interfere with trading
Security
- Only your configured chat ID can send commands
- The bot token should be kept secret — never commit it
- Consider using a dedicated bot for each bot instance
Alerts & Webhooks
Zooni can send notifications via webhook to Slack, Discord, or any HTTP POST endpoint.
Setup
Add a webhook URL to your config:
[alerts]
webhook_url = "https://hooks.slack.com/services/T.../B.../..."
Event Types
| Event | Config Flag | Default | Alert Level |
|---|---|---|---|
| Order fill | on_fill | false | Info |
| Risk breach | on_risk_breach | true | Critical |
| Bot start/stop | on_lifecycle | true | Info |
| API error | on_error | true | Warning |
| P&L summary | pnl_summary_interval_secs | 0 (disabled) | Info |
Configuration
[alerts]
webhook_url = "https://hooks.slack.com/services/..."
on_fill = false # fills can be noisy on tight grids
on_risk_breach = true # always know when limits are hit
on_lifecycle = true # startup/shutdown notifications
on_error = true # API failures, circuit breaker trips
pnl_summary_interval_secs = 3600 # hourly P&L digest (0 = disabled)
Message Format
Alerts are sent as Slack-compatible JSON:
{"text": "ℹ️ *[zooni]* `lifecycle` — Bot started on BTCUSDT (testnet, dry-run)"}
Alert Levels
| Level | Prefix | Used For |
|---|---|---|
| Info | ℹ️ | Fills, lifecycle, P&L summaries |
| Warning | ⚠️ | API errors, circuit breaker |
| Critical | 🚨 | Risk limit breaches |
Webhook Compatibility
The payload uses Slack’s {"text": "..."} format, which is also compatible with:
- Slack — Incoming Webhooks
- Discord — Webhook URLs (via Slack-compatible endpoint)
- Any HTTP service — that accepts POST with JSON body
Fire-and-Forget
Alert delivery is non-blocking. If the webhook fails (network error, timeout), it’s logged but never blocks or crashes the trading logic. The webhook has a 10-second timeout.
Prometheus Metrics
Zooni exposes metrics in Prometheus exposition format for integration with Grafana or other monitoring stacks.
Endpoint
GET http://localhost:9090/metrics
Available Metrics
# HELP orders_placed Total orders placed
# TYPE orders_placed counter
orders_placed 42
# HELP orders_filled Total orders filled
# TYPE orders_filled counter
orders_filled 28
# HELP orders_failed Total orders failed
# TYPE orders_failed counter
orders_failed 0
# HELP daily_pnl Daily realized P&L (USDT)
# TYPE daily_pnl gauge
daily_pnl 12.50
# HELP total_pnl Total realized P&L (USDT)
# TYPE total_pnl gauge
total_pnl 45.30
# HELP equity Current equity (USDT)
# TYPE equity gauge
equity 10045.30
# HELP uptime_seconds Bot uptime in seconds
# TYPE uptime_seconds counter
uptime_seconds 3600
Grafana Setup
- Add Prometheus as a data source pointing to
http://<bot-host>:9090/metrics - Create dashboards for:
- P&L over time (
daily_pnl,total_pnl) - Order flow (
orders_placed,orders_filled,orders_failed) - Equity curve (
equity) - Uptime (
uptime_seconds)
- P&L over time (
Alertmanager
Configure Prometheus Alertmanager rules for:
- alert: HighFailureRate
expr: orders_failed / orders_placed > 0.1
for: 5m
annotations:
summary: "Order failure rate above 10%"
- alert: NegativeDailyPnL
expr: daily_pnl < -100
for: 1m
annotations:
summary: "Daily P&L below -100 USDT"
Docker Deployment
Zooni ships with a multi-stage Dockerfile that produces a minimal runtime image.
Build
docker build -t zooni .
The build:
- Compiles all three binaries (
trading-bot,backtest,scan) in a Rust builder stage - Copies only the binaries into a slim Debian runtime image
- Final image includes just
ca-certificatesfor HTTPS
Run
# Basic run
docker run -v $(pwd)/config.toml:/data/config.toml zooni
# With Telegram
docker run \
-e TELEGRAM_BOT_TOKEN="123456:ABC..." \
-e TELEGRAM_CHAT_ID="987654321" \
-v $(pwd)/config.toml:/data/config.toml \
zooni
# Expose dashboard
docker run \
-p 9090:9090 \
-v $(pwd)/config.toml:/data/config.toml \
zooni
# Persistent database
docker run \
-v $(pwd)/config.toml:/data/config.toml \
-v $(pwd)/data:/data \
zooni
# Dry-run mode
docker run \
-v $(pwd)/config.toml:/data/config.toml \
zooni config.toml --dry-run
Docker Compose
services:
zooni:
build: .
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./config.toml:/data/config.toml:ro
- zooni-data:/data
environment:
- RUST_LOG=info
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
volumes:
zooni-data:
Scanner in Docker
docker run --rm \
-v $(pwd)/config.toml:/data/config.toml \
--entrypoint scan \
zooni --config config.toml --top 10
Notes
- Config is expected at
/data/config.toml - Database is created at
/data/trading-bot.db - Mount
/dataas a volume to persist the database across restarts - The image is based on
debian:bookworm-slim(~80MB)
Systemd (VPS) Deployment
Run Zooni as a systemd service on any Linux VPS for production use.
Setup
1. Create a Service User
sudo useradd -r -s /bin/false trading
2. Install the Binary
sudo mkdir -p /opt/trading-bot
sudo cp target/release/trading-bot /opt/trading-bot/
sudo cp config.toml /opt/trading-bot/
sudo chown -R trading:trading /opt/trading-bot
sudo chmod 600 /opt/trading-bot/config.toml
3. Install the Service File
sudo cp trading-bot.service /etc/systemd/system/
sudo systemctl daemon-reload
4. Start
sudo systemctl enable --now trading-bot
Service File
The included trading-bot.service has security hardening:
[Unit]
Description=Trading Bot - Grid Strategy
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=trading
Group=trading
WorkingDirectory=/opt/trading-bot
ExecStart=/opt/trading-bot/trading-bot /opt/trading-bot/config.toml
Restart=always
RestartSec=10
Environment=RUST_LOG=info
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/trading-bot
PrivateTmp=true
# Resource limits
LimitNOFILE=65536
MemoryMax=512M
[Install]
WantedBy=multi-user.target
Management
# Status
sudo systemctl status trading-bot
# Logs (live)
sudo journalctl -u trading-bot -f
# Logs (last hour)
sudo journalctl -u trading-bot --since "1 hour ago"
# Stop
sudo systemctl stop trading-bot
# Restart
sudo systemctl restart trading-bot
# Disable auto-start
sudo systemctl disable trading-bot
Adding Telegram
Add environment variables to the service:
sudo systemctl edit trading-bot
[Service]
Environment=TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
Environment=TELEGRAM_CHAT_ID=987654321
Then restart:
sudo systemctl restart trading-bot
Updating
# Build new binary
cargo build --release
# Deploy
sudo systemctl stop trading-bot
sudo cp target/release/trading-bot /opt/trading-bot/
sudo systemctl start trading-bot
Exposing the Dashboard
The dashboard listens on port 9090. To expose it:
# Simple: allow port through firewall
sudo ufw allow 9090
# Better: reverse proxy with nginx
# /etc/nginx/sites-available/zooni
server {
listen 443 ssl;
server_name zooni.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:9090;
}
}
For a private dashboard, use SSH tunneling instead:
ssh -L 9090:localhost:9090 your-vps
CI/CD
Zooni uses GitHub Actions for continuous integration and automated releases.
Pipeline
Every push and PR to main triggers:
┌─────────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ Check & Lint│ ──> │ Test │ ──> │ Build Release│ ──> │ Auto-Release│
│ fmt, clippy │ │ all tests│ │ binaries │ │ (main only) │
└─────────────┘ └─────────┘ └─────────────┘ └─────────────┘
1. Check & Lint
cargo fmt --all -- --check— enforces consistent formattingcargo clippy --all-targets --all-features— catches common mistakes, treated as errors (-D warnings)
2. Test
cargo test --all— runs all 64 tests across all crates
3. Build Release
cargo build --release --all-targets— full release build- Uploads binaries (
trading-bot,backtest,scan) as GitHub Actions artifacts
4. Auto-Release
On pushes to main only:
- Generates a date-based version tag:
v2026.4.23.<run_number> - Creates a GitHub Release with the binaries attached
- Includes auto-generated release notes from commit messages
Versioning
Tags follow the pattern vYYYY.M.D.N where N is the GitHub Actions run number. This handles multiple releases per day.
Workflow File
The pipeline is defined in .github/workflows/ci.yml. Key settings:
RUSTFLAGS: "-D warnings"— all warnings are errors- Uses
dtolnay/rust-toolchain@stablefor consistent Rust versions - Uses
Swatinem/rust-cache@v2to cache cargo builds between runs - Release job requires
contents: writepermission
Running Locally
Replicate what CI does:
# Same checks CI runs
cargo fmt --all -- --check
cargo clippy --all-targets --all-features
cargo test --all
cargo build --release --all-targets
Configuration Reference
Complete reference for all fields in config.toml.
[bot]
General bot settings.
| Field | Type | Default | Description |
|---|---|---|---|
dry_run | bool | false | Simulate orders without exchange calls |
db_path | string | "trading-bot.db" | SQLite database file path |
max_retries | u32 | 3 | Max retry attempts per API call |
circuit_breaker_threshold | u32 | 5 | Consecutive failures before circuit break |
circuit_breaker_cooldown_secs | u64 | 60 | Seconds to pause after circuit break |
shutdown_wait_secs | u64 | 10 | Seconds to wait for in-flight orders at shutdown |
[bybit]
Exchange API credentials and connection settings.
| Field | Type | Default | Description |
|---|---|---|---|
api_key | string | required | Bybit API key (empty string OK in dry-run) |
api_secret | string | required | Bybit API secret |
testnet | bool | true | Use testnet endpoints |
recv_window_ms | u64 | 5000 | Request receive window (milliseconds) |
account_type | string | "UNIFIED" | Account type for wallet balance lookups |
[grid]
Grid trading strategy parameters. Ignored when autopilot.enabled = true.
| Field | Type | Default | Description |
|---|---|---|---|
symbol | string | required | Trading pair (e.g., "BTCUSDT") |
lower_price | f64 | required | Grid lower price bound |
upper_price | f64 | required | Grid upper price bound (must be > lower) |
levels | u32 | required | Number of grid lines (must be >= 2) |
qty_per_level_base | f64 | required | Base asset quantity per grid order |
poll_secs | u64 | 3 | Seconds between fill-detection polls |
cancel_existing_on_start | bool | true | Cancel all existing orders on symbol at startup |
auto_rebalance | bool | false | Shift grid when price exits the range |
trailing_profit_threshold | f64 | 0.0 | Take-profit threshold in quote currency (0 = disabled) |
[risk]
Risk management limits. All fraction values are relative to starting_equity_quote.
| Field | Type | Default | Description |
|---|---|---|---|
starting_equity_quote | f64 | required | Starting balance in quote currency (USDT) |
max_total_exposure | f64 | 0.80 | Max total portfolio exposure (80%) |
max_per_market | f64 | 0.40 | Max exposure per market type |
max_per_symbol | f64 | 0.20 | Max exposure per trading pair |
max_per_strategy | f64 | 0.15 | Max exposure per strategy |
daily_loss_limit | f64 | 0.05 | Daily loss limit (5%) |
weekly_loss_limit | f64 | 0.10 | Weekly loss limit (10%) |
max_drawdown | f64 | 0.15 | Max drawdown from equity peak (15%) |
[alerts]
Webhook notification settings. Omit the entire section to disable alerts.
| Field | Type | Default | Description |
|---|---|---|---|
webhook_url | string? | null | Webhook URL (Slack, Discord, or HTTP POST) |
on_fill | bool | false | Send alert on every order fill |
on_risk_breach | bool | true | Alert when risk limits are hit |
on_lifecycle | bool | true | Alert on bot startup/shutdown |
on_error | bool | true | Alert on API errors and circuit breaker |
pnl_summary_interval_secs | u64 | 0 | Periodic P&L summary interval (0 = disabled) |
[autopilot]
Autonomous trading mode. When enabled, [grid] symbol/range are ignored.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable auto-pilot mode |
scan_interval_secs | u64 | 1800 | Re-scan market every N seconds (30 min) |
switch_threshold | f64 | 15.0 | Score improvement needed to switch symbols |
max_grids | usize | 1 | Max simultaneous grids (symbols) |
[autopilot.scanner]
Scanner tuning for auto-pilot market analysis.
| Field | Type | Default | Description |
|---|---|---|---|
min_turnover | f64 | 1000000.0 | Minimum 24h turnover in quote currency |
max_spread_pct | f64 | 0.2 | Maximum bid-ask spread percentage |
quote_currencies | string[] | ["USDT"] | Quote currencies to scan |
max_results | usize | 10 | Top N candidates to analyze in depth |
kline_interval | string | "60" | Kline interval in minutes |
kline_count | usize | 200 | Number of candles to fetch per symbol |
Validation Rules
grid.levelsmust be >= 2 (unless autopilot is enabled)grid.lower_pricemust be > 0 and <grid.upper_pricegrid.qty_per_level_basemust be > 0bybit.api_keyandapi_secretmust be non-empty unlessbot.dry_run = truerisk.starting_equity_quotemust be > 0
API Endpoints
The bot runs an HTTP server on port 9090 with the following endpoints.
GET /
Returns the interactive HTML dashboard. Dark-themed, auto-refreshing, with charts and grid visualization.
Content-Type: text/html
GET /health
Simple health check.
Content-Type: text/plain
Response: ok
GET /status
Bot status as JSON. Useful for external monitoring scripts.
Content-Type: application/json
Response:
{
"orders_placed": 42,
"orders_filled": 28,
"orders_failed": 0,
"daily_pnl": 12.50,
"total_pnl": 45.30,
"equity": 10045.30,
"uptime_secs": 3600
}
GET /metrics
Prometheus exposition format. Scrape this endpoint with Prometheus for time-series monitoring.
Content-Type: text/plain
Response:
# HELP orders_placed Total orders placed
# TYPE orders_placed counter
orders_placed 42
# HELP orders_filled Total orders filled
# TYPE orders_filled counter
orders_filled 28
# HELP orders_failed Total orders failed
# TYPE orders_failed counter
orders_failed 0
# HELP daily_pnl Daily realized PnL
# TYPE daily_pnl gauge
daily_pnl 12.50
# HELP total_pnl Total realized PnL
# TYPE total_pnl gauge
total_pnl 45.30
# HELP equity Current equity
# TYPE equity gauge
equity 10045.30
# HELP uptime_seconds Bot uptime
# TYPE uptime_seconds counter
uptime_seconds 3600
GET /api/live
Full live data for the dashboard. Includes time-series history, grid state, and regime info.
Content-Type: application/json
Response:
{
"price_history": [64500.0, 64520.0, 64480.0],
"equity_history": [10000.0, 10002.5, 10005.0],
"pnl_history": [0.0, 2.5, 5.0],
"recent_fills": [
{
"time": "2026-04-23T10:15:00Z",
"symbol": "BTCUSDT",
"side": "Buy",
"price": 64500.0,
"qty": 0.001,
"pnl": 0.5
}
],
"grid_levels": [
{"price": 64000.0, "side": "Buy", "status": "open"},
{"price": 65000.0, "side": "Sell", "status": "open"}
],
"regime": "Ranging",
"regime_details": "ADX: 18.2, RSI: 48.7"
}
Integration Examples
Curl
# Health check
curl http://localhost:9090/health
# JSON status
curl -s http://localhost:9090/status | jq .
# P&L only
curl -s http://localhost:9090/status | jq '{daily: .daily_pnl, total: .total_pnl}'
Monitoring Script
#!/bin/bash
PNL=$(curl -s http://localhost:9090/status | jq .daily_pnl)
if (( $(echo "$PNL < -100" | bc -l) )); then
echo "ALERT: Daily PnL is $PNL" | mail -s "Zooni Alert" you@example.com
fi
Architecture
Source Layout
src/
├── main.rs Entry point, CLI args, --version
├── lib.rs Library crate module registry
├── bot.rs Core orchestration: startup → poll → shutdown
├── grid.rs Pure grid math (price levels, order generation)
├── autopilot.rs Autonomous scan → deploy → switch cycle
├── config.rs TOML config loader + validation
│
├── indicators.rs Technical indicators (SMA, EMA, RSI, ADX, ATR, BB, VWAP)
├── regime.rs Market regime detection + classification
├── scanner.rs Market scanner — rank symbols by suitability
├── sizing.rs Kelly criterion + regime-adjusted position sizing
│
├── risk.rs Pre-trade risk checks (exposure, loss, drawdown)
├── persistence.rs SQLite order journal + P&L storage
├── retry.rs Exponential backoff + circuit breaker
├── rate_limit.rs Token bucket rate limiter
├── market_hours.rs Trading session windows (crypto/equity/commodity)
│
├── metrics.rs Web dashboard + Prometheus metrics server
├── telegram.rs Telegram bot for remote control
├── tui.rs Terminal UI dashboard (ratatui)
├── alerts.rs Webhook notifications (Slack/Discord/HTTP)
├── ws_private.rs Private WebSocket stream handler
├── types.rs Shared types (Side, OrderStatus, etc.)
│
├── strategy/
│ ├── mod.rs Strategy trait + StrategyAction enum
│ └── dca.rs Dollar-cost averaging strategy
│
├── venues/
│ ├── bybit/
│ │ ├── mod.rs Bybit venue re-exports
│ │ ├── rest.rs REST API client (V5, spot)
│ │ ├── ws.rs WebSocket client
│ │ └── signer.rs HMAC-SHA256 request signing
│ ├── hyperliquid/ (planned)
│ ├── zerodha/ (planned)
│ ├── interactive_brokers/ (planned)
│ └── mcx/ (planned — via Indian brokers)
│
└── bin/
├── backtest.rs Backtester CLI
└── scan.rs Market scanner CLI
Data Flow
┌──────────────┐
│ Exchange │
│ (Bybit V5) │
└──────┬───────┘
│ REST API / WebSocket
┌──────┴───────┐
│ Rate Limiter │
│ + Retry │
│ + Circuit │
│ Breaker │
└──────┬───────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Scanner │ │ Grid │ │ Metrics │
│ + Regime │ │ Engine │ │ + Dashboard│
│ + Sizing │ │ │ │ │
└─────┬─────┘ └─────┬─────┘ └───────────┘
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ Autopilot │ │ Risk │
│ (optional)│────>│ Engine │
└───────────┘ └─────┬─────┘
│
┌─────┴─────┐
│ SQLite │
│ Persistence│
└───────────┘
Key Design Decisions
Single Binary
All functionality (bot, dashboard, Telegram, metrics) runs in one process. No microservices, no IPC, no message queues. This simplifies deployment and reduces failure modes.
Pure Grid Math
grid.rs is pure computation — no I/O, no async. It takes a price range and levels, returns order prices. This makes it testable and reusable across live trading, backtesting, and auto-pilot.
Atomic State
Risk engine state, circuit breaker counters, and kill switch use AtomicU64/AtomicBool. No locks needed for the hot path.
Fire-and-Forget Alerts
Webhook delivery never blocks trading. Failures are logged and ignored.
SQLite with WAL
The database uses WAL (Write-Ahead Logging) mode for concurrent reads during writes. One writer (the bot), many readers (dashboard queries, CLI tools).
Concurrency Model
Main thread (tokio runtime)
├── Bot poll loop (grid reconciliation every poll_secs)
├── Metrics HTTP server (axum, port 9090)
├── Telegram poller (every 5s)
├── Regime detector (every ~5 min)
└── Auto-pilot scanner (every scan_interval_secs)
All tasks run on the same tokio runtime. Shared state (metrics, TUI state) is accessed via Arc<Mutex<_>> or Arc<Atomic*>.
Supported Venues
Bybit (Active)
Full implementation for Bybit spot markets via the V5 API.
Capabilities
| Feature | Status |
|---|---|
| Spot limit orders | Implemented |
| Market orders | Implemented |
| Cancel orders | Implemented |
| Open order polling | Implemented |
| Wallet balance | Implemented |
| All tickers (24h) | Implemented |
| Klines (OHLCV) | Implemented |
| Server time sync | Implemented |
| HMAC-SHA256 signing | Implemented |
| WebSocket (private) | Implemented |
| Testnet support | Implemented |
Endpoints
| Environment | Base URL |
|---|---|
| Mainnet | https://api.bybit.com |
| Testnet | https://api-testnet.bybit.com |
API Version
Uses V5 unified API. All endpoints go through /v5/.
Hyperliquid (Planned)
Skeleton implementation exists. REST and WebSocket stubs are in place.
Target Features
- Perpetual futures trading
- On-chain order book
- No KYC required
Zerodha (Planned)
Skeleton implementation for Indian equities via Kite Connect v3.
Target Features
- NSE/BSE equity trading
- F&O (futures and options)
- MCX commodities (via Zerodha’s MCX membership)
- Market hours awareness for Indian sessions
Notes
- MCX (Multi Commodity Exchange) is accessed through Indian brokers like Zerodha, not as a standalone API
- Requires Zerodha Kite Connect subscription
Interactive Brokers (Planned)
Skeleton implementation for US equities via TWS/IB Gateway.
Target Features
- US stock trading
- Options
- TWS API / IB Gateway connection
Notes
- Requires Interactive Brokers account
- Connects via TWS (Trader Workstation) or IB Gateway
- Different API model from REST-based exchanges
Adding a New Venue
The venue abstraction lives in src/venues/. To add a new exchange:
- Create a new directory under
src/venues/(e.g.,src/venues/binance/) - Implement the REST client with methods matching the Bybit interface:
place_order(),cancel_order(),open_orders()wallet_balance(),server_time()all_tickers_spot(),klines()(for scanner support)
- Add request signing if required
- Register the module in
src/venues/mod.rs - Wire it into
bot.rswith a venue selection mechanism