Introduction
Automated trading bot system on Binance / Hyperliquid Spot, powered by OpenClaw multi-agent.
boss (you)
│ Telegram
▼
coo ← Coordinator · Routing · Human-in-the-loop
├── finance ← Trading · AI Confirm · Execute · Risk · PnL · Trailing
├── ops ← Watchdog · Health check · Infra
├── tech ← Backend · System · Bug fixes
└── scout ← TA Scan · Signal · Market Intel · Trending · Research
The system runs two parallel loops:
Trading loop (driven by scout, every 5 minutes)
scoutscans the market → detects signalscoosends a checklist to Telegram → boss confirms YES / NO- If YES →
financeperforms a final AI analysis (EV > 25%, confidence ≥ 8) - If criteria met → places a bracket order on-chain (entry + stop-loss + take-profit simultaneously)
cooreports the result back to boss
Protection loop (driven by finance, every 1 minute)
- Moves stop-loss up following price if position is profitable ≥ trigger%
- Alerts immediately if drawdown exceeds 15%
- Sends PnL summary report at 21:00 daily
Boss is the sole decision-maker — the system never trades without confirmation.
| Agent | Role |
|---|---|
coo | Coordinator — communicates with boss via Telegram, routes tasks |
ops | Watchdog, infrastructure health check |
finance | AI signal confirmation (EV > 25%, confidence ≥ 8), order execution, trailing stop, PnL reports, drawdown alerts |
tech | Backend maintenance, system infrastructure, bug fixes |
scout | TA scan & signal generation, market intelligence — trending coins, sector rotation, on-demand coin research |
Prerequisites
- Basic knowledge of Docker + Docker Compose
- VPS with at least 2 GB RAM
- OpenClaw installed in a Docker container — OpenClaw Docker Installation Guide
- Understanding of multi-agent concepts in OpenClaw (see 5 Agents)
License
OpenTrader requires a license key to operate for tracking active users. It is free.
License server
https://otauth.skywirex.com
Add to .env:
LICENSE_SERVER_URL=https://otauth.skywirex.com
Getting a license key
Option 1 — Via Dashboard (recommended)
After docker compose up, open your browser at http://localhost:8000/api/dashboard.
If no license exists, a setup modal appears automatically:
- Enter your Email and Name
- Click “Get free license key”
- The key is activated and saved to the Docker volume immediately
Option 2 — Via API
curl -X POST http://localhost:8000/api/license/register \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "name": "Your Name"}'
Option 3 — Via environment variable (Docker / CI)
If you already have a key, set it in .env — the app activates it automatically on startup:
OPENTRADER_LICENSE_KEY=OT-XXXX-XXXX-XXXX-XXXX
Checking status
curl http://localhost:8000/api/license/status
{
"status": "active",
"plan": "free",
"features": { "max_positions": 3, "max_watchlist": 10, ... },
"expires_at": null,
"validated_at": "2026-04-15T08:00:00+00:00"
}
How it works
| Machine ID | UUID generated on first run, stored at /app/state/machine_id in the Docker volume |
| License cache | Stored at /app/state/license.json — persists across restarts and image rebuilds |
| Re-validation | App calls the license server every 24h to revalidate |
| Offline grace | If the server is unavailable, the app continues running for up to 72h from the last validation |
Delete key
Windows PS
Remove-Item "\app\state\license.json" -ErrorAction SilentlyContinue
Remove-Item "\app\state\machine_id" -ErrorAction SilentlyContinue
Docker
The cache file lives in the Docker volume at /app/state/license.json. Three ways to reset:
Option 1 — Delete cache file only (keeps machine_id)
The app will re-activate with the same key from the env var on next startup.
docker compose exec opentrader rm /app/state/license.json
Option 2 — Delete machine_id too (full reset)
Treated as a “new machine” — a new machine_id is generated, and the old key will no longer bind (a different key is needed).
docker compose exec opentrader rm /app/state/license.json /app/state/machine_id
Option 3 — Delete entire volume (nuclear option)
docker compose down
docker volume rm opentrader_state
If you only want to force re-validate with the server, Option 1 is sufficient.
Binance Testnet API Keys
Getting API Key and Secret Key from Binance Testnet.
Step 1 — Access the correct Testnet platform
Binance splits Testnet into two separate platforms for Spot and Futures:
- Spot Testnet: https://testnet.binance.vision/
- Futures Testnet: https://testnet.binancefuture.com/
Step 2 — Log in
- On the Spot Testnet page (
testnet.binance.vision), click Log in. - Unlike the real exchange, authentication is done via GitHub. Click “Log in with GitHub” and authorize the application.
Step 3 — Generate an API Key
- Once logged in, you will be redirected to the API management page.
- Click Generate HMAC_SHA256 Key (RSA and Ed25519 are also available, but HMAC_SHA256 is the most common standard and easiest to configure for general scripts).
- Enter a description for your key (e.g.
trading-bot-dev) and click Generate.
Step 4 — Store safely
- The system will immediately display your API Key and Secret Key.
- Important: You must copy and save the
Secret Keyright now (recommended: paste it into your.envfile). The platform will never show the Secret Key again after you refresh the page. If you lose it, you will need to delete it and generate a new one. - Your Testnet account is automatically funded with virtual assets (e.g. BNB, BTC, USDT, BUSD) for testing.
Step 5 — Add keys to .env
Open the .env file at the project root (create it from .env.example if it doesn’t exist yet):
cp .env.example .env
Fill in the API Key and Secret Key you just generated:
BINANCE_API_KEY=your_api_key_here
BINANCE_API_SECRET=your_secret_key_here
Then restart the container so the bot picks up the new config:
docker compose restart opentrader
Verify the bot received the keys:
docker exec opentrader env | grep BINANCE
⚠️ Never commit
.envto git. It is already listed in.gitignore— double-check if you forked the repo.
Orchestration Flows
A. Automated trading pipeline
One optional range strategy can be triggered on demand:
A1. MR Combined (on demand)
┌─────────────────────────────────────────────────────────────────┐
│ MANUAL / TOOL-TRIGGERED │
│ │ │
│ ▼ │
│ scout ──► POST /api/mr-combined/scan-and-signal │
│ │ HTF/MTF context gate │
│ │ 4H range validation │
│ │ 1H setup near support/resistance │
│ │ pending signal with TP1/TP2 + range levels │
│ │ │
│ │ signal? │
│ ├── NO → end │
│ │ │
│ ▼ YES │
│ bot ──► Telegram ──► boss 🔍 MR COMBINED SIGNAL │
│ │ YES or NO? │
│ NO ──► REJECTED → skip │
│ YES ──► finance │
│ │ AI Confirm (EV > 25%, C ≥ 8) │
│ ▼ │
│ POST /trade → bracket order │
│ finance ──► Telegram 🟢 ORDER PLACED │
└─────────────────────────────────────────────────────────────────┘
B. Trailing stop (cron every 1 minute)
CRON every 1m
│
▼
finance ──► GET /trailing
│
│ updated.count > 0?
├── NO → ANNOUNCE_SKIP
│
└── YES ──sessions_send──► coo ──► Telegram
📈 TRAILING STOP
BTC: SL $X → $Y
C. Health check (cron every 1 hour)
CRON every 1h
│
▼
ops ──► GET /status
│
│ anomaly?
├── NO → ANNOUNCE_SKIP
│
└── YES ──sessions_send──► coo ──► Telegram
⚠️ SYSTEM ALERT
D. Daily PnL report (cron 21:00)
CRON 21:00
│
▼
finance ──► GET /status
│
▼
finance ──sessions_send──► coo ──► Telegram
📊 Daily Report
Win/Loss, PnL%
E. Drawdown alert (realtime)
finance (trailing loop)
│ loss > 15% detected
▼
finance ──sessions_send──► coo ──► Telegram
🚨 DRAWDOWN ALERT > 15%
automated trading paused
F. Manual request from boss
boss ──► Telegram ──► coo
│
routing table:
┌──────────────────────────┬─────────┐
│ trade/AI confirm/execute │ finance │
│ PnL/risk/drawdown/signal │ finance │
│ TA scan/signal │ scout │
│ health/infra/watchdog │ ops │
│ backend/code/bug fix │ tech │
└──────────────────────────┴─────────┘
│
sessions_send → agent
│
result → coo → format → boss
Cron Schedule
| Job | Schedule | Agent | Description |
|---|---|---|---|
trailing_stop | every 1 minute | finance | Move SL up if profit ≥ trigger% |
health_check | every 1 hour | ops | Check exchange connectivity |
daily_pnl | 21:00 daily | finance | End-of-day PnL summary report |
Trend data is not a cron job — the 1H trend scan runs automatically inside the
opentradercontainer viaentrypoint.sh: once on startup and every hour thereafter. Scout can fetch on-demand single-TF trend viaGET /api/trend/{symbol}or read scanned snapshots viaGET /api/trend/all.
Cron jobs are configured in openclaw/cron/jobs.json (OpenClaw CronStoreFile format), not in openclaw.json or AGENTS.md. Copy to config/cron/jobs.json during setup.
Create & Configure Agents
OpenTrader uses 4 AI agents running inside OpenClaw: coo, tech, ops, finance. Each agent needs its own workspace containing files that define its behaviour and personality.
Step 1 — Create a Telegram bot
Agent coo communicates with boss via Telegram. You need to create a bot before starting the system.
1.1 Create bot with BotFather
- Open Telegram, search for @BotFather
- Send the command
/newbot - Set a display name (e.g.
OpenTrader COO) - Set a username (must end in
bot, e.g.opentrader_coo_bot) - BotFather returns a token like
123456789:AAxxxxxx...— this is yourTELEGRAM_BOT_TOKEN
1.2 Get your Chat ID
The Chat ID is your Telegram identifier — COO uses it to know who is authorised to send commands.
- Send any message to the bot you just created
- Open the following URL in your browser (replace
<TOKEN>with your token):https://api.telegram.org/bot<TOKEN>/getUpdates - Find the field
"chat":{"id":...}in the response — that is yourTELEGRAM_CHAT_ID
1.3 Add to .env
TELEGRAM_BOT_TOKEN=123456789:AAxxxxxx...
TELEGRAM_CHAT_ID=987654321
Step 2 — Configure openclaw.json
Edit openclaw/openclaw.json to wire up the Telegram channel, routing, inter-agent communication, and agent defaults.
2.1 Add Telegram coo account in channels
"channels": {
"telegram": {
"accounts": {
"default": {
...
},
"coo": {
"enabled": true,
"dmPolicy": "pairing",
"botToken": "123456789:AAxxxxxx...",
"groupPolicy": "allowlist",
"streaming": {
"mode": "partial"
}
}
}
}
}
The coo account uses the bot token from Step 1. dmPolicy: "pairing" means only paired users can DM the bot.
2.2 Bindings — route inbound chat to agent coo
"bindings": [
{
"agentId": "coo",
"match": {
"channel": "telegram",
"accountId": "coo",
"peer": {
"kind": "direct",
"id": "<your-telegram-id>"
}
}
}
]
Any direct message arriving on the coo Telegram account from your Telegram ID → routed to agent coo.
2.3 agentToAgent — allow agents to talk to each other
"tools": {
"agentToAgent": {
"enabled": true,
"allow": ["coo", "tech", "ops", "finance"]
}
}
Grants agent coo permission to dispatch tasks to tech, ops, and finance — and allows those agents to call each other as needed.
2.4 sessions.visibility — agents see each other’s runs
"tools": {
"sessions": {
"visibility": "all"
}
}
By default an agent can only see its own sessions. Setting visibility: "all" lets every agent see all running sessions in the system — required so that COO can monitor subagent progress and orchestrate correctly.
Why this matters: Without
"all", COO cannot see the result of a spawnedfinanceorscoutrun; it would have to wait blindly. With"all", COO can poll or observe the subagent session directly.
2.5 Note on nested runs and Telegram routing
OpenClaw routing is deterministic — replies always return to the channel the message came from. The model cannot override this.
When an agent uses sessions_send to deliver a message to COO, COO runs in a nested run and text output goes to channel=webchat, not Telegram. This is a fixed architectural constraint.
Solution: Use the message built-in tool instead of text output:
# Get boss chat ID from env
exec: printenv TELEGRAM_CHAT_ID → [BOSS_ID]
# Send via message tool — bypasses routing, goes directly to Telegram
message(
channel: "telegram",
target: [BOSS_ID],
message: "Content to send to boss"
)
TELEGRAM_CHAT_ID is always available in the container since OpenClaw requires this env var to connect the Telegram channel.
2.5 Agent defaults
"agents": {
"defaults": {
"model": "9router/opentrader",
"subagents": {
"maxConcurrent": 8,
"archiveAfterMinutes": 60
}
}
}
| Field | Meaning |
|---|---|
model | Default model for any agent that does not declare its own — 9router/opentrader |
subagents.maxConcurrent | Maximum 8 subagent runs allowed concurrently |
subagents.archiveAfterMinutes | Old runs are archived after 60 minutes |
Step 3 — Create workspaces for 5 agents
Each agent needs a workspace — a directory containing behaviour files (AGENTS.md, SOUL.md). OpenClaw reads these files every time an agent starts a session.
Option A — Copy from template (recommended)
The repo already includes complete templates for all 5 agents in openclaw/workspaces/. Just copy them into the mount directory:
# Create mount directory (if not already present)
mkdir -p config workspace
# Copy per-agent workspaces
cp -r openclaw/workspaces/coo config/workspace-coo
cp -r openclaw/workspaces/finance config/workspace-finance
cp -r openclaw/workspaces/ops config/workspace-ops
cp -r openclaw/workspaces/tech config/workspace-tech
cp -r openclaw/workspaces/scout config/workspace-scout
# Skill for the finance agent (AI confirm + execute)
mkdir -p config/workspace-finance/skills/sniper
cp openclaw/skills/sniper/SKILL.md \
config/workspace-finance/skills/sniper/
Result inside config/:
config/
├── openclaw.json
├── cron/
│ └── jobs.json
├── workspace-coo/
│ ├── AGENTS.md
│ └── SOUL.md
├── workspace-finance/
│ ├── AGENTS.md
│ ├── SOUL.md
│ └── skills/sniper/SKILL.md
├── workspace-ops/
│ ├── AGENTS.md
│ └── SOUL.md
└── workspace-tech/
├── AGENTS.md
└── SOUL.md
Option B — Create via OpenClaw chat
Once the container is running, you can ask the agent to create the workspace through the chat interface:
“Please create a new workspace named
workspace-techat/home/node/.openclaw/workspace-tech. Create the AGENTS.md and SOUL.md files with the following content: [paste content]”
OpenClaw will create the directory and write the files using its write tool.
Step 4 — Configure SOUL.md and AGENTS.md
This is the most important step — it determines how each agent thinks and acts.
Two file types
| File | Defines | Injected into |
|---|---|---|
SOUL.md | Personality, tone, stance — who the agent is | Main session |
AGENTS.md | Step-by-step procedures, hard rules, output format — what the agent does | Every session (including isolated cron) |
Important:
SOUL.mdis not injected into isolated cron sessions. Therefore all critical rules (timeouts, thresholds, output format) must be inAGENTS.md, notSOUL.md. See: SOUL.md vs AGENTS.md
Agent roles
| Agent | Who they are | What they do |
|---|---|---|
coo | Coordinator — the sole gateway between boss and the system | Receives commands from boss, routes to the right agent, formats results, sends trade alerts |
finance | Trading & risk manager — the final checkpoint before placing orders | AI signal confirmation (EV > 25%, confidence ≥ 8), calls /trade, trailing stop every minute, PnL reports at 21:00, drawdown alerts > 15% |
ops | Operations — infrastructure watchdog | Monitors system health, runs health-check cron, alerts COO on anomalies |
tech | Backend engineer — system maintenance | Code fixes, infrastructure, debugging, backend operations |
scout | Market scanner & intelligence analyst | Can trigger MR Combined analysis on demand, sends CUSTOM_FILTER signals to COO; also trending coins, sector rotation, on-demand research via CoinGecko API |
Upload files via OpenClaw chat
Once the container is running, you can update AGENTS.md / SOUL.md content directly via chat without accessing the server:
Example prompt:
“Here are the
AGENTS.mdandSOUL.mdfiles for the finance agent. Please replace the current content in workspace/home/node/.openclaw/workspace-finance:[AGENTS.md] (paste full AGENTS.md content)
[SOUL.md] (paste full SOUL.md content)
After writing, confirm the contents of each file.“

