English | 한국어
An automated crypto trading system where LangGraph multi-agents read market data, technical indicators, and news to decide LONG / SHORT / CLOSE_POSITION / HOLD. A risk engine checks every decision before it runs, and several positions can be open at the same time.
Runs in paper trading mode by default. Live orders need both
TRADING_MODE=liveandLIVE_TRADING_ENABLED=true. This is not investment advice.
- Multi-agent decisions: Technical analyst and sentiment analyst run in parallel, a Bull vs Bear debate follows, and a fund manager makes the final call.
- Risk engine in code: Leverage limit, drawdown kill switch, re-entry cooldown, confidence threshold, and position sizing from risk budget.
- Multi-position: Opens new positions without touching existing ones, and closes a specific one by
position_id. - Real-time SL/TP: A WebSocket ticker monitor closes positions the moment stop-loss or take-profit is hit, with trailing stop and trailing TP.
- Observability: Per-node latency, I/O, and token usage traced in Opik (optional), plus Slack alerts.
flowchart TD
subgraph DataLayer["Data Layer"]
WS["Bithumb WebSocket"]
CS["CandleStreamer"]
TM["TickerMonitor"]
DB[("SQLite / PostgreSQL")]
WS --> CS --> DB
WS --> TM
end
subgraph AgentWorkflow["LangGraph Multi-Agent Workflow"]
CB["Context Builder<br/>prices · indicators · news · open positions"]
TA["Technical Analyst<br/>OHLCV · indicators"]
SA["Sentiment Analyst<br/>news · market mood"]
RD["Researcher Debate<br/>Bull vs Bear"]
FM["Fund Manager<br/>final action"]
CB --> TA
CB --> SA
TA --> RD
SA --> RD
RD --> FM
CB -. "Portfolio & Risk" .-> FM
end
subgraph ExecutionLayer["Execution Layer"]
RE["Risk Engine<br/>approve / reject / size"]
EX["Paper / Live Executor<br/>open · close per position"]
EXC(["Bithumb / Paper"])
RE --> EX --> EXC
end
subgraph NotificationLayer["Notification Layer"]
SL["Slack"]
GM["Gemini Summarizer<br/>(Vertex AI)"]
end
DB --> CB
FM -- "LONG / SHORT / CLOSE_POSITION / HOLD" --> RE
TM -- "real-time SL/TP watch" --> RE
EX -. "new entry" .-> GM --> SL
EX -. "SL/TP fill" .-> SL
Requires Python 3.12+ and uv.
uv sync
cp .env.example .env # set LLM provider and API keys
uv run coin-trading init-db # create tables (first time only)
uv run coin-trading run-once # collect data + one LLM decision
uv run streamlit run src/coin_trading/dashboard.pyThe default DATABASE_URL is a local SQLite file. docker-compose.yml provides PostgreSQL, worker, and dashboard services.
| Command | Description |
|---|---|
init-db |
Create DB tables. --reset clears data |
serve-all |
Real-time WebSocket + scheduled decision loop + health check (production) |
refresh-data |
Collect prices and indicators only |
decide-once |
One LLM decision from data already in the DB |
run-once |
refresh-data then decide-once |
Run each as uv run coin-trading <command>.
Entry
| Action | Condition |
|---|---|
| LONG | Uptrend · RSI 30–65 · risk/reward ≥ 2:1 · at least one soft signal |
| SHORT | Downtrend · RSI 35–70 · risk/reward ≥ 2:1 · at least one soft signal |
| HOLD | Unclear signal · confidence < 0.50 · not enough data |
Soft signals: momentum (RSI/MACD direction), volume (volume_ratio ≥ 0.40), or debate result (Bull/Bear WEAK or stronger).
Minimum SL/TP distance (enforced in code for every provider)
| Minimum | |
|---|---|
| SL distance | max(1.5 × ATR, entry × 0.3%) |
| TP distance | max(3.0 × ATR, entry × 0.6%) |
| Risk/Reward | 2:1 or more |
Position size by confidence
| confidence | Max allocation |
|---|---|
| < 0.60 | 50% of MAX_POSITION_ALLOCATION_PCT |
| 0.60 – 0.75 | 75% of MAX_POSITION_ALLOCATION_PCT |
| ≥ 0.75 | Up to MAX_POSITION_ALLOCATION_PCT |
Close is suggested when TP is reached, the trend reverses, price is within 0.3 × ATR of SL, or there is no progress after 8–10 hours.
signal
├─ HOLD → no order
├─ CLOSE_POSITION → validate position_id → close
└─ LONG / SHORT
├─ leverage > MAX_LEVERAGE → reject
├─ drawdown > KILL_SWITCH_DRAWDOWN → reject (kill switch)
├─ re-entry cooldown not over → reject
├─ confidence < 0.50 → reject
└─ approve → size the order → execute
risk_budget = current_equity × RISK_PER_TRADE
raw_quantity = risk_budget / |entry_price - stop_loss|
allocation_pct = min(LLM suggestion, MAX_POSITION_ALLOCATION_PCT)
max_notional = current_equity × allocation_pct × leverage
final_quantity = min(raw_quantity, max_notional / entry_price)
| Mechanism | Behavior | Env var |
|---|---|---|
| Trailing stop | SL follows the price by a fixed % | TRAILING_STOP_PCT |
| Trailing TP (comeback) | When price breaks TP, the old TP becomes a trigger and TP extends. Closes if price falls back to the trigger | TRAILING_TP_PCT |
| Tighten SL near TP | Within 1.5 × ATR of TP, SL moves 0.5 × ATR toward entry | automatic |
| Safety setting | Default | Description |
|---|---|---|
RISK_PER_TRADE |
5% | Max equity lost if one trade hits SL |
MAX_POSITION_ALLOCATION_PCT |
20% | Max equity in one position |
KILL_SWITCH_DRAWDOWN |
10% | Blocks all new entries above this drawdown |
LIQUIDATION_BUFFER |
8% | Warning event within 8% of liquidation price |
PRICE_CONSISTENCY_THRESHOLD_PCT |
0.5% | Cancels the order if price moves more than this during the LLM decision |
TRADING_MODE |
paper |
Live orders need live and LIVE_TRADING_ENABLED=true |
News comes from the CoinDesk Data API (default) or RSS feeds and is used by the sentiment analyst. CoinDesk's own SENTIMENT label (POSITIVE / NEUTRAL / NEGATIVE) is passed to the LLM as is. A high watermark (AppState.coindesk_last_published_ts) makes each cycle insert only new articles.
NEWS_SOURCE=coindesk # coindesk | rss
COINDESK_API_KEY=...
COINDESK_CATEGORIES=BTC # empty = all
COINDESK_FETCH_LIMIT=50- Opik: Set
OPIK_API_KEY,OPIK_WORKSPACE, andOPIK_PROJECT_NAMEto trace each node. Unset means tracing is off and trading is not affected. Node timings are also written to stdout andlogs/trading.log. - Slack (
SLACK_WEBHOOK_URL): new entries (summarized by Gemini on Vertex AI, with a plain fallback), SL/TP fills, and health checks at 08:00 / 15:00 / 21:00.
See .env.example for all options. Main ones:
TRADING_MODE=paper # paper | live
EXCHANGE=bithumb_spot
SYMBOL=KRW-BTC
RISK_PER_TRADE=0.05
MAX_LEVERAGE=1
MAX_POSITION_ALLOCATION_PCT=20
KILL_SWITCH_DRAWDOWN=0.10
LLM_PROVIDER=openrouter # mock | openai | gemini | openrouter | nvidia | vertex
LLM_MODEL=google/gemma-4-31b-it:freeAnalyst and researcher agents can use a different model with ANALYST_LLM_* and RESEARCHER_LLM_*. Without LLM_PROVIDER, the mock provider is used.
scripts/bot.sh registers the bot with launchd.
./scripts/bot.sh start # start, auto-start on boot, auto-restart on crash
./scripts/bot.sh stop # stop and unregister
./scripts/bot.sh status # PID, memory, CPU
./scripts/bot.sh logs # follow trading log
./scripts/bot.sh errors # last 50 error linesLogs rotate daily in logs/trading.log and are kept for 7 days.
src/coin_trading/
├── agent/ LangGraph agents (analysts, researchers, fund manager), prompts, schemas
├── db/ SQLAlchemy models and session
├── market/ Price collection, indicators, news, Bithumb REST/WebSocket
├── trade/ Risk engine, portfolio, paper/live executors
├── notifications/ Slack, Gemini summarizer
├── app.py CLI entry point
├── config.py Pydantic settings
├── dashboard.py Streamlit dashboard
└── scheduler.py Pipeline and serve-all loop
uv run pytest