skills/spot-canvas/sn/trading-bot

trading-bot

SKILL.md

Trading Bot — Signal-Driven Architecture

Build a trading bot that consumes live signals from SignalNGN via sn signals, executes trades via ledger trades add, and enforces risk management locally.

Architecture

sn signals --json  →  Bot Process  →  ledger trades add (trade recording)
     ↑                    ↓                    ↓
  NATS stream     Risk Management Loop    ledger positions (state queries)
                  (periodic price check)

The bot is a single long-running process with two concurrent concerns:

  1. Signal loop — reads JSON lines from sn signals --json stdout, decides whether to act, executes trades
  2. Risk loop — periodically checks open positions against SL/TP/trailing stop/max hold time, closes positions that hit limits

Prerequisites: sn and ledger CLIs installed and configured. See the sn and ledger skills for setup. No other dependencies needed — sn provides signals AND live prices, ledger provides trade recording AND position queries.


Core Loop

Spawn sn signals --json as a subprocess. Read stdout line by line. Each line is a complete JSON signal.

while true:
    proc = spawn("sn signals --json")
    for line in proc.stdout:
        signal = parse_json(line)
        handle_signal(signal)
        maybe_run_risk_check()   # every N seconds between signals
    # proc exited — wait, then restart
    sleep(10)
    refresh_config()

Key points:

  • sn signals maintains the NATS connection internally and handles auth
  • Signals have a 2-minute TTL in NATS headers — stale signals are never delivered
  • If the subprocess exits (network blip, server restart), restart it with backoff
  • On restart, refresh the trading config — it may have changed server-side

Signal Payload

Each JSON line from sn signals --json:

{
  "exchange": "coinbase",
  "product": "BTC-USD",
  "granularity": "FIVE_MINUTES",
  "strategy": "ml_xgboost",
  "action": "BUY",
  "confidence": 0.82,
  "price": 98500.50,
  "stop_loss": 97200.00,
  "take_profit": 100800.00,
  "risk_reasoning": "ATR-based stop 1.3% below entry",
  "reason": "Strong bullish signal across 38 features",
  "position_pct": 0.25,
  "market": "futures",
  "leverage": 2,
  "indicators": { "rsi": 58.3, "macd_hist": 0.0042, "sma50": 96800, "sma200": 91200 },
  "timestamp": 1740218400
}
Field Use
action BUY (open long), SELL (close long), SHORT (open short), COVER (close short)
confidence 0–1. Filter by threshold (e.g. only act on ≥ 0.72)
price Candle close at signal time. Use as entry price
stop_loss / take_profit Strategy-suggested levels. Validate before using (see Risk Management)
position_pct Kelly-derived sizing (0 = no recommendation, use fixed sizing)
risk_reasoning Human-readable explanation of SL/TP rationale
market "spot" or "futures"

Trading Config

Fetch the server-side trading config to know which products and strategies are active.

sn trading list --enabled --json

Returns an array of config objects. Build a lookup map keyed by product_id:

{
  "BTC-USD": {
    "exchange": "coinbase",
    "granularity": "FIVE_MINUTES",
    "long_leverage": 2,
    "short_leverage": 2,
    "strategies_long": ["ml_xgboost"],
    "strategies_short": ["ml_xgboost"],
    "strategies_spot": []
  }
}

Product + Strategy Filtering

Critical: The signal stream contains signals for ALL tenants on the platform. The engine publishes signals from the union of all users' trading configs. Your bot must filter signals to only act on products and strategies in your own trading config. Without this filter, the bot will trade random products from other users' configs.

Two-layer filter (both required):

  1. Product filter: Reject any signal whose product is not a key in your trading config map. This is the first check — if the product isn't in your config, drop the signal immediately.

  2. Strategy filter: For products that pass the product filter, check that the signal's strategy matches one of the allowed strategies for that product.

# Layer 1: Product must be in our config
cfg = trading_config.get(signal.product)
if cfg is None:
    → reject (product not in our config)

# Layer 2: Strategy must be allowed for this product
allowed = cfg.strategies_long + cfg.strategies_short + cfg.strategies_spot
if not prefix_match(signal.strategy, allowed):
    → reject (strategy not allowed)

The engine appends direction suffixes to strategy names when publishing signals:

Config name Signal strategy field
ml_xgboost ml_xgboost (long) or ml_xgboost+trend
ml_xgboost ml_xgboost_short or ml_xgboost_short+bearish
user:bb_squeeze user:bb_squeeze or user:bb_squeeze+trend

Use prefix matching: a signal's strategy matches a config entry if it equals the entry OR starts with entry+ or entry_. This is critical — exact matching will miss most signals.

for allowed in config_strategies:
    if signal.strategy == allowed
    or signal.strategy.startswith(allowed + "+")
    or signal.strategy.startswith(allowed + "_"):
        → match

Trade Execution

