Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Analyzes markets using technical indicators (RSI, ADX, ATR, Bollinger Bands, VWAP) to detect whether a market is ranging, trending, or volatile
  2. Picks the best symbols by scanning all available pairs and ranking them by grid-trading suitability
  3. Deploys grid orders — layered buy and sell limit orders that capture profit from price oscillations
  4. Manages risk with per-symbol exposure caps, daily/weekly loss limits, max drawdown protection, and a kill switch
  5. 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

BinaryPurpose
trading-botMain bot — runs grids, autopilot, dashboard, Telegram
backtestTest strategies against historical or synthetic price data
scanOne-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

  • Rust toolchain (edition 2024)
  • A Bybit account — testnet recommended for initial setup

Build from Source

git clone https://github.com/syedhamidali/zooni.git
cd zooni
cargo build --release

This produces three binaries in target/release/:

BinaryDescription
trading-botMain trading bot
backtestStrategy backtester
scanMarket 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

SectionPurpose
[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

VariablePurpose
RUST_LOGLog verbosity (e.g., info, debug, trading_bot=debug,reqwest=warn)
TELEGRAM_BOT_TOKENTelegram bot token for remote control
TELEGRAM_CHAT_IDTelegram 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 .gitignore blocks config.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:

  1. Set testnet = false in config.toml
  2. Replace API keys with your mainnet credentials
  3. Set starting_equity_quote to your actual balance
  4. 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:

  1. Stop placing new orders
  2. Wait for in-flight API calls to complete (configurable via shutdown_wait_secs)
  3. Cancel remaining open orders on the exchange
  4. 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

  1. The bot divides a price range into evenly spaced grid levels
  2. Buy orders are placed below the current price
  3. Sell orders are placed above the current price
  4. When a buy fills, a corresponding sell is placed one level up
  5. When a sell fills, a corresponding buy is placed one level down
  6. 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 ConditionGrid Performance
Ranging/sidewaysExcellent — frequent fills, consistent profit
Low volatility rangeGood — steady but slower
Trending upPoor — all buys fill, sells don’t
Trending downPoor — all sells fill, buys don’t
High volatilityRisky — 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:

LimitDefaultDescription
max_total_exposure80%Maximum total portfolio exposure
max_per_market40%Maximum exposure per market type
max_per_symbol20%Maximum exposure per trading pair
max_per_strategy15%Maximum exposure per strategy

All values are fractions of starting_equity_quote.

Loss Limits

Automatic pause when losses exceed thresholds:

LimitDefaultWhat Happens
daily_loss_limit5%Pauses new orders for the rest of the day
weekly_loss_limit10%Pauses new orders for the rest of the week
max_drawdown15%Pauses until equity recovers above threshold

Kill Switch

Ctrl+C triggers an atomic kill switch that immediately stops all trading. The bot will:

  1. Cancel all open orders on the exchange
  2. Wait for in-flight API calls (up to shutdown_wait_secs)
  3. 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:

  1. Kill switch — is the bot shutting down?
  2. Daily P&L — has the daily loss limit been hit?
  3. Weekly P&L — has the weekly loss limit been hit?
  4. Drawdown — is equity too far below peak?
  5. Symbol exposure — would this order exceed the per-symbol cap?
  6. 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

IndicatorFunctionUsed For
SMASimple Moving AverageTrend direction baseline
EMAExponential Moving AverageResponsive trend detection
RSIRelative Strength Index (0-100)Overbought/oversold detection
ADXAverage Directional Index (0-100)Trend strength measurement
ATRAverage True RangeVolatility measurement
Bollinger BandsPrice envelope (upper, mid, lower)Range/breakout detection
VWAPVolume-Weighted Average PriceFair value reference

Regime Detection

The regime detector classifies the market into four states based on indicator values:

RegimeConditionsGrid Suitability
RangingLow ADX (< 25), neutral RSI, tight BBBest — frequent oscillations
Trending UpHigh ADX (> 25), RSI > 60Poor — buys fill, sells don’t
Trending DownHigh ADX (> 25), RSI < 40Poor — sells fill, buys don’t
VolatileWide 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:

  1. Kelly Criterion — optimal bet size based on win rate and payoff ratio
  2. 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:

  1. Cancel existing grid orders
  2. Wait for in-flight orders to settle
  3. 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_quote and 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

FlagDefaultDescription
--configconfig.tomlConfig file path (uses [bybit] for API endpoint)
--top10Number of top results to show
--quoteUSDTQuote currency to filter by
--min-turnoverFrom configMinimum 24h turnover in quote currency
--interval60Kline 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

  1. Pre-filter: Remove pairs with low turnover or wide spreads
  2. Fetch klines: Get recent candlestick data for each candidate
  3. Regime analysis: Compute ADX, RSI, Bollinger Bands, ATR
  4. Score: Composite of regime suitability, volatility, and liquidity
  5. 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

FlagDescription
--data <path>CSV file with historical prices
--synthetic <N>Generate N bars of synthetic data instead
--optimizeSweep over levels and price ranges, rank results
--adaptiveUse 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 MiniChart canvas library for lightweight rendering
  • Responsive layout

Endpoints

PathContent-TypeDescription
GET /text/htmlInteractive dashboard
GET /healthtext/plainHealth check ("ok")
GET /statusapplication/jsonBot status snapshot
GET /metricstext/plainPrometheus exposition format
GET /api/liveapplication/jsonFull 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

KeyAction
qQuit (graceful shutdown)
Ctrl+CForce 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

  1. Message @BotFather on Telegram
  2. Send /newbot and follow the prompts
  3. Copy the bot token (e.g., 123456:ABC-DEF...)

2. Get Your Chat ID

  1. Message your new bot
  2. Visit https://api.telegram.org/bot<TOKEN>/getUpdates
  3. Find your chat.id in 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

CommandDescription
/statusCurrent bot status (mode, uptime, circuit breaker)
/pnlDaily and total P&L
/gridActive grid levels and fill count
/pausePause trading (keeps bot running)
/resumeResume trading after pause
/fillsRecent fill history

How It Works

  • The Telegram module polls getUpdates every 5 seconds
  • Only messages from the configured TELEGRAM_CHAT_ID are 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

EventConfig FlagDefaultAlert Level
Order fillon_fillfalseInfo
Risk breachon_risk_breachtrueCritical
Bot start/stopon_lifecycletrueInfo
API erroron_errortrueWarning
P&L summarypnl_summary_interval_secs0 (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

LevelPrefixUsed 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

  1. Add Prometheus as a data source pointing to http://<bot-host>:9090/metrics
  2. 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)

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:

  1. Compiles all three binaries (trading-bot, backtest, scan) in a Rust builder stage
  2. Copies only the binaries into a slim Debian runtime image
  3. Final image includes just ca-certificates for 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 /data as 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 formatting
  • cargo 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@stable for consistent Rust versions
  • Uses Swatinem/rust-cache@v2 to cache cargo builds between runs
  • Release job requires contents: write permission

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.

FieldTypeDefaultDescription
dry_runboolfalseSimulate orders without exchange calls
db_pathstring"trading-bot.db"SQLite database file path
max_retriesu323Max retry attempts per API call
circuit_breaker_thresholdu325Consecutive failures before circuit break
circuit_breaker_cooldown_secsu6460Seconds to pause after circuit break
shutdown_wait_secsu6410Seconds to wait for in-flight orders at shutdown

[bybit]

Exchange API credentials and connection settings.

FieldTypeDefaultDescription
api_keystringrequiredBybit API key (empty string OK in dry-run)
api_secretstringrequiredBybit API secret
testnetbooltrueUse testnet endpoints
recv_window_msu645000Request receive window (milliseconds)
account_typestring"UNIFIED"Account type for wallet balance lookups

[grid]

Grid trading strategy parameters. Ignored when autopilot.enabled = true.

FieldTypeDefaultDescription
symbolstringrequiredTrading pair (e.g., "BTCUSDT")
lower_pricef64requiredGrid lower price bound
upper_pricef64requiredGrid upper price bound (must be > lower)
levelsu32requiredNumber of grid lines (must be >= 2)
qty_per_level_basef64requiredBase asset quantity per grid order
poll_secsu643Seconds between fill-detection polls
cancel_existing_on_startbooltrueCancel all existing orders on symbol at startup
auto_rebalanceboolfalseShift grid when price exits the range
trailing_profit_thresholdf640.0Take-profit threshold in quote currency (0 = disabled)

[risk]

Risk management limits. All fraction values are relative to starting_equity_quote.

FieldTypeDefaultDescription
starting_equity_quotef64requiredStarting balance in quote currency (USDT)
max_total_exposuref640.80Max total portfolio exposure (80%)
max_per_marketf640.40Max exposure per market type
max_per_symbolf640.20Max exposure per trading pair
max_per_strategyf640.15Max exposure per strategy
daily_loss_limitf640.05Daily loss limit (5%)
weekly_loss_limitf640.10Weekly loss limit (10%)
max_drawdownf640.15Max drawdown from equity peak (15%)

[alerts]

Webhook notification settings. Omit the entire section to disable alerts.

FieldTypeDefaultDescription
webhook_urlstring?nullWebhook URL (Slack, Discord, or HTTP POST)
on_fillboolfalseSend alert on every order fill
on_risk_breachbooltrueAlert when risk limits are hit
on_lifecyclebooltrueAlert on bot startup/shutdown
on_errorbooltrueAlert on API errors and circuit breaker
pnl_summary_interval_secsu640Periodic P&L summary interval (0 = disabled)

[autopilot]

Autonomous trading mode. When enabled, [grid] symbol/range are ignored.

FieldTypeDefaultDescription
enabledboolfalseEnable auto-pilot mode
scan_interval_secsu641800Re-scan market every N seconds (30 min)
switch_thresholdf6415.0Score improvement needed to switch symbols
max_gridsusize1Max simultaneous grids (symbols)

[autopilot.scanner]

Scanner tuning for auto-pilot market analysis.

FieldTypeDefaultDescription
min_turnoverf641000000.0Minimum 24h turnover in quote currency
max_spread_pctf640.2Maximum bid-ask spread percentage
quote_currenciesstring[]["USDT"]Quote currencies to scan
max_resultsusize10Top N candidates to analyze in depth
kline_intervalstring"60"Kline interval in minutes
kline_countusize200Number of candles to fetch per symbol

Validation Rules

  • grid.levels must be >= 2 (unless autopilot is enabled)
  • grid.lower_price must be > 0 and < grid.upper_price
  • grid.qty_per_level_base must be > 0
  • bybit.api_key and api_secret must be non-empty unless bot.dry_run = true
  • risk.starting_equity_quote must 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

FeatureStatus
Spot limit ordersImplemented
Market ordersImplemented
Cancel ordersImplemented
Open order pollingImplemented
Wallet balanceImplemented
All tickers (24h)Implemented
Klines (OHLCV)Implemented
Server time syncImplemented
HMAC-SHA256 signingImplemented
WebSocket (private)Implemented
Testnet supportImplemented

Endpoints

EnvironmentBase URL
Mainnethttps://api.bybit.com
Testnethttps://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:

  1. Create a new directory under src/venues/ (e.g., src/venues/binance/)
  2. 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)
  3. Add request signing if required
  4. Register the module in src/venues/mod.rs
  5. Wire it into bot.rs with a venue selection mechanism