go-trader — Crypto Trading Bot
A Go + Python hybrid trading system. A single Go binary (~8MB idle RAM) orchestrates 50+ strategies across spot, options, perpetual futures, and CME futures by spawning short-lived Python scripts. Both paper and live execution are supported per strategy.
Supported platforms: Binance US, Deribit, IBKR/CME, Hyperliquid, TopStep, Robinhood (crypto + stock options), OKX (spot + perps + options), Luno. Per-platform Discord/Telegram channels post hourly summaries plus immediate trade alerts. When a new release ships, a host deployment DMs the configured owner — reply yes and it pulls, rebuilds, and restarts itself. A Docker deployment updates by pulling a new image instead.
Join the Discord: https://discord.gg/46d7Fa2dXz
Getting Started
Quick flow for a new server: tell OpenClaw or Hermes:
install https://github.com/richkuo/go-trader and init.
AI Agent Setup (Recommended)
Give your AI agent SKILL.md (raw: https://raw.githubusercontent.com/richkuo/go-trader/main/SKILL.md) — it clones the repo, installs deps, walks through configuration, builds the binary, and starts the service. For non-Claude agents see AGENTS.md. Using OpenClaw or Hermes? Just say "Set up go-trader".
Interactive Setup (go-trader init)
./go-trader init
Walks asset/strategy/platform/capital/risk/Discord choices and writes scheduler/config.json. Defaults to a minimal BTC spot starter; risk prompts appear only when live trading is selected. Scripted: ./go-trader init --json '{"assets":["BTC"],"enableSpot":true,"spotStrategies":["sma_crossover"],"spotCapital":1000,"spotDrawdown":10}' --output config.json
Docker (macOS, Windows, Linux)
Run go-trader in a container with no Go, Python or uv on your computer. The guide covers install, paper-only setup, the dashboard, backups, upgrades and recovery: docs/DOCKER.md.
git clone https://github.com/richkuo/go-trader.git && cd go-trader/docker
cp env.example .env && cp go-trader.env.example go-trader.env # set GO_TRADER_TAG and STATUS_AUTH_TOKEN
docker compose run --rm cli init
docker compose up -d
On a Linux server, the systemd service below stays the main production setup.
Manual Setup
git clone https://github.com/richkuo/go-trader.git && cd go-trader
curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR=/usr/local/bin UV_NO_MODIFY_PATH=1 sh # uv for every account (SKILL.md Prerequisites)
uv sync --no-dev # Python deps from lockfile (service host; dev checkout: uv sync)
VER=$(git describe --tags --always --dirty 2>/dev/null || echo dev)
cd scheduler && go build -ldflags "-X main.Version=$VER" -o ../go-trader . && cd ..
./go-trader init # or --json '{...}', or copy config.example.json
./go-trader --config scheduler/config.json --once # smoke-test one cycle
export DISCORD_BOT_TOKEN="your-token"
sudo bash scripts/install-service.sh # systemd install + enable + start
curl -s localhost:8099/status | python3 -m json.tool
Running multiple instances
Use systemd/[email protected]. Code under /opt/go-trader-; runtime config outside the tree at /var/lib/go-trader/ (#1056) so rsync/git clean cannot clobber live config. StateDirectory=go-trader/%i keeps that path writable under ProtectSystem=strict.
sudo mkdir -p /opt/go-trader-paper-testing/scheduler /var/lib/go-trader/paper-testing
sudo cp go-trader /opt/go-trader-paper-testing/
sudo cp scheduler/config.json /var/lib/go-trader/paper-testing/config.json
sudo ln -s /var/lib/go-trader/paper-testing/config.json /opt/go-trader-paper-testing/scheduler/config.json
sudo chown -R go-trader:go-trader /opt/go-trader-paper-testing /var/lib/go-trader/paper-testing
sudo bash scripts/install-service.sh systemd/[email protected] paper-testing
Existing in-tree deploy: stop the service, then scripts/migrate-config-out-of-tree.sh --instance (refuses while daemon is live). NO_START=1 enables without starting. Detail: SKILL.md.
Hand-made unit (for example go-trader-live.service run as root from a workspace): sudo python3 scripts/migrate-service-layout.py plan --unit checks a move to this layout, and apply makes it with state transfer, proofs and automatic recovery; rollback returns it. Updates never run it. Detail: SKILL.md § Run And Install Service.
Folding paper deployments into one paper service. Paper and live never share a service: the fold refuses any target that runs a --mode=live strategy. To build a new combined paper service, stop every unit named in the run, then bash scripts/merge-paper-instance.sh --new-target (dry run) and, once it prints VERDICT: READY, the same command with --apply; it creates go-trader@ and enables nothing. --live instead names an existing paper-only service (the flag name is historical). Every state file stays where it is: the target config gains paper_db_file for --paper, every paper id gets a -paper suffix (numbered on a name clash) with storage_strategy_id keeping the stored identity, and a systemd drop-in grants each folded database directory.
--source folds a deployment into its own partition paper: instead of the shared paper one: it adds a paper_sources entry with that id and the deployment's database, aliases each strategy as and stamps paper_source=. --paper and --source may be combined and --source may repeat, so several deployments fold in one run, each keeping its own risk limits, latch and state file. --diff previews the whole plan from the config files alone, with no unit stopped. Every folded deployment's leaderboard_summaries entries are kept.
The script holds every database's locks for the run, verifies the merged config with one release's own inspect --all --json and storage-inspect --json, and never opens a database for writing. Back up every state file first, then daemon-reload, disable each folded unit, start the target unit, verify the boot [storage] lines and the first cycle, and only then retire the folded status ports. --rollback takes the same --paper and --source arguments as the apply it undoes, restores the config and drop-ins, never touches a database, and must run newest merge first. See scheduler/config.live-paper.example.json and SKILL.md § Storage Ownership, whose cutover checklist names the exact recovery boundary.
Architecture
Go scheduler (always running, ~8MB idle)
↓ each cycle, spawns short-lived Python check scripts
↓ receives JSON signals, executes paper/live trades, manages risk
↓ persists to scheduler/state.db, serves localhost:8099/status
↓ posts Discord/Telegram summaries and trade alerts
Python adapters: binanceus, deribit, ibkr, hyperliquid, topstep, robinhood, okx, luno
One deployment is one systemd instance: one config, one SQLite state file, one Go daemon. Live and paper run as separate instances today. One instance may also hold both modes, with portfolio risk partitioned by live and paper scope.
flowchart TB
subgraph Instance["go-trader instance (systemd unit)"]
CFG["config.json<br/>StateDirectory"]
DB[("state.db<br/>SQLite")]
subgraph Daemon["Go scheduler daemon"]
LOOP["Cycle loop<br/>due strategies, six-phase cycle"]
RISK["Portfolio risk per scope<br/>drawdown latch, kill switch,<br/>daily loss, notional and exposure caps"]
EXEC["Executor<br/>confirmed-fill gate, booking"]
PROT["Protection<br/>on-chain SL/TP, trailing,<br/>liquidation clamp"]
RECON["Reconciliation<br/>shared wallet, cashflow, fills"]
MIRROR["Replay mirror<br/>live decisions to paper"]
FEED["Market feed (market_feed=websocket, or shared from role=feed services over Unix sockets:<br/>websocket primary, REST backup with a request budget)<br/>one HL websocket, history repair,<br/>sealed per-evaluation snapshot"]
OPS["Operator surfaces<br/>Discord bot, loopback dashboard,<br/>owner DMs"]
end
subgraph Py["One-shot Python subprocesses (per cycle)"]
CHECK["check_<platform>.py<br/>candles, regime, open/close signals<br/>(HL: one batch per symbol+timeframe;<br/>market_feed=websocket: sealed Go snapshot on stdin)"]
XQ["check_<platform>.py execute<br/>live orders via adapter"]
REG["check_regime.py / check_price.py"]
end
end
EXCH["Exchanges<br/>Hyperliquid, Binance US, OKX, Deribit,<br/>IBKR, TopStep, Robinhood, Luno"]
DISC["Discord / Telegram"]
LOG["replay_log_path<br/>shared decision log"]
CFG --> LOOP
FEED --> LOOP
FEED -->|candles, mids| EXCH
LOOP -->|spawn, parse JSON| CHECK
LOOP --> REG
LOOP --> RISK --> EXEC
EXEC -->|live only| XQ
EXEC --> PROT
PROT -->|reduce-only orders| XQ
CHECK -->|public data| EXCH
XQ -->|signed orders, fills| EXCH
RECON --> EXCH
LOOP --> RECON
EXEC --> DB
RISK --> DB
LOOP --> MIRROR
MIRROR <--> LOG
OPS --> DISC
RISK -->|alerts, reset prompt| OPS
DB --> OPS
Paper instances run the same daemon without --mode=live: the execute subprocess is never spawned, fills are modeled, and protection is virtual.
Python provides quant libraries (pandas, numpy, scipy, CCXT); Go provides memory efficiency. Peak ~220MB for ~30s during checks, then back to ~8MB idle.
Strategies & Platforms
Strategies are auto-discovered from shared_strategies/ at go-trader init time. Common picks: spot entries include chart_pattern (the starter default), anchored_vwap, anchored_vwap_channel, anchored_vwap_reversion, liquidity_sweeps, atr_band_revert, momentum_pro, mean_reversion_pro, regime_adaptive_htf; futures/perps also include bear_pullback_st, vwap_rejection_st, delta_neutral_funding, breakout. Options use vol_mean_reversion, momentum_options, protective_puts, covered_calls (plus wheel and butterfly on Robinhood); new trades are scored vs. existing positions (strike distance, expiry spread, Greek balance). Max 4 positions per options strategy; min score 0.3 to execute. (Older strategies like sma_crossover, rsi, macd, mean_reversion, momentum, bollinger_bands, triple_ema, tema_cross etc. are flagged edge-deprecated after a fee-audit re-screen — #1275 — and hidden from discovery, but still load for existing configs/backtests.)
| Platform | Type | Assets | Live env vars | Paper data |
|---|---|---|---|---|
| Binance US | Spot | BTC, ETH, SOL | — | CCXT public |
| Deribit | Options | BTC, ETH | — | Live quotes |
| IBKR/CME | Options | BTC, ETH | IBKR creds | Black-Scholes |
| Hyperliquid | Perps | any HL-listed | HYPERLIQUID_SECRET_KEY | SDK public |
| TopStep | Futures | ES, NQ, MES, MNQ, CL, GC | TOPSTEP_API_KEY / _SECRET / _ACCOUNT_ID | yfinance |
| Robinhood | Crypto | BTC, ETH, SOL, DOGE, … | ROBINHOOD_USERNAME / _PASSWORD / _TOTP_SECRET | yfinance |
| Robinhood | Stock options | SPY, QQQ, AAPL, … | (same as above) | Black-Scholes |
| OKX | Spot + Perps + Options | BTC, ETH, SOL | OKX_API_KEY / _SECRET / _PASSPHRASE (OKX_SANDBOX=1 for demo) | CCXT public |
| Luno | Spot | BTC, ETH, … | Luno creds | CCXT public |
Hyperliquid perps direction — per-strategy direction: "long" | "short" | "both". long (default) opens longs only; short opens shorts only; both flips on reversals. Bidirectional/short-focused strategies (triple_ema_bidir, bear_pullback_st, vwap_rejection_st, chart_pattern, anchored_vwap, anchored_vwap_channel, anchored_vwap_reversion, liquidity_sweeps, momentum_pro, mean_reversion_pro, rsi_bb_combo, consolidation_range, atr_band_revert, mtf_confluence, funding_skew, regime_adaptive) require "short" or "both". Legacy allow_shorts migrates automatically. donchian_breakout is deprecated (hidden from discovery, still loadable via explicit config).
Coin sharing on Hyperliquid — multiple HL strategies (including type: "manual") can share a coin/wallet with per-strategy SQLite bookkeeping over one on-chain position. Peers must share margin_mode + leverage; reduce-only SL/TP are sized per strategy. Sub-accounts are the only path to fully independent direction/leverage/margin.
Configuration Reference
scheduler/config.json
Generate via ./go-trader init or --json. Skeleton:
{
"config_version": 20,
"interval_seconds": 3600,
"db_file": "scheduler/state.db",
"log_dir": "logs",
"auto_update": "daily",
"status_port": 8099,
"risk_free_rate": 0.04,
"default_stop_loss_atr_mult": 1.0,
"portfolio_risk": {
"max_drawdown_pct": 25,
"max_notional_usd": 0,
"warn_threshold_pct": 60
},
"regime": {
"enabled": false,
"period": 14,
"adx_threshold": 20
},
"discord": {
"enabled": true,
"token": "",
"owner_id": "",
"channels": { "spot": "CHANNEL_ID", "options": "CHANNEL_ID", "hyperliquid": "CHANNEL_ID", "topstep": "CHANNEL_ID", "robinhood": "CHANNEL_ID", "okx": "CHANNEL_ID", "luno": "CHANNEL_ID" },
"trade_alert_channels": { "hyperliquid": "TRADE_CHANNEL_ID" }
},
"platforms": {
"hyperliquid": { "risk": { "max_drawdown_pct": 50 } }
},
"strategies": [ ... ]
}
config_version migrates on startup (current 19: v19 renames the per-regime stop fields to stop_loss_atr_mult_regime / trailing_stop_atr_mult_regime, after v18's trail_stop_atr_regime rename). Configs older than 13 are rejected at load — start the pre-upgrade binary once to migrate first.
Split live, paper and paper-source state files
| Field | Description | Default |
|-------|-------------|---------|
| db_file | Primary state file. In the split layout it owns the live scope, process metadata, the live-only wallet and cash-flow tables, and shared regime history. Restart-required | scheduler/state.db |
| paper_db_file | Optional second file owning the paper scope's books, risk row, kill-switch events and correlation snapshot. Omit it and the single-file layout is unchanged. It must resolve to a different physical file than db_file — relative paths, symbolic links and hard links are all checked, and an alias exits with code 80. Restart-required | absent |
| paper_sources | Folds several paper deployments into one process. Each entry carries id ([a-z0-9][a-z0-9_-]{0,31}, never live, paper or primary), db_file, and an optional label and portfolio_risk override. Every path must resolve to a different physical file than every other state file. Restart-required | absent |
| paper_source (per strategy) | The paper_sources id this paper strategy belongs to. It selects the strategy's risk partition (paper:) and therefore its limits, latch, correlation model and state file. Refused on a live strategy and on an id no entry declares. Restart-required | absent |
| storage_strategy_id (per strategy) | The row identifier this strategy owns inside its file; defaults to id. Must be unique within one file; the same value in two different files is the supported alias. Set it to the previous id to rename a strategy with no stored rewrite and no book reset. Restart-required | id |
Every file is locked before any migration or startup write, so a second scheduler — --once included — refuses to run. ./go-trader storage-inspect prints a read-only ownership report for every file and is the discovery command for backups. Back up and restore all of them together — db_file, paper_db_file and each paper_sources[].db_file, with their -wal and -shm sidecars — while the service is stopped, restoring in the order primary, paper, then sources by id. scripts/update.sh excludes the same list from its rsync.
Portfolio Risk
| Field | Description | Default |
|-------|-------------|---------|
| portfolio_risk.max_drawdown_pct | Kill switch — halt trading in that mode when its portfolio drops this % from peak. Live and paper strategies keep separate peaks, latches and ledgers, so one mode can never halt the other | 25 |
| portfolio_risk.max_notional_usd | Cap on total gross notional — holds new opens when exceeded; closes/SL keep running (0 = disabled) | 0 |
| portfolio_risk.warn_threshold_pct | Warning when drawdown reaches this % of max_drawdown_pct | 60 |
| portfolio_risk.daily_max_loss_usd / daily_max_loss_pct | Hard daily loss limit — holds new entries (not closes) until UTC rollover; both may be set, lower resolved USD wins (0 = disabled) | 0 |
| portfolio_risk.max_same_direction_notional_usd / max_asset_concentration_pct | Blocks new same-direction/single-asset opens once the cap would be exceeded (0 = disabled) | 0 |
| portfolio_risk.paper | Optional override block with the same fields, applied to paper strategies only. Omitted or zero fields inherit the parent; paper.max_notional_usd is restart-required | absent |
| risk_free_rate | Annualized rate for Sharpe calculations | 0.04 |
| status_port | HTTP status port (+5 fallback on collision); override with --status-port | 8099 |
| default_stop_loss_atr_mult | Fleet-wide HL perps fallback when all five stop_loss_ / trailing_stop_ fields omitted; 0 opts out | 1.0 |
Regime Detection
Optional ADX+DI 3-state gate (trending_up / trending_down / ranging) from the strategy's OHLCV. allowed_regimes blocks new entries when the current regime isn't whitelisted (closes always pass). regime.enabled and regime.windows require restart; allowed_regimes and per-strategy window selectors are SIGHUP-reloadable when flat. Options strategies don't emit a regime label yet.
{
"regime": { "enabled": true, "period": 14, "adx_threshold": 20 },
"strategies": [{ "id": "hl-momentum-btc", "allowed_regimes": ["trending_up", "trending_down"] }]
}
regime.period defaults to 14; regime.adx_threshold to 20 (below → ranging).
Multi-window regime (#792). Optional regime.windows runs independent ADX classifiers per named horizon (value = ADX period in bars). Empty windows → legacy single-window from regime.period. Three per-strategy selectors (empty/default → regime.period):
| Selector | Consumer |
|---|---|
| regime_gate_window | Entry gate (allowed_regimes) |
| regime_atr_window | Regime-aware SL/TP multipliers (stamped at open) |
| regime_directional_window | regime_directional_policy resolver |
OHLCV fetch scales to the longest window. go-trader inspect shows resolved selectors and stamped windows on open positions.
Regime-aware ATR multipliers (HL perps). With regime.enabled, swap scalar stop/TP fields for *_regime siblings (stop_loss_atr_mult_regime, trailing_stop_atr_mult_regime, tiered_tp_atr_regime, tiered_tp_atr_live_regime). {"use_defaults": true} expands a baseline table; explicit form requires all three ADX labels. Regime is frozen at open for stops; live TP regime refs re-resolve each tick. (Renamed #1475; pre-v19 spellings stop_loss_atr_regime/trail_stop_atr_regime migrate on load.)
Correlation Tracking
Opt-in via correlation.enabled: true. Warns when a single asset exceeds max_concentration_pct (default 60) of gross exposure or max_same_direction_pct (default 75) of strategies on an asset share a direction.
Auto-Update & DM Upgrades
auto_update: "off" (default), "daily", or "heartbeat". When an update is found, channels are notified; with discord.owner_id set, reply yes to a DM to run scripts/update.sh and restart. Post-upgrade, new config fields may be collected via DM (10-minute window per field). Discord user ID: right-click username → Copy User ID (Developer Mode: Settings → Advanced).
Discord Settings
| Field | Description |
|---|---|
| discord.enabled | Toggle Discord notifications |
| discord.token | Leave blank — set DISCORD_BOT_TOKEN env var |
| discord.owner_id | Owner DM for upgrades + config migration (DISCORD_OWNER_ID) |
| discord.channels | Map keyed by spot / options / / |
| discord.trade_alert_channels | Optional per-type trade-alert routing; SIGHUP-reloadable |
| telegram.trade_alert_channels | Same override for Telegram |
Summary Frequency
Top-level summary_frequency map keyed by channel name. Trades always post immediately.
{ "summary_frequency": { "spot": "hourly", "hyperliquid": "every", "topstep": "30m" } }
Values: every / per_check / always, hourly, daily, Go durations (30m, 2h), or "" (legacy defaults). Wall-clock based, persisted in SQLite.
Strategy Entry
| Field | Description | Default |
|---|---|---|
| id | Unique identifier (e.g. hl-momentum-btc) | required |
| type | spot / options / perps / futures / manual | required |
| platform | binanceus / deribit / ibkr / hyperliquid / topstep / robinhood / okx / luno | required |
| script, args | Python entry-point + argv (auto-filled for manual) | required |
| capital | Virtual starting capital in USD. May be omitted only when every member of one supported 2+ live perps wallet uses shared-wallet pool budgeting with margin_per_trade_usd | 1000 |
| max_drawdown_pct | Per-strategy CB; peak-relative (spot/options/futures), margin-relative (perps) | spot 5, options 10, perps 5 |
| circuit_breaker | Set false to disable both CB arms; latched CB still drains | enabled |
| llm_entry_analysis | {enabled, model, max_debate_rounds, timeout_s, notify_dm, notify_channel} — post-open LLM multi-agent entry commentary (advisory only; never touches the trade). Digest defaults to DM (notify_dm on); the shared channel is opt-in (notify_channel off) | disabled |
| interval_seconds | Check interval (0 → global) | 0 |
| htf_filter | Higher-timeframe trend filter | false |
| closed_bar_decisions | Binance.US spot, OKX spot/perps, HL perps — signal, entry ATR and entry sizing use the last closed bar (as the backtester does); protection keeps current prices; restart-required | false |
| resting_tp_trade_through | HL perps paper only — a tier take-profit books only after a completed bar since entry traded one tick past the venue-rounded limit (the backtester models the same rule with --resting-tp-trade-through); live is unchanged; JSON true/false only; restart-required | false |
| open_strategy | Co-located ref {name, params} overriding entry; falls back to args[0] | null |
| close_strategy | Single {name, params} close evaluator ref | null |
| leverage | Perps — exchange leverage (also sizing if sizing_leverage omitted) | 1 |
| sizing_leverage | Perps — order sizing multiplier | leverage |
| margin_per_trade_usd | Live HL/OKX perps — per-open margin cap. In shared-wallet pool mode, notional = min(cap, account equity − deployed wallet margin) × leverage, with each position reserved at the larger of entry-price or mark-price margin | omitted |
| stop_loss_pct / stop_loss_margin_pct / stop_loss_atr_mult / trailing_stop_pct / trailing_stop_atr_mult | HL perps — at most one positive value; all omitted → default_stop_loss_atr_mult × entry_atr; 0 opts out | omitted |
| trailing_stop_min_move_pct | HL trailing stop debounce (OID cap 1000) | 0.5 |
| margin_mode | HL perps — isolated / cross; from flat only | isolated |
| direction | Perps — long / short / both | long |
| allowed_regimes | Whitelist for new entries; requires regime.enabled | (no gate) |
| regime_gate_window / regime_atr_window / regime_directional_window | Multi-window selectors | legacy |
| theta_harvest | Early-exit config for sold options | null |
Shared-wallet pool budgeting is enabled structurally: configure at least two live Hyperliquid or OKX perps strategies on the same process account, omit capital, capital_pct, and initial_capital from every member, and set a positive margin_per_trade_usd on every member. Mixed pooled/allocated members are rejected. Missing account balance data blocks opens/adds/flips but never blocks closes. Portfolio risk may reuse the immediately preceding real pooled balance for one failed risk evaluation; without that snapshot it suppresses only equity drawdown while perps-margin protection stays active. Flip release uses the position's stored leverage so it exactly cancels reservation after config changes. Operator TOTAL counts a freshly fetched wallet balance even when per-member ledger attribution fails. Switching back to allocated capital requires a restart and reseeds the virtual cash book once while preserving pool-era gains/losses. If a capital_pct balance cannot resolve during that restart, the strategy stays manage-only so exits and protection continue, and a later restart retries the transition. Restart never auto-clears a pooled wallet's portfolio kill switch.
Custom Strategy Parameters
Per-strategy params merges under built-in defaults (config wins; runtime data wins over config).
{ "id": "ts-st-es", "type": "futures", "platform": "topstep",
"script": "shared_scripts/check_topstep.py",
"args": ["supertrend", "ES", "5m", "--mode=paper"],
"params": {"multiplier": 2.0, "atr_period": 10} }
Theta Harvesting (Options)
profit_target_pct (% premium captured), stop_loss_pct (% premium lost), min_dte_close (force-close inside N days).
{ "theta_harvest": { "enabled": true, "profit_target_pct": 60, "stop_loss_pct": 200, "min_dte_close": 3 } }
Dashboard partition selector. The dashboard toolbar shows a partition selector whenever the process owns two or more partitions, which a live plus default-paper deployment already meets, folded sources or not. The selector lists live, the default paper partition and each folded source. The selection is carried on every panel read as ?partition=live|paper|paper:, so the strategy list, overview, leaderboard, diagnostics, dead-strategy count, portfolio risk and correlation all show one partition at a time. Diagnostics pages and totals are filtered in the owning state file, so the count matches the rows you can page through. Cash flow stays live-owned and reports itself unavailable for a paper partition; the close-evaluator catalogue is shared by every partition. The selector stays hidden when the process owns one partition.
Manual Trading on Hyperliquid
Hand-placed positions (or TradingView alerts) tracked for P&L, stops/TPs, and Discord summaries — declare type: "manual" and use:
./go-trader manual-open hl-manual-btc # defaults: --side long --margin 50
./go-trader manual-open hl-manual-btc --side long --notional 500 --atr 250
./go-trader manual-open hl-manual-btc --side short --size 0.05 --record-only --fill-price 64500
./go-trader manual-open hl-manual-btc --limit-price 68000 --side long --margin 50
./go-trader manual-open hl-manual-btc --limit-price 68000 --tif Gtc --expire-after 4h
./go-trader manual-cancel <limit-order-id>
./go-trader manual-clear-limit-row <order-oid> --flattened # discard an off-book row you closed by hand
./go-trader manual-update-sl hl-manual-btc --trigger 66000
./go-trader manual-cancel-sl hl-manual-btc
./go-trader manual-close hl-manual-btc [--qty 0.025]
./go-trader force-close hl-tcross-eth-live [--qty 0.025] # live HL perps strategy close
Sizing: mutually exclusive --size / --notional / --margin (default --margin 50 when omitted). --side defaults to long. Omitting --atr auto-fetches ATR(14); leverage-aware fallback if fetch fails. SL + tiered TPs placed inline so the position is never naked.
Close defaults (#1115/#1135): with regime.enabled and a resolvable per-regime trail, manual defaults to trailing_tp_ratchet_regime (regime trail owns the SL); otherwise tiered_tp_atr_live + scalar 2.0×ATR SL (#1121). Override via close_strategy, stop fields, or user_defaults.manual (hot-reloadable via SIGHUP). Fleet close ladders live under user_defaults.close; standalone *_atr_mult_regime defaults live under user_defaults.regime_atr.
Operator guardrails, refusals, and the queueing model: SKILL.md § Manual Trading.
manual-update-sl / manual-cancel-sl queue daemon-side cancel-then-place edits — rejected when automated ATR/regime/trailing protection would re-pin next cycle. force-close is for live Hyperliquid type=perps strategy positions; it submits the reduce-only close and queues the fill for the scheduler to adopt into state/trades. --dry-run previews without exchange calls. Limit opens are post-only (ALO) by default or GTC with --tif Gtc; scheduler polls fills each cycle.
Backfilling Hyperliquid Fees
./go-trader backfill hl-fees --strategy hl-btc-momentum # dry-run
./go-trader backfill hl-fees --all --apply # apply (stop daemon first)
./go-trader backfill trade-ledger --all --apply # shared-wallet gross-PnL migration
Full backfill procedure, skip reasons, and the cash-replay gate: SKILL.md.
--apply refuses while another go-trader process holds the same DB. Trade-ledger backfill is idempotent — run once after adopting the gross-PnL convention.
Trade Diagnostics
./go-trader diagnostics # all strategies
./go-trader diagnostics --strategy hl-btc-momentum
Per-trade quality report over closed positions: MFE/MAE/capture ratio, win rate and NET PnL split by regime-at-open and direction, with sample-size-gated findings and the exact backtest command to validate each one. Read-only against the state DB; never blocks or alters a close.
Build & Deploy
Canonical path: scripts/update.sh — git pull --ff-only → uv sync --no-dev → version-stamped go build → atomic binary swap → optional restart with /health verify and rollback on failure. Startup probe refuses Go/Python version mismatch — prefer the script over hand-rolled rebuilds.
sudo bash scripts/update.sh --restart # systemd (default)
bash scripts/update.sh --restart --restart-mode signal # bare-process (pidfile + run.sh)
bash scripts/update.sh --rsync-from /path/to/staged-build --restart
bash scripts/update.sh --all --restart # batch all instances
When your own account owns the deployment tree, run bash scripts/update.sh --restart without sudo; the script calls sudo itself only for the systemd steps. As root, the update refuses a tree whose owning account runs any unit without the [email protected] sandbox (SKILL.md § Auto-Update, "Root on a tree another account owns").
Optional: sudo bash scripts/shared-feed-convert.sh plan --consumer starts a checked, reversible conversion to the shared market feed. Updates never run it. See SKILL.md § Shared market feed.
| Change | Action |
|--------|--------|
| Go or Python source | sudo bash scripts/update.sh --restart |
| Config (hot-reloadable subset) | systemctl kill -s HUP go-trader |
| Config (roster, script/args/type/platform, regime block) | systemctl restart go-trader |
| Service file | systemctl daemon-reload && systemctl restart go-trader |
Restart modes, batch discovery, and graceful drain: SKILL.md.
Monitoring
systemctl status go-trader
curl -s localhost:8099/status # live prices + P&L
curl -s localhost:8099/health
open http://localhost:8099/dashboard # charts, trades, equity, regime badge, tuner, reports
open http://localhost:8099/tuning # research-run tuning page (suggest-only)
journalctl --namespace=+go-trader -u go-trader -n 50
./go-trader inspect <strategy-id> # resolved config + SL/TP provenance
./go-trader inspect --all --json
./go-trader agent-info # capabilities, schema, env vars, live state
Logs live in their own journal namespace. Both shipped units set LogNamespace=go-trader, so every go-trader unit logs to a separate journal with its own size cap, and none of its lines go to /var/log/syslog. A plain journalctl -u go-trader shows only systemd's start and stop lines. Add --namespace=go-trader for the go-trader output alone, or --namespace=+go-trader to merge it with the default journal (systemd's own lines for the unit and anything logged before the move). scripts/install-service.sh and scripts/update.sh --restart install systemd/[email protected] as /etc/systemd/[email protected]: SystemMaxUse=2G, ForwardToSyslog=no, persistent storage. That file is managed by go-trader and replaced on a difference (the old copy is kept as .prev). Put local settings in /etc/systemd/[email protected]/*.conf and apply them with sudo systemctl restart [email protected]. Journal namespaces need systemd 245 or newer; on an older systemd the installers warn, skip the config, and systemd ignores the unit line, so logs stay in the default journal.
Loopback-only status server (localhost:). Dashboard includes candle charts, trade history, equity sparklines, strategy tuner, and /reports. A separate /tuning page launches persistent research retunes across one or more strategies and diffs the ranked results against live config — suggestions are never auto-applied. Set status_token for mutating API calls from the browser. Prefer VPN or reverse proxy over binding 0.0.0.0.
Tailscale Serve — publish HTTPS on the tailnet while go-trader stays on loopback:
tailscale serve --bg --https=8443 http://127.0.0.1:8099 # live (8099)
tailscale serve --bg --https=8444 http://127.0.0.1:8100 # paper instance (8100)
Open https://. status_token still applies.
inspect is read-only against live deploys — shows which stop field won, resolved TP tiers, and direction provenance. Discord summaries: header shows aggregate initial capital; table columns Value | PnL | PnL% | DD | Wallet% | Tf | Int | #T | W/L plus Book Sharpe footer.
Risk Management
- Portfolio kill switch — halts at
portfolio_risk.max_drawdown_pct(default 25); submits real closes on HL / OKX perps / Robinhood crypto / TopStep. Owner-DM reset confirmation wait is tunable viakill_switch_reset_dm_timeout(Go duration string, e.g."6h"; default 6h). - Per-strategy circuit breakers — max-drawdown (24h cooldown) or consecutive losses (default 5, 1h cooldown); threshold and both cooldowns tunable per strategy via
cb_drawdown_cooldown_minutes/cb_loss_streak_threshold/cb_loss_streak_cooldown_minutes. HL/OKX perps, Robinhood crypto, TopStep auto-close; OKX spot and Robinhood options need manual flatten. Latched HL perps CB still permits trailing-SL management.circuit_breaker: falsedisables firing. - Hyperliquid stop-loss — one positive field among seven mutually-exclusive stop owners (
stop_loss_pct,stop_loss_margin_pct,stop_loss_atr_mult,stop_loss_atr_mult_regime,trailing_stop_pct,trailing_stop_atr_mult,trailing_stop_atr_mult_regime); all omitted →default_stop_loss_atr_mult × entry_atr(1.0);0opts out. A stop past the Hyperliquid liquidation price is clamped, never left unreachable. - On-chain N-tier TP/SL —
tiered_tp_atr/tiered_tp_atr_live(default tiers[{1.5×, 0.4}, {3×, 0.8}, {5×, 1.0}]). - Trailing-ratchet close —
trailing_tp_ratchet/trailing_tp_ratchet_regime: cleared tiers tighten a single trailing stop; no fixed on-chain TPs. HL perps +manual. - AVWAP stop close —
avwap_stop: exits when price breaches the anchored VWAP bybuffer_atr_mult× ATR on the losing side; virtual exit only (no on-chain trigger). - Regime gate, HL margin mode (
isolateddefault), correlation warnings (opt-in), options position limits, theta harvesting.
TradingView Export
./go-trader export tradingview --strategy hl-btc-momentum --output tv-hl-btc.csv
./go-trader export tradingview --all --output tv-all.csv
Built-in mappings cover known OKX/BinanceUS pairs; add tradingview_export.symbol_overrides for the rest. Export procedure: SKILL.md.
Booked-ledger export (read-only)
For reconciliation, capture a consistent copy of every state file first, then export one Hyperliquid strategy from that copy:
./go-trader export capture --config /var/lib/go-trader/config.json --output-dir /var/tmp/ledger-snap
./go-trader export ledger --manifest /var/tmp/ledger-snap/capture.json --partition live --strategy hl-btc-momentum --output hl-btc.ledger.json
Capture runs on Linux only: SQLite reads each file through VACUUM INTO from a private mount namespace in which the state directories are read-only, so the running scheduler's files, WAL and shared memory are never written. It needs root with CAP_SYS_ADMIN or unprivileged user namespaces, and refuses a WAL-mode file that no running scheduler holds open. Each file is captured in its own transaction. The export needs no trading secrets, verifies every hash and the exact file inventory before reading, and writes a new JSON file (schema go-trader.booked-ledger, version 1) that keeps raw fees, gross flags, position and order ids, both tradeNetPnL and tradeLedgerDelta, and marks missing history as unavailable instead of guessing. Funding with no position stays unallocated; orphan wallet funding is listed separately. Supported owners: Hyperliquid perps and Hyperliquid manual strategies. Details and refusals: SKILL.md § Booked-Ledger Export.
Trading Fees
| Market | Fee | Slippage | |--------|-----|----------| | Binance US Spot | 0.1% taker | 0.05% against the trade (paper and backtest) | | Deribit Options | 0.03% of premium | — | | IBKR/CME Options | $0.25/contract | — | | Hyperliquid Perps | 0.045% taker (also on resting take-profit tier fills); 0.015% maker constant, unverified and not applied to tier fills | 0.05% against the trade (paper and backtest) | | TopStep Futures | Per-contract (configurable) | 0.05% against the trade (paper and backtest) | | Robinhood Crypto | No commission (spread embedded) | 0.05% against the trade (paper and backtest) | | Robinhood Options | $0.03/contract (regulatory fee) | — |
Live fills record exchange-reported fees and order IDs. The fee rates are the code constants. An operator check of a 2026-10-06 live capture (issue 1726) reports every booked fee group at or below the 0.045% taker constant and could not classify take-profit tier fills as maker, so paper and the backtester without a maker rate charge taker on tier fills. The live ledger comparison is the one exception: it charges the manifest maker_fee_pct on simulated tier fills. The capture is private, so the repository cannot verify this check; the exact account rate and the maker rate stay unverified.
Layout & Dependencies
scheduler/ (Go) · shared_scripts/ · platforms/ · shared_tools/, shared_strategies/ · backtest/ · systemd/, scripts/ · SKILL.md, AGENTS.md.
Python 3.12+ via uv; Go 1.26.2; systemd.
Troubleshooting
| Problem | Solution |
|---|---|
| No Discord messages | Check DISCORD_BOT_TOKEN, channel IDs, bot permissions |
| Service won't start | journalctl --namespace=+go-trader -u go-trader -n 50 (the + merges systemd's start and exit lines with the go-trader output) |
| Need the per-check detail (script argv, HOLD signals, prices) | Set "log_level": "debug" and sudo systemctl kill -s HUP go-trader; set it back to "info" after. See SKILL.md § Adjustable Settings |
| Didn't come back after reboot | Re-run sudo bash scripts/install-service.sh |
| Strategy not trading | Circuit breaker in /status, verify params |
| Reset positions | rm scheduler/state.db && systemctl restart go-trader (remove every configured state file: db_file, paper_db_file and each paper_sources[].db_file) |
| Inspect state-file ownership | ./go-trader storage-inspect --config — read-only; add --require-idle to reject while the daemon owns a file |
| Live mode fails | Set env vars from Platforms table |
| "state DB missing but live strategies configured" | Restore scheduler/state.db from backup, or GO_TRADER_ALLOW_MISSING_STATE=1 for first-run. With paper_db_file or paper_sources set, restore every file together with its -wal / -shm sidecars, in the order primary, paper, then sources by id |
| Which files does a backup need? | ./go-trader storage-inspect --json --config names the canonical path and the partitions of every state file; update_resolve_db_exclude in scripts/update_helpers.sh enumerates the same list for the updater |
| A unit exits 79 after a fold | Two processes cannot own one state file. Whichever scheduler starts second fails to take the ownership lock and refuses to start with exit 79; the process already holding the lock keeps trading, so read journalctl --namespace=+go-trader -u for the unit that exited and leave the running one alone. Usually a folded paper unit was restarted or came back after a reboot: disable every folded unit (systemctl disable go-trader@) and start only the merged live unit |
| Exit code 80 on startup | The storage layout was rejected (aliased files, a book in the wrong file, an ambiguous legacy risk row). Run ./go-trader storage-inspect — it names the file and the identifier |
Risk Disclaimer
This software is provided for informational and educational purposes only and does not constitute financial advice. Trading involves substantial risk of loss; past performance is not indicative of future results. The authors make no guarantees regarding accuracy, profitability, or outcomes, and accept no liability for any losses incurred. You are solely responsible for your investment decisions — only trade with funds you can afford to lose.
This is not financial advice. Trade at your own risk.