The agent uses its write tool to write directly into the workspace. Changes take effect in the next session — no container restart needed.
Syncing USER.md and IDENTITY.md
OpenClaw automatically creates USER.md (information about the user) and IDENTITY.md (agent self-description) in the workspace after the first session. If they need to reflect the agent’s role:
Example prompt to sync finance:
“Please update
IDENTITY.mdin workspace-finance to match this role: an agent that analyses trading signals and makes confirm/reject decisions based on EV and confidence, executes bracket orders, manages trailing stops, and reports PnL. Receives trade signals only from ops via sessions_send. Tone: cold on entry, steady on protection, honest on reports.”
This helps the agent maintain an accurate self-awareness across sessions.
Step 5 — Editing after deployment
When you need to change an agent’s behaviour:
# Edit local template
nano openclaw/workspaces/finance/AGENTS.md
# Sync into config (mount dir)
cp openclaw/workspaces/finance/AGENTS.md config/workspace-finance/AGENTS.md
Changes take effect in the next new session — OpenClaw re-reads the file every time a session starts, no container restart required.
To edit via chat, use a prompt as shown above — the agent writes directly to the workspace path.
Verification
After completing setup:
# Start the full system
docker compose up -d
# View openclaw logs to confirm agents loaded correct workspaces
docker logs openclaw -f
# Test: send a message to the Telegram bot
# → COO should reply within a few seconds
Check agent status at http://localhost:8000/api/dashboard.
OpenClaw Installation
See Create & Configure Agents to learn how to create a Telegram bot, set up workspaces, and upload SOUL.md/AGENTS.md before running the commands below.
Step 1 — Copy config into the mount directory
# Create mount directory
mkdir -p config workspace
# Main config (agents, channels)
cp openclaw/openclaw.json config/openclaw.json
# Cron jobs (5 jobs: scan, trailing, health, pnl)
mkdir -p config/cron
cp openclaw/cron/jobs.json config/cron/jobs.json
# Per-agent workspaces
cp -r openclaw/workspaces/coo config/workspace-coo
cp -r openclaw/workspaces/tech config/workspace-tech
cp -r openclaw/workspaces/ops config/workspace-ops
cp -r openclaw/workspaces/finance config/workspace-finance
cp -r openclaw/workspaces/scout config/workspace-scout
# Skill for the tech agent
mkdir -p config/workspace-tech/skills/sniper
cp openclaw/skills/sniper/SKILL.md \
config/workspace-tech/skills/sniper/
Step 2 — Fill in tokens in .env
TELEGRAM_BOT_TOKEN=<token from @BotFather>
TELEGRAM_CHAT_ID=<your chat ID>
Step 3 — Start and verify
docker compose up -d
# View openclaw logs — confirm agents loaded correct workspaces
docker logs openclaw -f
# Test: send any message to the Telegram bot
# → COO should reply within a few seconds
Docker Architecture
Three containers run on the same isolated bridge network (openclaw_9router_net):
┌──────────────────────────────────────────────────────┐
│ openclaw_9router_net │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ openclaw │ │ 9router │ │
│ │ :18789 │ │ :20128 │ │
│ └────┬─────┘ └──────────┘ │
│ │ http://opentrader:8000 │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ opentrader │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ uvicorn FastAPI :8000 │ │ │
│ │ └─────────────────────────────┘ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ trend-scan-1h (background)│ │ │
│ │ │ runs on startup + every 1h │ │ │
│ │ └─────────────────────────────┘ │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
OpenClaw agents call the bot via HTTP rather than invoking python3 directly — completely separating the Python runtime from the Node.js container.
Entrypoint
entrypoint.sh (at the repo root, mounted into the image) is the Docker CMD. It starts two processes inside opentrader:
scripts/trend_scan.sh— runs in the background. Waits 20 s for uvicorn to be ready, then runs the 1H trend scan immediately and repeats everyTREND_SCAN_INTERVAL_SECseconds (default: 3600).uvicorn app.main:app— runs in the foreground. The container stays alive as long as uvicorn is running.
Scout can always call GET /api/trend/{symbol} to get an on-demand single-TF TrendResult. Cached scan snapshots are exposed via GET /api/trend/all.
Bot API (Operations)
For day-to-day operations, use the full API reference at:
This page is intentionally short and acts as a quick pointer from the Operations section.
How Trade Execution Works
A concise end-to-end walkthrough of how OpenTrader goes from a market scan to a live bracket order on Binance.
The 5-layer pipeline
SCOUT → COO → BOSS → FINANCE → BOT API → BINANCE
Layer 1 — SCOUT scans the market
For range-specific review flows, the system can trigger POST /api/mr-combined/scan-and-signal on demand.
Layer 2 — Signal submitted to COO
Scout calls POST /api/signal with a full JSON payload (symbol, price, SL %, TP %, TA checklist). The bot stores the signal in memory and automatically sends a Telegram message to BOSS with a summary and YES / NO prompt.
A 5-minute expiry is enforced from this point. If BOSS confirms after 5 minutes the signal is rejected and no trade is placed.
Layer 3 — BOSS confirms (human-in-the-loop)
BOSS replies 1-YES ETHUSDT or 0-NO ETHUSDT on Telegram. COO receives the reply, fetches the signal from GET /api/signal/ETHUSDT, and spawns the Finance agent.
Layer 4 — FINANCE runs AI confirmation
Finance receives the signal JSON from COO, performs an AI analysis, and outputs a structured result:
{
"confirm": true,
"ev": 32.5,
"confidence": 8,
"reason": "RSI oversold + MACD cross + volume spike",
"stop_loss_pct": 3.0,
"take_profit_pct": 6.5
}
Both conditions must be met to proceed:
| Criterion | Threshold | Result if not met |
|---|---|---|
| Expected value | ev > 25 | confirm=false, trade blocked |
| Confidence | confidence >= 8 | confirm=false, trade blocked |
Finance never calls /trade before outputting this JSON. This is a hard rule — not a guideline.
Layer 5 — Order execution on Binance
Finance calls POST /trade. The bot calculates position size from portfolio %, then places 3 orders simultaneously as a bracket:
BUY ETHUSDT ← entry (MARKET)
SELL ETHUSDT ← stop-loss (STOP_LIMIT) ┐ OCO pair
SELL ETHUSDT ← take-profit (LIMIT) ┘
After a successful fill, Finance calls POST /api/signal/ETHUSDT/confirm to remove the signal from memory. Scout can then signal again for the same symbol once the position closes.
After entry
| Event | Handled by |
|---|---|
| Price moves in favor | Trailing stop raises SL automatically (every 1 min) |
| Stop-loss hit | Binance closes automatically (OCO triggers) |
| Take-profit hit | Binance closes automatically (OCO triggers) |
| Manual close (single) | POST /api/close?symbol=BTC → cancel OCO → MARKET SELL |
| Manual close (all) | POST /api/closeall → cancel OCO → MARKET SELL |
Summary
Scout detects signal → Bot notifies BOSS via Telegram → BOSS confirms → Finance AI validates (EV + confidence) → Bot places bracket order on Binance → OCO pair manages SL/TP automatically.
Bracket order in detail
Binance Spot has no native bracket order type. OpenTrader simulates one by placing 3 separate orders in sequence.
Step 1 — Entry
MARKET BUY ETHUSDT
Fills immediately at the current market price. The next two orders are placed only after this fill is confirmed.
Step 2 — OCO (One-Cancels-the-Other)
A single OCO submission places two linked SELL orders simultaneously:
LIMIT_MAKER SELL @ tp_px ← take-profit (above entry)
STOP_LOSS_LIMIT SELL @ sl_px ← stop-loss (below entry)
When either order fills, Binance automatically cancels the other. The two orders share one orderListId — they cannot exist independently.
Example
Entry: BUY ETH @ $2,450 (MARKET)
Stop-loss: SELL ETH @ $2,377 (STOP_LOSS_LIMIT, -3%)
Take-profit: SELL ETH @ $2,622 (LIMIT_MAKER, +7%)
Price rises to $2,622 → TP fills → SL is auto-cancelled.
Price drops to $2,377 → SL triggers → TP is auto-cancelled.
How SL and TP behave during fast moves
Take-profit — reliable
LIMIT_MAKER only fills at the specified price or better. If price spikes through the TP level the order fills immediately with no adverse slippage.
Stop-loss — has slippage risk
STOP_LOSS_LIMIT uses two price levels:
stopPrice = sl_px ← trigger price
price = sl_px × 0.998 ← actual limit price (−0.2% buffer)
When stopPrice is touched, Binance places a LIMIT SELL at price. That limit order then waits in the order book for a fill.
Risk: If the coin gaps down faster than the 0.2% buffer — for example on a sudden news dump — the limit order sits below the market and does not fill. The position stays open and the loss continues to grow.
| Scenario | 0.2% buffer |
|---|---|
| BTC / ETH normal volatility | Sufficient |
| Altcoin on sudden bad news | Likely insufficient (2–5% gap) |
| Market-wide flash crash | Likely insufficient |
Adjusting the buffer
The buffer is set in app/adapters/binance.py line 125:
stop_limit_px = round(sl_px * (0.998 if is_buy else 1.002), pd)
Increase to 0.995 (0.5%) or 0.99 (1%) for coins with higher volatility. The trade-off: a wider buffer guarantees a fill but sells at a slightly worse price than sl_px.
Why OCO must be cancelled before manual close
The two OCO orders lock the coin balance on Binance. Attempting a MARKET SELL while OCO orders are active will fail with APIError(-2010): Account has insufficient balance — the coins are already committed to the pending SELL orders.
POST /api/close handles this correctly: it cancels the OCO first, waits for the balance to free up, then places the MARKET SELL.
Manual trading flow (LIMIT entry + TTL)
Boss: "LONG BTC, Entry: 94500, SL: 2%, RR=1:3, TTL: 6h"
↓
COO → POST /api/trade/manual
↓
action_manual_trade():
- Tính size từ balance
- Tính sl_px = 94500 × 0.98 = 92610
- Tính tp_px = 94500 × 1.06 = 100170 (sl 2% × RR 3)
- place_limit_entry() → đặt LIMIT GTC @ 94500 → entry_oid
- Lưu vào state["pending_entries"] với expires_at = now + 6h
↓
COO alert boss: "⏳ Lệnh LIMIT đang chờ khớp @ $94,500 (TTL 6h)"
══ Mỗi 2 phút — trailing cron ══
↓
Bước 2 (mới): GET /api/pending-entries/check
↓
action_check_pending():
├─ get_order_status() → FILLED
│ └─ place_oco(sl_px, tp_px) ← OCO đặt đúng lúc, đủ coin
│ alert boss: "🟢 VÀO LỆNH (MANUAL) BTC @ $94,500"
│ → trade_record vào state["trades"]
│
├─ TTL hết → cancel_order() → alert boss: "⚠️ Hết hạn 6h, đã hủy"
│
└─ NEW / PARTIAL → giữ nguyên, poll lần sau
Key point: for manual LIMIT entry, OCO is only created after the entry is actually FILLED.
Agent nào chạy trailing cron?
Nhìn vào jobs.json và api.py, câu trả lời là cả hai, mỗi bên một tầng:
Finance Agent (cron mỗi 2 phút)
└─ curl GET /api/trailing
└─ api.py: _run("--action", "trailing")
└─ subprocess: python -m app.opentrader --action trailing
└─ action_trailing() ← logic thực sự chạy ở đây
| Tầng | Ai | Làm gì |
|---|---|---|
| Trigger | Finance agent | Gọi curl đến API, đọc response, format alert gửi boss |
| Orchestration | api.py (_run()) | Spawn subprocess, capture stdout JSON, cập nhật dashboard |
| Execution | app/opentrader.py | Tính trailing SL, gọi exchange adapter, ghi state |
Finance agent không tự tính toán trailing; nó chỉ trigger API và chuyển tiếp kết quả.
Tương tự với check_pending: Finance agent gọi curl → api.py spawn bot → action_check_pending() poll exchange và đặt OCO khi entry đã khớp.
Manual Trading Flow
Web Dashboard
Visit http://localhost:8000/api/dashboard after docker compose up.
┌─────────────────────────────────────────────────────────────────┐
│ 🤖 OpenTrader │ HYPERLIQUID │ testnet │ updated 10:30 │
├──────────────┬──────────────────────────────────────────────────┤
│ AGENTS │ RECENT ORDERS │
│ │ Symbol Dir Entry SL TP │
│ ● COO │ BTC BUY $65,000 $63k -3% $69k +6% │
│ Idle │ │
│ ├──────────────────────────────────────────────────┤
│ ● OPS │ ACTIVITY LOG │
│ Running │ 10:28 OPS Day trading scan — 5 symbols │
│ │ 10:28 OPS Custom check BTC BUY │
│ ● TECH │ 10:29 COO Waiting for boss: BTC BUY │
│ Awaiting │ 10:29 COO Boss CONFIRMED: BTC │
│ confirm │ 10:30 TECH AI confirm BTC — EV=32 conf=9 │
│ │ 10:30 TECH Order OK: BTC entry=$65,000 │
│ ● FIN │ 10:30 FIN Trailing check — 1 position │
│ Idle │ │
├──────────────┴──────────────────────────────────────────────────┤
│ Today: 2 orders │ Win/Loss: 1/1 │ Consecutive losses: 0 │
└─────────────────────────────────────────────────────────────────┘
Zones
| Zone | Content | Refresh |
|---|---|---|
| Agents (sidebar) | Realtime status of 5 agents + dot animation | 3 seconds |
| Recent orders | Table of today’s orders (entry, SL, TP, size) | 30 seconds |
| Activity log | Feed of the 100 most recent events from all agents | 3 seconds |
| Footer | Daily summary: order count, win/loss, consecutive losses | 30 seconds |
Agent status colours
- ⚪
idle— waiting for commands - 🔵
running— currently executing (blue pulse) - 🟡
waiting— awaiting boss YES/NO (yellow pulse) - 🔴
error— issue requires attention
Agents self-report their status
Each time an agent performs a task, it POSTs its status to the dashboard:
curl -s -X POST "http://opentrader:8000/api/agent/ops" \
-H "Content-Type: application/json" \
-d '{"status":"running","action":"Day trading scan — 5 symbols"}'
Local Development
For debugging the bot without rebuilding the Docker image. OpenClaw still runs in a container (port 18789 exposed to host), while the Python bot runs directly on your machine.
[host machine]
uvicorn app.main:app --port 8000
▲ │
│ curl │ curl
│ ▼
[Docker] openclaw ←→ host.docker.internal:8000
:18789 (exposed)
1. Install dependencies
pip install -r requirements.txt
2. Stop opentrader in Docker
docker compose up -d openclaw 9router
3. Fill in .env and run the bot server
cp .env.example .env
nano .env # fill in HL_PRIVATE_KEY / BINANCE_API_KEY ...
OPENTRADER_CONFIG=config/config.toml uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
4. Point openclaw to the host machine
# Mac / Windows (Docker Desktop)
sed -i 's|http://opentrader:8000|http://host.docker.internal:8000|g' \
config/openclaw.json
# Linux
HOST_IP=$(docker network inspect openclaw_9router_net \
--format '{{(index .IPAM.Config 0).Gateway}}')
sed -i "s|http://opentrader:8000|http://${HOST_IP}:8000|g" \
config/openclaw.json
docker compose restart openclaw
5. Quick test
curl http://localhost:8000/api/health
curl -X POST "http://localhost:8000/api/mr-combined/scan-and-signal?agent=tech" -H "Content-Type: application/json" -d '{"symbols":["BTC"]}'
curl "http://localhost:8000/api/status"
6. Return to full Docker
sed -i 's|http://host.docker.internal:8000|http://opentrader:8000|g' \
config/openclaw.json
docker compose up -d --build
Switch Exchange
Change one line in config/config.toml then restart the container:
[exchange]
active = "binance" # or "hyperliquid"
mode = "testnet" # or "mainnet"
docker compose restart opentrader
Logs & State
State files and logs are stored in the Docker named volume opentrader_state (persists across restarts):
# Stream logs in realtime
docker logs opentrader -f
# View state file
docker exec opentrader cat /app/state/opentrader_state.json
# Tail bot log
docker exec opentrader tail -f /app/state/opentrader.log
Strategy
Signal → Boss confirms → AI Confirm → Bracket Order
One active scan strategy remains:
MR Combined
Range-trading strategy with multi-timeframe context and explicit range management.
- Trigger on demand →
POST /api/mr-combined/scan-and-signal - Confirm higher-timeframe market context before allowing a range trade
- Validate 4H range quality, then look for a 1H setup near support/resistance
- Produce a pending signal with tier, TP1/TP2, and range levels
- Boss YES →
financeAI confirm → bracket order
Add a Custom Strategy
Scan logic is fully isolated in app/strategies/ — adding a new strategy requires no changes to app/opentrader.py.
Step 1 — Create app/strategies/my_strategy.py
import pandas as pd
from app.strategies.base import BaseStrategy
from app.config import cfg
class MyStrategy(BaseStrategy):
name = "my_strategy"
def scan(self, df: pd.DataFrame, symbol: str, bot_name: str) -> dict:
bbot = cfg.bot(bot_name)
# ... analysis logic ...
return {
"symbol" : symbol,
"bot" : bot_name,
"direction": "buy", # "buy" | "sell" | "skip"
"price" : float(df.iloc[-1]["c"]),
"checklist": [{"name": "Condition X", "pass": True, "detail": "..."}],
"passed" : 1,
"total" : 1,
"ask_coo" : True,
"sl_pct" : bbot.stop_loss_pct,
"tp_pct" : bbot.take_profit_pct,
}
Step 2 — Register in app/strategies/__init__.py
from app.strategies.my_strategy import MyStrategy
# ...
_registry = {
...
MyStrategy.name: MyStrategy, # ← add this line
}
Step 3 — Activate in config.toml
[strategy]
default = "my_strategy"
Risk Notes & FAQ
Risk Notes
- Always test on testnet for at least 48 hours before going to mainnet
- Start with 1% of portfolio per trade
- On-chain stop-loss — safe even if VPS goes down (Hyperliquid only)
- Binance SL is a server-side OCO order — requires a stable VPS
- No profit guarantee — backtest thoroughly before increasing position size
- Use
max_trading_usdtto separate trading capital from reserve funds — prevents the bot from sizing against your full balance as the account grows
Position Sizing
The bot calculates trade size as:
trading_capital = min(balance, max_trading_usdt) # if max_trading_usdt = 0 → use full balance
size = trading_capital × size_pct / price
| Parameter | Location | Description |
|---|---|---|
max_trading_usdt | [risk] | Cap on capital the bot may use (0 = no cap) |
Practical example:
[risk]
max_trading_usdt = 2000.0
[bot.mr_combined]
risk_normal = 1.0
→ Account balance is $50,000 USDT, but the bot sizes trades against $2,000 only → $100 per trade. The remaining $48,000 is never touched.
FAQ
Q: Does bot trade if boss doesn’t reply? A: No. Timeout of 5 minutes with no reply → automatically REJECTED and candidate is skipped.
Q: Can I disable human-in-the-loop?
A: Yes — set ask_coo: false in the scan logic or configure coo to auto-CONFIRM. Not recommended when starting out.
Bot API Reference
opentrader exposes HTTP API on port 8000 (internal Docker network).
- Base URL (inside
openclawcontainer):http://opentrader:8000 - Prefix for all routes:
/api
Summary
| Section | Endpoints |
|---|---|
| Health | GET /api/health |
| License | GET /api/license/status, POST /api/license/register, POST /api/license/activate |
| Dashboard | GET /api/dashboard, POST /api/agent/{name}, GET /api/state |
| News | GET /api/news |
| Portfolio | GET /api/portfolio |
| Bot Actions | POST /api/trade, POST /api/trade/manual, GET /api/pending-entries, GET /api/pending-entries/check, GET /api/pending-entries/recover, GET /api/trailing, GET /api/status, POST /api/close, POST /api/closeall, POST /api/reset-daily |
| Scan & Signal | POST /api/mr-combined/scan-and-signal |
| Trend Single-TF | POST /api/trend/{symbol}, GET /api/trend/{symbol}, GET /api/trend/all, GET /api/trend/scan |
| Market Multi-TF | GET /api/market/{symbol}, GET /api/market/scan, GET /api/market/all |
| Telegram | GET /api/notify |
| Trade Management | GET /api/trades, GET /api/trades/history, GET /api/trades/stats, POST /api/trades/sync, GET /api/trades/recover, POST /api/trades/restore |
| Signal Flow | POST /api/signal, GET /api/signal/list, GET /api/signal/pending, GET /api/signal/{symbol}, POST /api/signal/{symbol}/confirm, POST /api/signal/{symbol}/reject, POST /api/signal/cleanup |
Health
GET /api/health
Always available (no license required).
{
"ok": true,
"license_status": "active",
"plan": "free",
"commit": "a1b2c3d"
}
license_status:activeorrequiredcommit: from envGIT_COMMIT(fallbackdev)
License
GET /api/license/status
Returns current machine license state.
POST /api/license/register
Register free license and auto-activate.
Request body:
{ "email": "[email protected]", "name": "Trader Name" }
POST /api/license/activate
Activate existing key.
Request body:
{ "license_key": "OT-XXXX-XXXX-XXXX-XXXX" }
Dashboard
GET /api/dashboard
Serves dashboard HTML.
POST /api/agent/{name}
Update an agent card status (coo, ops, tech, finance, scout).
Request body:
{ "status": "running", "action": "Day trading scan" }
status:idle|running|waiting|error
GET /api/state
Returns full dashboard state (agents, log, ts).
News
GET /api/news
Fetches RSS crypto news with in-memory cache.
Portfolio
GET /api/portfolio
Returns balance/holdings from current active exchange adapter.
Bot Actions (license required)
POST /api/trade
Executes a trade via app.opentrader subprocess.
Query params:
| Param | Required | Type | Notes |
|---|---|---|---|
symbol | yes | string | e.g. ETHUSDT |
direction | yes | string | buy/sell |
bot | yes | string | Bot name |
sl | yes | float | Stop-loss % |
tp | yes | float | Take-profit % |
ev | yes | float | Must be > 25 |
confidence | yes | int | Must be >= 8 |
agent | no | string | Default ops |
Validation errors:
422ifev <= 25422ifconfidence < 8
GET /api/trailing
Updates trailing stops.
- Query:
agent(defaulttech)
GET /api/status
Returns bot status from subprocess.
- Query:
agent(optional)
POST /api/close
Cancels SL/TP OCO then closes a single position at market price.
- Query:
symbol(required) — e.g.BTC,ETH - Query:
agent(defaultops)
POST /api/closeall
Cancels all SL/TP OCO orders and closes all open positions at market price.
- Query:
agent(defaultops)
POST /api/reset-daily
Resets trades_today counter in bot state.
Response:
{ "ok": true, "trades_today_before": 2, "trades_today_after": 0 }
POST /api/trade/manual
Place LIMIT entry order at fixed price — OCO orders placed when filled.
Query params:
| Param | Required | Type | Notes |
|---|---|---|---|
symbol | yes | string | e.g. ETHUSDT |
direction | yes | string | buy or sell |
entry_px | yes | float | Fixed entry price |
sl_pct | yes | float | Stop-loss % |
rr | no | string | Risk:Reward e.g. 1:3 (use with sl_pct) |
tp_pct | no | float | Take-profit % (required if no rr) |
ttl_hours | no | float | Max wait time (default 24h) |
agent | no | string | Default coo |
Mode RR: sl_pct + rr='1:3' → tp_pct = sl_pct × 3
Mode Explicit: sl_pct + tp_pct → use directly
GET /api/pending-entries
List pending LIMIT entries waiting to be filled (from SQLite).
{ "pending": [], "count": 0 }
GET /api/pending-entries/check
Poll fill status of all pending entries — place OCO when filled, cancel when TTL expired.
- Query:
agent(defaultfinance)
GET /api/pending-entries/recover
Scan exchange for LIMIT orders with clientOrderId starting with MT_ → reconstruct and restore to SQLite. Use when switching machines or SQLite lost.
Response:
{ "ok": true, "recovered": 2, "skipped": 0, "entries": [...] }
Scan & Signal (license required)
POST /api/mr-combined/scan-and-signal
Runs the MR Combined scan and emits pending signals.
- Query:
agent(defaultscout) - Request body:
{ "symbols": ["BTC", "ETH"] }(optional; defaults to watchlist)
Shared limiter response (scan pipelines):
{ "ok": true, "signaled": [], "skipped": [], "reason": "daily_limit_reached" }
Trend
POST /api/trend/{symbol}
Stores trend snapshot for a symbol/timeframe.
Request body:
{
"trend": "uptrend",
"timeframe": "1h",
"source": "trend_scan",
"type": "crypto",
"score": { "uptrend": 5, "downtrend": 0, "sideway": 2 },
"indicators": {},
"timestamp": 1714216800
}
Validation:
trend:uptrend|downtrend|sideway|unknowntimeframe:15m|30m|1h|4h|1d|1w
GET /api/trend/{symbol}
Gets on-demand single-TF trend for a symbol by requested timeframe.
- Query:
timeframe(default1h)
Response shape matches TrendResult:
{
"symbol": "BTC",
"timeframe": "1h",
"source": "binance",
"type": "crypto",
"trend": "uptrend",
"regime": "uptrend",
"no_trade": false,
"no_trade_reason": "",
"score": {
"uptrend": 4,
"downtrend": 1,
"sideway": 1
},
"indicators": {
"price_structure": {"vote": "uptrend"},
"ema": {"vote": "uptrend"},
"adx": {"vote": "uptrend"},
"rsi": {"vote": "sideway"},
"volume": {"vote": "downtrend"},
"atr": {"vote": "sideway"}
},
"timestamp": 1714216800
}
Common errors:
422invalid timeframe500fetch or analysis error
GET /api/trend/all
Gets all cached trend rows (or filter by timeframe).
- Query:
timeframe(optional)
Each cached row includes:
symbol,timeframe,source,typetrend,scoreage_sec,stale,timestamp
GET /api/trend/scan
Triggers immediate full watchlist trend scan via subprocess.
- Query:
timeframe(default1h) - Query:
agent(optional dashboard card)
Response per symbol now includes regime and no_trade:
{
"symbol": "BTC",
"trend": "uptrend",
"regime": "trend",
"no_trade": false,
"score": { "uptrend": 5, "downtrend": 1, "sideway": 0 }
}
Errors:
422invalid timeframe504timeout (>300s)
Market Multi-TF
Multi-timeframe analysis per v3.2 framework. Fetches HTF + MTF data for each symbol, runs Priority System (7 levels), and returns a priority_decision.
HTF map per MTF timeframe:
| MTF | HTF |
|---|---|
15m | 1h |
30m | 4h |
1h | 4h |
4h | 1d |
1d | 1d |
1w | 1w |
GET /api/market/{symbol}
Returns full multi-TF MarketContext for a single symbol.
- Query:
mtf_tf(default4h) — MTF timeframe - Query:
htf_tf(default1d) — override HTF timeframe (optional)
Behavior:
- Checks the in-memory market cache first
- Falls back to fresh fetch + analysis on cache miss, stale cache, or timeframe mismatch
- Fresh result is written back into the cache
Response:
{
"symbol": "BTC",
"htf_tf": "1d",
"mtf_tf": "4h",
"htf": {
"macro_phase": "bull",
"range_position": "low",
"liquidity_profile": "clean",
"range_high": 109000.0,
"range_low": 74000.0,
"detail": {}
},
"mtf": {
"regime": "trend",
"trend_direction": "up",
"bos_mode": "confirmed",
"trend_age": "fresh",
"compression_bias": "none",
"volatile_chop": false,
"atr_val": 1250.5,
"detail": {}
},
"priority_decision": "trade_long",
"no_trade_reason": "",
"timestamp": 1714216800
}
priority_decision values:
| Value | Meaning |
|---|---|
trade_long | All layers aligned for long entry |
trade_short | All layers aligned for short entry |
standby | Market not clear — wait |
no_trade | Absolute no-trade zone (volatile_chop, compression, etc.) |
watch_regime_shift | HTF/MTF conflict — monitor without trading |
htf.macro_phase: bull | bear | range
htf.range_position: low | mid | high | just_broken_up | just_broken_down | unknown
htf.liquidity_profile: clean | equal_highs | equal_lows | void
mtf.regime: trend | compression | sideway | volatile_chop | transition | unknown
mtf.bos_mode: confirmed | quick | none
mtf.trend_age: fresh (< 5× ATR from BOS) | extended (≥ 5× ATR) | none
Errors:
422invalid timeframe404symbol not found on exchange500fetch or analysis error
GET /api/market/scan
Runs full-watchlist market scan, caches each MarketContext, and returns the scan summary.
- Query:
mtf_tf(default4h) - Query:
agent(optional dashboard card)
Response:
{
"exchange": "binance",
"htf_tf": "1d",
"mtf_tf": "4h",
"scanned": 8,
"cached": 8,
"ttl_sec": 3600,
"results": [...],
"summary": {
"trade_long": 2,
"trade_short": 1,
"standby": 3,
"no_trade": 2
}
}
GET /api/market/all
Returns all cached MarketContext rows.
- Query:
stale(defaulttrue)true: include stale rowsfalse: exclude stale rows
Response:
{
"count": 8,
"ttl_sec": 3600,
"data": [
{
"symbol": "BTC",
"htf_tf": "1d",
"mtf_tf": "4h",
"priority_decision": "trade_long",
"age_sec": 42,
"stale": false,
"timestamp": 1714216800
}
]
}
CLI equivalent:
python -m app.trend.trend_scan --timeframe 4h --mode market
Python SDK:
from app.trend import analyze_symbol
ctx = analyze_symbol("BTC", mtf_tf="4h", htf_tf="1d")
print(ctx.priority_decision) # "trade_long" | "no_trade" | ...
print(ctx.htf.macro_phase) # "bull" | "bear" | "range"
print(ctx.htf.range_position) # "low" | "mid" | "high" | ...
print(ctx.mtf.regime) # "trend" | "compression" | ...
Telegram
GET /api/notify
Sends Telegram message.
Query params:
message(required)
Common errors:
503Telegram env not configured502Telegram API error
Trade Management
GET /api/trades
Returns open trades from runtime state.
{ "trades": [], "count": 0 }
GET /api/trades/history
Returns closed-trade history from SQLite.
Query params:
limit(default50, min1, max200)offset(default0, min0)
GET /api/trades/stats
Returns aggregated trade-history stats.
POST /api/trades/sync
Syncs exchange closed trades into history storage.
GET /api/trades/recover
Recovers open trade records from exchange OCO/open orders.
POST /api/trades/restore
Manually restores a trade into runtime state.
Request body:
{
"symbol": "BNBUSDT",
"direction": "buy",
"size": 1.5,
"entry_px": 598.0,
"sl_px": 580.0,
"tp_px": 638.0,
"sl_oid": 10293001,
"tp_oid": 10293002,
"bot": "mr_combined",
"date": "2026-04-27"
}
Signal Flow
POST /api/signal
Creates/updates pending signal and sends Telegram approval request.
Request body fields:
- Required:
symbol,direction,bot - Optional:
price,sl_pct,tp_pct,checklist,passed,total - Optional indicators:
rsi,macd_hist,bb_lower,bb_upper,bb_mid,vol_ratio,buy_signals,sell_signals,adx,stoch_k - Mean-reversion extras:
tp1_pct,tp2_pct,range_support,range_resistance,range_midline
Possible status in response:
pendingtelegram_failedduplicate_skippedtrade_exists_skippedno_position_sell_skipped
GET /api/signal/list
Returns all in-memory signals.
GET /api/signal/pending
Returns text/plain formatted pending signals for COO forwarding.
GET /api/signal/{symbol}
Returns one pending signal detail for finance validation.
Errors:
404signal not found409signal not inpending410signal expired
POST /api/signal/{symbol}/confirm
Consumes signal after successful execution.
POST /api/signal/{symbol}/reject
Marks signal as skipped.
POST /api/signal/cleanup
Expires or deletes old signals.
Query params:
max_age_minutes(default5)
Response:
{
"ok": true,
"expired": ["ETHUSDT"],
"deleted": ["SOLUSDT"],
"count_expired": 1,
"count_deleted": 1
}
Error codes
| Code | Meaning |
|---|---|
400 | Bad request / invalid JSON body |
404 | Resource not found (e.g. signal) |
409 | Signal exists but not executable |
410 | Signal expired |
422 | Validation failed (timeframe / trend / AI confirm thresholds) |
500 | Internal bot/subprocess error |
502 | Telegram provider/API error |
503 | License required or Telegram not configured |
504 | Subprocess timeout (>300s) |
Quick curl
BASE=http://opentrader:8000
# Health & license
curl "$BASE/api/health"
curl "$BASE/api/license/status"
# Bot actions
curl -X POST "$BASE/api/trade?symbol=ETHUSDT&direction=buy&bot=mr_combined&sl=2.0&tp=4.0&ev=31.5&confidence=9"
curl -X POST "$BASE/api/trade/manual?symbol=ETHUSDT&direction=buy&entry_px=2450.0&sl_pct=3.0&rr=1:3&agent=coo"
curl "$BASE/api/pending-entries"
curl "$BASE/api/pending-entries/check?agent=finance"
curl "$BASE/api/pending-entries/recover"
curl "$BASE/api/trailing?agent=tech"
curl "$BASE/api/status"
curl -X POST "$BASE/api/close?symbol=BTC"
curl -X POST "$BASE/api/closeall"
curl -X POST "$BASE/api/reset-daily"
# Scan & signal
curl -X POST "$BASE/api/mr-combined/scan-and-signal?agent=scout" \
-H "Content-Type: application/json" \
-d '{"symbols":["BTC","ETH"]}'
# Trend
curl "$BASE/api/trend/BTC?timeframe=1h"
curl "$BASE/api/trend/BTC?timeframe=1w"
curl "$BASE/api/trend/all?timeframe=4h"
curl "$BASE/api/trend/scan?timeframe=1d&agent=tech"
# Market Multi-TF (multi-TF, v3.2 Priority System)
curl "$BASE/api/market/BTC?mtf_tf=4h"
curl "$BASE/api/market/scan?mtf_tf=4h&agent=tech"
curl "$BASE/api/market/all"
curl "$BASE/api/market/all?stale=false"
# Trades
curl "$BASE/api/trades"
curl "$BASE/api/trades/history?limit=20&offset=0"
curl "$BASE/api/trades/stats"
curl -X POST "$BASE/api/trades/sync"
curl "$BASE/api/trades/recover"
# Signal
curl -X POST "$BASE/api/signal" -H "Content-Type: application/json" -d '{"symbol":"ETHUSDT","direction":"buy","bot":"mr_combined","price":2450.5,"sl_pct":3.0,"tp_pct":7.0,"passed":5,"total":7}'
curl "$BASE/api/signal/list"
curl "$BASE/api/signal/pending"
curl "$BASE/api/signal/ETHUSDT"
curl -X POST "$BASE/api/signal/ETHUSDT/confirm"
curl -X POST "$BASE/api/signal/ETHUSDT/reject"
curl -X POST "$BASE/api/signal/cleanup?max_age_minutes=5"
Repo Structure
opentrader/
├── README.md
├── README.vi.md
├── .env.example
├── .dockerignore
├── Dockerfile # 2-stage build → image opentrader
├── docker-compose.yml # 3 services: openclaw + 9router + opentrader
├── requirements.txt # pip dependencies
│
├── app/ # Main Python package
│ ├── __init__.py
│ ├── main.py # FastAPI entry point (uvicorn app.main:app)
│ ├── opentrader.py # CLI: scan / trade / trailing / status
│ ├── config.py # Reads config.toml → typed config objects
│ ├── dashboard.py # Web dashboard (FastAPI router)
│ ├── license.py # License validation client
│ │
│ ├── adapters/ # Exchange adapters
│ │ ├── __init__.py
│ │ ├── base.py # ExchangeAdapter (abstract base class)
│ │ ├── binance.py # Binance Spot (OCO bracket order)
│ │ └── hyperliquid.py # Hyperliquid Perp
│ │
│ └── strategies/ # Scan strategy plugins
│ ├── __init__.py # Registry + get_strategy(name)
│ ├── base.py # BaseStrategy (abstract)
│
├── config/ # Configuration (mounted as volume on deploy)
│ └── config.toml # Central config: exchange, risk, watchlist, strategy
│
├── docs/ # mdBook documentation
│ ├── book.toml
│ ├── src/ # English docs
│ │ ├── SUMMARY.md
│ │ └── ...
│ └── vi/ # Vietnamese docs
│ └── src/
│ ├── SUMMARY.md
│ └── ...
│
└── openclaw/ # OpenClaw multi-agent configs
├── openclaw.json # Agents + channels config
├── cron/
│ └── jobs.json # Cron jobs (scan 5m, trailing 1m, health 1h, PnL 21:00)
├── workspaces/
│ ├── SOUL-vs-AGENTS.md
│ ├── coo/
│ │ ├── AGENTS.md # Routing rules, alert formats, human-in-the-loop
│ │ └── SOUL.md # Personality: coordinator, communicator
│ ├── finance/
│ │ ├── AGENTS.md # AI confirm, execute, trailing, PnL, drawdown
│ │ └── SOUL.md # Personality: precise, cold entry, honest reports
│ ├── ops/
│ │ ├── AGENTS.md # Health check, watchdog, infra alerts
│ │ └── SOUL.md # Personality: systematic, vigilant
│ ├── tech/
│ │ ├── AGENTS.md # Backend debug, system check, infra
│ │ └── SOUL.md # Personality: methodical, thorough
│ └── scout/
│ ├── AGENTS.md # TA scan, signal gen, market overview, sector, coin research
│ └── SOUL.md # Personality: data-first, precise, filter not generator
└── skills/
└── sniper/SKILL.md
OpenClaw File Structure
openclaw/
├── openclaw.json # Agents + channels config (cron jobs not included here)
├── cron/
│ └── jobs.json # 5 cron jobs — CronStoreFile format (version: 1)
│
├── workspaces/
│ ├── coo/AGENTS.md # Procedures + operating rules for agent coo
│ ├── coo/SOUL.md # Personality and tone for agent coo
│ ├── tech/AGENTS.md # Procedures + operating rules for agent tech
│ ├── tech/SOUL.md # Personality and tone for agent tech
│ ├── ops/AGENTS.md # Procedures + operating rules for agent ops
│ ├── ops/SOUL.md # Personality and tone for agent ops
│ ├── finance/AGENTS.md # Procedures + operating rules for agent finance
│ ├── finance/SOUL.md # Personality and tone for agent finance
│ └── SOUL-vs-AGENTS.md # Explanation of the difference between the two file types
│
└── skills/
└── sniper/SKILL.md # Order placement skill template (Hyperliquid & Binance)
Modifying agent behaviour: Edit
AGENTS.mdto change operating procedures, editSOUL.mdto change personality and tone. Restart the openclaw container after editing — no Python image rebuild required.
SOUL.md vs AGENTS.md
Summary from official docs: https://docs.openclaw.ai/concepts/soul
One-line distinction
SOUL.md = voice, stance, style — who the agent is AGENTS.md = operating rules — what the agent does
Loading context
| SOUL.md | AGENTS.md | |
|---|---|---|
| Main session | ✅ injected | ✅ injected |
| Sub-agent / isolated cron | ❌ not injected | ✅ injected |
This is why AGENTS.md must be self-contained for every hard rule — when ops spawns tech or a cron runs isolated, SOUL.md is not present.
What belongs where
| Belongs in SOUL.md | Belongs in AGENTS.md |
|---|---|
| Tone, voice | Step-by-step procedures |
| Stance and opinions | Confirm/reject conditions |
| Bluntness level | Specific output format |
| Humor approach | Timeout, retry logic |
| Character limits (“Never paraphrase”) | Hard limits with numbers (“sl_pct < 1.5”) |
| Overall vibe | Dashboard reporting |
Warning from the docs
“Personality is not permission to be sloppy.”
A strong SOUL.md does not mean AGENTS.md can be loose. The two files complement each other — neither replaces the other.
Quick classification test
When unsure which file a rule belongs in, ask:
- “Without this, will the agent do the wrong thing?” → AGENTS.md
- “Without this, the agent still does the right thing but doesn’t sound like itself?” → SOUL.md
Communication Channels
The system communicates with boss via Telegram.
Telegram
Configure in openclaw/openclaw.json:
TELEGRAM_BOT_TOKEN=<token from @BotFather>
TELEGRAM_CHAT_ID=<your chat ID>
Getting TELEGRAM_CHAT_ID: send a message to the bot → use https://api.telegram.org/bot<TOKEN>/getUpdates to find chat.id.
See the full setup guide at Create & Configure Agents.
FAQ
OpenClaw
Q: Does OpenClaw cost anything?
A: No — OpenClaw is self-hosted and runs on your own VPS. You only pay for the VPS and model API (9router). See Risk Notes & FAQ for cost details.
Q: Will the bot trade if boss doesn’t reply?
A: No. A 5-minute timeout with no reply → automatically REJECTED, candidate is skipped. The system never places an order without an explicit confirmation from boss.
Q: What is agent huan, is it related to OpenTrader?
A: huan is a built-in agent of the OpenClaw platform, used exclusively for long-term strategy direction — completely separate from the trading system. OpenTrader’s coo agent is an independent agent that only handles trading and communicates with boss about trades.
Q: Can I disable human-in-the-loop?
A: Yes — set ask_coo: false in the scan logic or configure coo to auto-send CONFIRMED. Not recommended when starting out; only enable once you trust the signals from your strategy.
Agents & Workspace
Q: Do workspace file changes require a container restart?
A: No. OpenClaw re-reads workspace files each time a new session is initialised. Changes to AGENTS.md or SOUL.md take effect in the next session immediately.
Q: What is the difference between SOUL.md and AGENTS.md?
A: SOUL.md defines personality and tone (who the agent is). AGENTS.md defines procedures and hard rules (what the agent does). Important: SOUL.md is not injected into isolated cron sessions — all critical rules must be in AGENTS.md. See: SOUL.md vs AGENTS.md.
Cron Jobs
Q: Where are cron jobs configured?
A: In openclaw/cron/jobs.json using OpenClaw’s CronStoreFile format ({ "version": 1, "jobs": [...] }). This file is not inside openclaw.json. Copy to config/cron/jobs.json during setup. See Cron Schedule.
Q: Why aren’t cron jobs in openclaw.json?
A: Because CronConfig in OpenClaw has no jobs field — cron jobs are managed separately via a store file or the CLI (openclaw cron add). openclaw.json only contains cron meta-settings (enabled, maxConcurrentRuns, …), not job definitions.
Binance Testnet
Q: What do I do when I run out of USDT on Binance testnet?
A: Go to testnet.binance.vision, log in with GitHub, and click “Get USDT” to receive more test funds. If the button isn’t visible, create a new API key — testnet often resets the balance alongside a new key.
Q: What percentage of balance is used per trade?
A: Position sizing depends on the active strategy configuration together with max_trading_usdt under [risk].
See Risk Notes & FAQ for the sizing formula and how trading capital is capped.
Q: I want the bot to only use a portion of my USDT for trading, keeping the rest as reserve. How?
A: Set max_trading_usdt under [risk] in config/config.toml:
[risk]
max_trading_usdt = 2000.0 # bot sizes positions based on $2,000 only
The bot uses min(balance, max_trading_usdt) as the capital base — not the full balance:
Balance: $10,000 USDT | max_trading_usdt: $2,000 | size_pct: 5%
→ Per trade: $100 (5% × $2,000, not 5% × $10,000)
A value of 0 (default) means no cap — full balance is used, same as before.
Trading Cap is shown in real time in the dashboard footer.
Communication Channels
Q: What channel does the system use to communicate with boss?
A: Telegram. Configure your bot token and chat ID in .env and openclaw.json. See Communication Channels.