Opening a Position

  1. Check for existing position: ledger positions <account> --json — skip if already have an open position for this symbol + side
  2. Calculate size: Use position_pct (Kelly) if > 0, otherwise fixed percentage of portfolio
  3. Calculate quantity: size_usd / price
  4. Validate SL/TP: See Risk Management section
  5. Record via ledger: ledger trades add <account> --symbol ... --side ... --quantity ... --price ...
  6. Save position state: Persist SL/TP/trailing stop data locally for the risk loop
# Check existing position
ledger positions paper --json | jq '.[] | select(.symbol == "BTC-USD" and .side == "long" and .status == "open")'

# Record entry trade
ledger trades add paper \
  --symbol BTC-USD --side buy --quantity 0.015 --price 98500.50 \
  --fee 0.30 --market-type futures --leverage 2 --margin 750.00 \
  --strategy ml_xgboost --confidence 0.82 \
  --entry-reason "Strong bullish signal across 38 features" \
  --stop-loss 97200 --take-profit 100800
  • side: "buy" for long entry or short exit, "sell" for short entry or long exit
  • fee: Estimate — 0.02% for futures, 0.1% for spot
  • leverage and margin: Only for futures. margin = size_usd / leverage
  • Trade ID is auto-generated. Pass --trade-id to set a specific one (idempotent)

Closing a Position

  1. Get position details: ledger portfolio <account> --json → find the position by symbol + side
  2. Use the position's avg_entry_price and quantity for P&L calculation
  3. Record the closing trade with the opposite side (sell to close long, buy to close short)
  4. Include --exit-reason (e.g. "🛑 STOP LOSS", "🎯 TARGET HIT", "🔒 TRAILING STOP")
  5. Clean up local position state
ledger trades add paper \
  --symbol BTC-USD --side sell --quantity 0.015 --price 97180.00 \
  --fee 0.30 --market-type futures \
  --exit-reason "🛑 STOP LOSS"

Position Sizing

if signal.position_pct > 0:
    base_size = portfolio_value × position_pct    # Kelly-derived
else:
    base_size = portfolio_value × fixed_pct       # e.g. 15%

size = clamp(base_size, min=150, max=2000)        # USD bounds
quantity = size / price
  • Kelly sizing (position_pct): The signal provides a fraction of capital to risk, derived from the strategy's historical win rate and payoff ratio
  • Fixed sizing: Fallback when no Kelly recommendation. 10–20% of portfolio per position is typical
  • Bounds: Enforce min/max to avoid micro-positions or overexposure

Risk Management

See references/risk-management.md for full implementation details.

Summary of the risk management stack:

Layer Trigger Default
Stop Loss Price hits SL level -3% post-leverage
Take Profit Price hits TP level +6% post-leverage
Trailing Stop Activates at +2% P&L, trails 1.5% behind peak
Max Hold Position held > N hours 72 hours

SL/TP Validation

Always validate the signal's SL/TP before using them:

sl_valid = stop_loss > 0 AND |stop_loss - price| / price > 0.001   # >0.1% from entry
tp_valid = take_profit > 0 AND |take_profit - price| / price > 0.001

if not sl_valid → use default SL (entry ± 3%/leverage)
if not tp_valid → use default TP (entry ± 6%/leverage)

This catches degenerate cases where SL/TP equal the entry price.

Risk Loop

Run every 5 minutes (or between signal processing). For each open position:

  1. Fetch current price
  2. Calculate P&L percentage (accounting for leverage and direction)
  3. Update trailing stop if P&L exceeds activation threshold
  4. Check all exit conditions (SL → trailing → TP → max hold)
  5. Close position if any condition triggers

Cooldowns

Prevent duplicate trades from rapid-fire signals on the same product:

key = f"{product}:{strategy}:{action}"
if last_signal[key] was < 5 minutes ago → skip

5 minutes is a sensible default for 5-minute candle strategies.


Daemon Management

For production, run the bot as a managed process that auto-restarts on failure.

macOS (launchd):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.trading-bot</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/python3</string>
        <string>/path/to/trading_bot.py</string>
    </array>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin:~/go/bin</string>
    </dict>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/path/to/bot.log</string>
    <key>StandardErrorPath</key>
    <string>/path/to/bot.log</string>
    <key>WorkingDirectory</key>
    <string>/path/to/workspace</string>
</dict>
</plist>

Control:

launchctl load   ~/Library/LaunchAgents/com.example.trading-bot.plist   # start
launchctl unload ~/Library/LaunchAgents/com.example.trading-bot.plist   # stop
launchctl stop   com.example.trading-bot                                 # restart (KeepAlive relaunches)

Linux (systemd): Standard user service unit with Restart=always.


Notifications

Send trade notifications to Telegram (or any messaging channel) on:

  • Position opened — include entry price, size, SL/TP, strategy, indicators
  • Position closed — include exit price, P&L (% and USD), reason
  • Risk management closes — include which condition triggered (🛑 SL, 🎯 TP, 🔒 trailing, ⏰ max hold)

Use OpenClaw's message send for notifications, or direct Telegram Bot API.


Portfolio Value Tracking

Track the live portfolio value as:

portfolio_value = starting_capital + total_realized_pnl + total_unrealized_pnl
  • starting_capital: Initial deposit (e.g. $10,000)
  • total_realized_pnl: From ledger portfolio <account>total_realized_pnl field (sum of all closed position P&L)
  • total_unrealized_pnl: Sum of (current_price - entry) / entry × leverage for each open position, multiplied by position cost basis

Display this on dashboards instead of a static starting balance. Color green when above starting capital, red when below.


State Files

The bot maintains local state files alongside the script:

File Purpose
.position_state.json SL/TP/trailing stop levels, peak P&L, open time per position
.exit_reasons.json Why each position was closed (for dashboard display)

These are the bot's local runtime state — the ledger is the source of truth for actual positions and trades. If state files are lost, the bot reconstructs defaults from ledger positions on next risk check.


Backtests

Use sn backtest to run and review historical strategy performance before going live.

Running a Backtest

sn backtest run \
  --exchange binance --product BTC-USD \
  --strategy ml_xgboost --granularity FIVE_MINUTES \
  --mode spot \
  --start 2025-01-01 --end 2025-12-31

Options: --mode spot|futures-long|futures-short, --leverage N, --trend-filter, --no-wait.

Strategy Params

Use --params key=value (repeatable) to override strategy parameters. The result always records the full effective params used — including defaults for anything not specified — so results are self-describing and comparable.

sn backtest run \
  --exchange binance --product BTC-USD \
  --strategy ml_xgboost --granularity FIVE_MINUTES \
  --params confidence=0.80 \
  --params exit_confidence=0.40 \
  --params rr_ratio=2.5 \
  --params atr_stop_mult=1.5

Available params per strategy:

Strategy Params
ml_xgboost confidence, exit_confidence, rr_ratio, atr_stop_mult
alpha_beast rsi_buy_max, rsi_sell_min, vol_multiplier, atr_stop_mult, rr_ratio
zscore_mean_reversion entry, exit, max_pos, dampening
rsi_mean_reversion oversold, overbought
bollinger_rsi rsi_oversold, rsi_overbought
macd_momentum threshold
volume_momentum multiplier
combined_rsi_macd oversold, overbought

Listing Results

sn backtest list                              # newest 20 results (default)
sn backtest list --limit 50                  # show 50 results
sn backtest list --limit 0                   # show all results
sn backtest list --sort winrate              # sort by win rate descending
sn backtest list --sort date                 # sort by date descending (default)
sn backtest list --sort winrate --limit 0    # all results, best win rate first
sn backtest list --product BTC-USD --limit 0 # filter by product, show all

--limit (default 20): number of results to return. 0 means all.
--sort (default date): date = newest first; winrate = highest win rate first.

Filters: --exchange, --product, --strategy.

Inspecting a Single Result

sn backtest get 113

Shows full metrics: total return, win rate, max drawdown, Sharpe ratio, profit factor, avg win/loss, max consecutive losses.

Interpreting Results

High win rate ≠ profitable. A strategy can show 77% win rate yet negative returns if winning trades are small and losing trades are large. Always check profit_factor (> 1.0 required) and avg_win / avg_loss ratio alongside win rate.

Key metrics to evaluate:

Metric What it measures Good threshold
total_return Overall P&L % for the period Positive
win_rate % of trades that closed in profit Context-dependent
profit_factor Gross profit / gross loss > 1.2
max_drawdown Largest peak-to-trough loss < 20%
sharpe_ratio Risk-adjusted return > 1.0
avg_win / avg_loss Win size vs loss size > 1.0

Checklist for a New Bot

  1. Install sn and ledger CLIs, run sn auth login
  2. Set up trading config: sn trading set <exchange> <product> --granularity ... --long ... --short ... --enable
  3. Reload engine: sn trading reload
  4. Implement signal loop (spawn sn signals --json, read lines)
  5. Implement strategy filter (prefix match against trading config)
  6. Implement trade execution (position check → size → ledger trades add)
  7. Implement SL/TP validation (reject degenerate values, fall back to defaults)
  8. Implement risk management loop (periodic price check → SL/TP/trailing/max hold)
  9. Implement cooldowns (prevent duplicate signals)
  10. Implement subprocess auto-restart with backoff
  11. Set up as a managed daemon (launchd/systemd)
  12. Add notifications (Telegram or other channel)
  13. Test with paper account before going live
Weekly Installs
7
Repository
spot-canvas/sn
First Seen
Feb 26, 2026
Installed on
openclaw7
gemini-cli7
claude-code7
github-copilot7
codex7
kimi-cli7