From clone to first signal in 5 minutes.
Helix ships as a single Python service. Default settings are safe — no real funds are at risk until you explicitly opt in. Always run 7+ days in testnet (or Paper for Kraken) before flipping to live.
# 1. Clone git clone git@github.com:aminbassam/Auto-Trader.git cd Auto-Trader # 2. Configure cp .env.example .env $EDITOR .env # set ACTIVE_EXCHANGE, API keys, Telegram, secret # 3. Boot (testnet by default) docker-compose up -d # 4. Verify curl http://localhost:8000/health | jq # → "status": "ok", "active_exchange": "binance", "trading_mode": "testnet" # 5. Point TradingView at your endpoint # Webhook URL: https://your-vps/webhook # Header: X-Webhook-Secret: $WEBHOOK_SECRET
A signal executor — not a strategy engine.
Indicators live in TradingView (Pine Script v5). Helix's only job is to receive validated signals, gate them through risk, and execute safely. That separation keeps the bot simple and the strategy iterable.
01
Risk before alpha
Daily loss cap, drawdown kill switch, consecutive-loss pause. The bot halts itself before it can blow up.
02
Defaults to safe
Boots in testnet. Switching to live requires confirmation. Withdraw permission on API keys is disabled and verified.
03
No orphan positions
State reconciler compares DB vs exchange on every boot. Mismatch → halt + Telegram alert. Humans decide.
At a glance
Repository | aminbassam/Auto-Trader |
Pair (MVP) | BTC/USDT (CCXT-normalised) |
Timeframe | 5-minute candles |
Exchanges | Binance · Kraken (single-active per deploy) |
Hosting | VPS · Dubai or Frankfurt · Docker |
Target uptime | 99.9% |
Signal → fill | < 2s |
Max drawdown target | < 15% (kill switch) |
Layered, async, and exchange-agnostic.
The factory pattern is the key v1.1 change: the rest of the codebase never references Binance or Kraken directly — only the abstract ExchangeClient interface. Adding Bybit or OKX in v2 means writing one new class.
TradingView Pine Script │ (alert fires) ▼ HTTPS POST → FastAPI /webhook [X-Webhook-Secret header] │ (Pydantic parse) ▼ SignalHandler.dedup(signal_id, 60s window) │ ▼ RiskManager.validate(signal) [7 pre-trade checks] │ (pass) ▼ ExchangeFactory.get_client() [reads ACTIVE_EXCHANGE] │ ▼ ExchangeClient.place_market_order(symbol, side, qty) ExchangeClient.place_stop_loss() [OCO or conditional close] │ ▼ SQLite trade log + Telegram notify
Abstract interface
Every exchange client implements the same surface. Callers code against the interface — never the concrete class.
class AbstractExchangeClient(ABC): @abstractmethod async def connect(): ... @abstractmethod async def disconnect(): ... @abstractmethod async def get_balance(): ... @abstractmethod async def get_ticker(symbol): ... @abstractmethod async def place_market_order(symbol, side, qty): ... @abstractmethod async def place_oco_order(symbol, side, qty, stop_price): ... @abstractmethod async def get_open_positions(): ... @abstractmethod async def cancel_order(order_id): ... class BinanceClient(AbstractExchangeClient): # spot · OCO · testnet class KrakenClient(AbstractExchangeClient): # spot · conditional close · paper
Every signal travels the same path.
Any check can veto. Rejected signals are logged with a reason and a Telegram alert. Successful trades are persisted, summarised, and tracked toward the daily loss budget.
- Webhook arrivesPOST /webhook with X-Webhook-Secret header. Rate-limited at 10/min.
- Payload validatesPydantic checks signal, symbol, price, signal_id. Malformed → 422.
- DeduplicationIf the same signal_id was seen in the last 60s, drop silently and log
signal_deduplicated. - Pre-trade risk checksSeven gates run. Failure → reject with reason, Telegram alert, no order placed.
- Position sizing
size = (balance × risk_pct) ÷ stop_pct. Below the exchange's min notional → reject. - Exchange dispatchFactory returns the BinanceClient or KrakenClient based on
ACTIVE_EXCHANGE. - Market order placedWith retry × 3 (exponential backoff) on transient network errors.
- Stop loss attachedOCO (Binance) or conditional close (Kraken). Exchange-side — survives bot crashes.
- Persist + notifySQLite WAL write, Telegram message, daily P&L counter incremented.
position_already_open while a trade is live. Pyramiding ships in v2.EMA cross with RSI filter, on 5m BTC/USDT.
The default strategy is a clean trend-following signal: EMA(9/21) crossover, gated by an RSI(14) momentum filter. The bot does not compute indicators — they all run in TradingView Pine v5 and arrive pre-computed.
AND
RSI(14) > 50
action: open LONG
AND
RSI(14) < 50
action: close position
Source | TradingView Pine Script v5 |
Timeframe | 5m (must match the alert's chart) |
Pair | BTC/USDT |
Indicators | EMA(9), EMA(21), RSI(14) |
Take profit | Deferred for MVP — exit via reverse signal or stop loss. v2 |
Paste this into your TradingView alert.
TradingView fires alerts at the bot's webhook endpoint. The alert's "Message" field must contain this exact JSON shape, with placeholders for live values.
{
"signal": "BUY",
"symbol": "BTC/USDT",
"price": {{close}},
"timestamp": {{timenow}},
"strategy": "ema_crossover",
"signal_id": {{strategy.order.id}}
}
| Field | Required | Notes |
|---|---|---|
signal | required | "BUY" or "SELL" |
symbol | required | CCXT-normalised, e.g. "BTC/USDT" |
price | required | Logged for audit; bot uses live exchange price for sizing. |
timestamp | optional | TradingView server time at alert fire. |
strategy | optional | Free-form identifier — logged for analytics. |
signal_id | required | Critical for dedup. Must be unique per signal — bot drops same-id within 60s. |
X-Webhook-Secret: $WEBHOOK_SECRET. Missing or wrong → 401. Rotate the secret periodically.Fixed-fraction sizing, market orders, exchange-side stops.
Position size is calculated from account equity and stop-loss distance. Market orders for MVP — limit orders ship in v2. Stop losses live on the exchange, not in the bot, so they survive crashes.
Position sizing
position_size = (account_balance × risk_pct) ÷ stop_loss_distance_pct # Example with $10,000 equity, 1% risk, 2% stop: position_size = (10000 × 0.01) ÷ 0.02 = $5,000 notional ≈ 0.078 BTC at $64,210 # Risk per trade (USD) $ at risk = account_balance × risk_pct = $100
Order type | MARKET |
Position mode | one-way |
Max concurrent | 1 position (pyramiding in v2) |
Risk per trade | 1.0% of equity · env RISK_PER_TRADE_PCT |
Stop loss | 2.0% · env STOP_LOSS_PCT |
Min notional | $10 — queried from exchange at startup, cached |
Retry policy | 3 retries · exponential backoff on transient errors |
Single-active per deploy.
Pick one exchange per deployment via the ACTIVE_EXCHANGE env var. The factory loads the right client at boot — only that venue's credentials need to be set. Switching exchanges is one env edit + restart.
Kraken-specific notes
- No public testnet API. The bot ships with a
papermode that simulates fills against live order-book prices. Same risk-management behaviour as live — only the orders are never sent. - Conditional close orders. Kraken's equivalent to Binance OCO. Attached to the opening order via the
closeparam. - Stricter rate limits. CCXT's
enableRateLimitis on by default, with extra delay between calls. - XBT vs BTC. Kraken uses XBT for Bitcoin internally; CCXT normalises everything to BTC. You always write
BTC/USDT. - Nonce-based auth. Handled automatically by CCXT.
Seven gates between a signal and an order.
Risk has veto power over every trade. If any check fails, the signal is rejected with a reason and a Telegram alert. The circuit breaker is armed by default and trips on daily loss limit OR max consecutive losses.
Circuit-breaker thresholds
Risk per trade | 1.0% |
Max daily loss | 3.0% — halts until 00:00 UTC reset |
Max drawdown | 15.0% — permanent halt, requires manual restart |
Max consecutive losses | 5 → auto-pause |
Auto-resume | OFF — require manual /resume via Telegram |
All bot ↔ human communication through Telegram.
Outbound events fire on every state change. Inbound /commands let you query and control the bot from your phone. Only the configured TELEGRAM_CHAT_ID can issue commands — all others are silently ignored.
Event templates
trade_openedtrade_closedstop_loss_triggereddaily_limit_hitsignal_rejectederrordaily_summarybot_startupCommands
/status Bot state, open positions, daily P&L/balance Current account balance from exchange/today Today's trade summary/stop Emergency halt — closes positions, stops trading/resume Resume trading after manual haltTELEGRAM_CHAT_ID can execute commands. All others are silently ignored.Daily summary scheduled at 23:59 UTC.
Three HTTP endpoints.
/webhook is the only externally-facing route. /health is consumed by Docker and uptime monitors. /status is internal.
/status command.GET /health response
{
"status": "ok", // ok | degraded | halted
"uptime_seconds": 184302,
"active_exchange": "binance", // binance | kraken
"exchange_connected": true,
"last_signal_time": "2026-05-15T08:14:32Z",
"open_positions": 1,
"daily_pnl_pct": 1.13,
"trading_mode": "testnet" // testnet | live | paper
}
Three SQLite tables. WAL mode.
SQLAlchemy 2.0 async ORM. The schema migrates to Postgres in v2 — same shape, different backend.
trades
id | INTEGER PK AUTOINC |
signal_id | TEXT UNIQUE NOT NULL |
exchange | TEXT NOT NULL · binance | kraken v3 |
symbol | TEXT NOT NULL |
side | TEXT NOT NULL · BUY | SELL |
entry_price | REAL |
exit_price | REAL |
quantity | REAL |
stop_loss_price | REAL |
status | TEXT · open | closed | stopped_out | cancelled |
pnl | REAL |
pnl_pct | REAL |
exchange_order_id | TEXT |
opened_at | TIMESTAMP |
closed_at | TIMESTAMP |
close_reason | TEXT · signal | stop_loss | manual | circuit_breaker |
signals
id | INTEGER PK AUTOINC |
signal_id | TEXT NOT NULL |
payload | TEXT · raw JSON |
action | TEXT · accepted | rejected | deduplicated |
rejection_reason | TEXT |
received_at | TIMESTAMP |
daily_stats
date | TEXT PRIMARY KEY |
trades_count | INTEGER |
wins | INTEGER |
losses | INTEGER |
total_pnl | REAL |
total_pnl_pct | REAL |
max_drawdown_pct | REAL |
daily_loss_limit_hit | BOOLEAN |
All config from .env.
Loaded via pydantic-settings. Never hardcoded. Never logged. .env is gitignored; .env.example is committed as a template.
Required core
| Variable | Required | Description |
|---|---|---|
ACTIVE_EXCHANGE | required | binance | kraken — picks ExchangeClient at boot |
TRADING_MODE | required | testnet | live | paper — paper required for Kraken |
TELEGRAM_BOT_TOKEN | required | From @BotFather |
TELEGRAM_CHAT_ID | required | Your personal chat ID |
WEBHOOK_SECRET | required | Random string passed in X-Webhook-Secret |
Exchange credentials (only the active venue is read)
BINANCE_API_KEY | if binance | Trade-only permissions; never Withdraw. |
BINANCE_API_SECRET | if binance | Treat as password. |
KRAKEN_API_KEY | if kraken | From Kraken API settings. |
KRAKEN_API_SECRET | if kraken | Base64-encoded private key. |
Optional tuning (defaults applied)
TRADING_PAIR | optional | default: BTC/USDT |
LOG_LEVEL | optional | DEBUG | INFO | WARNING · default: INFO |
RISK_PER_TRADE_PCT | optional | default: 1.0 |
MAX_DAILY_LOSS_PCT | optional | default: 3.0 |
MAX_DRAWDOWN_PCT | optional | default: 15.0 |
STOP_LOSS_PCT | optional | default: 2.0 |
MAX_CONSECUTIVE_LOSSES | optional | default: 5 |
HOST | optional | default: 0.0.0.0 |
PORT | optional | default: 8000 |
Pinned, async-first Python.
Every exchange call is awaited; every webhook is non-blocking. Dependencies are pinned in requirements.txt.
| Language | Python 3.11+ |
| Framework | FastAPI |
| Async server | Uvicorn |
| Exchange library | ccxt (async mode) |
| Database | SQLite with WAL mode |
| ORM | SQLAlchemy 2.0 (async) |
| Validation | Pydantic v2 |
| Config | pydantic-settings (.env loader) |
| Alerts | python-telegram-bot (async) |
| Rate limiting | slowapi |
| Deployment | Docker · docker-compose |
| Hosting | VPS · Hetzner / DigitalOcean / Vultr · Frankfurt or Dubai |
| Charts & signals | TradingView Pine Script v5 |
Strict module boundaries.
Risk has veto power. Exchange clients are interchangeable behind the factory. Tests live alongside the modules they protect.
Auto-Trader/ ├── main.py — FastAPI app entry · Uvicorn runner ├── requirements.txt — Pinned deps ├── Dockerfile ├── docker-compose.yml ├── .env.example — Template (committed) ├── .env — Secrets (gitignored) ├── .gitignore ├── README.md │ ├── app/ │ ├── config.py — Pydantic settings, loads .env │ ├── models.py — Trade, Signal, DailyStats │ └── database.py — Async engine, WAL init │ ├── app/api/ │ ├── webhook.py — POST /webhook · dedup · validation │ └── health.py — GET /health │ ├── app/strategy/ │ ├── signal_handler.py — Parse · validate · deduplicate │ └── signal_models.py — Pydantic payload models │ ├── app/execution/ — ⭐ factory pattern (v3) │ ├── base_client.py — AbstractExchangeClient ABC │ ├── exchange_factory.py — Reads ACTIVE_EXCHANGE → client │ ├── binance_client.py — OCO orders · testnet support │ ├── kraken_client.py — Conditional close · paper mode │ └── order_manager.py — Exchange-agnostic dispatch │ ├── app/risk/ — ⭐ VETO power │ ├── risk_manager.py — Pre-trade checks · sizing │ └── circuit_breaker.py — Daily loss · drawdown · cooldown │ ├── app/alerts/ │ ├── telegram_bot.py — Notifications · /commands │ └── templates.py │ ├── app/recovery/ │ └── state_reconciler.py — DB ↔ exchange on boot │ ├── tests/ │ ├── test_risk_manager.py │ ├── test_exchange_factory.py │ ├── test_binance_client.py │ ├── test_kraken_client.py │ ├── test_signal_handler.py │ ├── test_webhook.py │ └── conftest.py — Mock exchanges (both) │ └── logs/ — Runtime logs (gitignored)
Withdraw is OFF. Always.
API keys never touch code or the database. The bot's exchange key must have Withdraw permission disabled — verified on startup. Anything less and the bot refuses to boot.
Encrypted secrets
Loaded from .env via pydantic-settings · never hardcoded · never appears in logs.
IP whitelisting
Configure in your exchange's API settings. Restrict to the VPS IP only.
Withdraw disabled
The bot probes permissions on save. If Withdraw is enabled, the bot refuses to start.
Webhook secret
Every /webhook request must include X-Webhook-Secret. Missing or wrong → 401. Rotate periodically.
Rate limiting
SlowAPI · /webhook 10/min · /health 30/min.
Audit log
All sensitive config changes appended to /var/log/audit.log with actor + timestamp.
Zero orphan tolerance.
On every boot, the reconciler compares the SQLite trade log with the exchange's open positions. Orphans halt the bot and notify — never auto-closed. Humans decide.
- Read
ACTIVE_EXCHANGEfrom configInstantiate that client. No multi-exchange ambiguity at boot. - Query open positionsVia
get_open_positions()on the active exchange. - Cross-reference with DBCompare exchange-side state with the last known DB snapshot.
- If orphan found → halt + alertLog a WARNING, fire a Telegram alert, stop trading. The user decides whether to keep, exit, or reconcile.
- Load daily P&L from DBSo the daily-loss limit picks up where it left off — restarts don't reset the budget.
- Verify pair availabilityConfirm the trading pair is still listed on the active exchange before resuming.
Docker on a VPS near the exchange.
Recommended hosting: Hetzner, DigitalOcean, or Vultr in Frankfurt or Dubai. Lower latency to Binance Frankfurt = faster signal-to-fill.
docker-compose
services: bot: build: . env_file: .env ports: ["8000:8000"] restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 5s retries: 3 volumes: - ./data:/app/data # SQLite + logs - ./logs:/app/logs
Going live
- Run 7+ consecutive days in
testnet(orpaperfor Kraken) with the strategy you intend to use live. - Verify Telegram alerts fire on every state change.
- Confirm circuit breakers are armed — try forcing a daily loss in testnet to watch them trip.
- Set
TRADING_MODE=livein.env. The bot will prompt you to typeI-UNDERSTAND-THE-RISKto confirm. - Restart. The bot will report live mode in
/healthand the Telegram startup message.
Three-phase delivery plan.
v1 ships reliable execution. v2 adds professional features (multi-pair, futures, dashboards, monitoring). v3 evolves into an AI quant platform.
Reliable Execution
- Single pair BTC/USDT
- TradingView webhook → FastAPI
- Binance + Kraken via CCXT (single-active)
- Exchange factory pattern
- OCO / conditional close stop loss
- Risk manager with pre-trade checks
- Daily loss circuit breaker
- Telegram alerts + /commands
- State recovery on restart
- Health check endpoint
- Docker deployment · testnet default
Professional Trading System
- Multi-pair support
- Futures / perpetual trading
- Trailing stop loss
- Take profit (fixed + trailing)
- Web dashboard (FastAPI + React)
- PostgreSQL migration
- Trade history + analytics
- Multi-exchange (Bybit, OKX)
- Grafana + Prometheus monitoring
- Auto-restart with systemd
AI Quant Platform
- Market regime detection (ML)
- Sentiment analysis (news + social)
- AI strategy optimization
- Reinforcement learning signals
- Adaptive position sizing
- Portfolio balancing
- Order book analytics
- Volatility engine
- Backtesting cluster
Spec revisions.
Newest first. Backwards-incompatible changes are flagged.
Added Kraken as a second supported exchange.
- Exchange factory pattern (AbstractExchangeClient → BinanceClient, KrakenClient)
- Single-active per deploy via
ACTIVE_EXCHANGEenv var - Kraken specifics: conditional close, paper-trade mode (no public testnet), stricter rate limit
- 3 new tasks (T15–T17 factory split) — task count: 34 → 37
- New env vars:
ACTIVE_EXCHANGE,KRAKEN_API_KEY,KRAKEN_API_SECRET,TRADING_PAIR tradestable gains anexchangecolumn/healthreturnsactive_exchange+trading_mode(testnet | live | paper)
Project spec finalized. 34-task plan defined.
- Framework: Flask → FastAPI (async-native)
- Max daily loss: 5% → 3%
- Stop loss expanded: boolean → full OCO config
- Deferred Bybit / OKX / BingX to v2 roadmap
- Added testnet default, signal dedup (60s), state recovery,
/health, rate limiting - Added 8 clarifying questions for build kickoff
Common questions.
Can I run Binance and Kraken at the same time?
ACTIVE_EXCHANGE values. True multi-exchange in a single process ships in v2.Why is there no public testnet for Kraken?
paper trading mode that simulates fills against live Kraken market data — same risk-management behaviour, but no orders are actually placed. Use TRADING_MODE=paper on Kraken.What happens if the bot crashes mid-trade?
Does the bot calculate indicators?
How is position size calculated?
position_size = (account_balance × risk_pct) ÷ stop_loss_distance_pct. With $10,000, 1% risk, and a 2% stop, that's $5,000 notional — about 0.078 BTC at $64,210. The actual at-risk amount per trade is just $100 (1% of equity).Can I add take-profit targets?
What does the circuit breaker actually do?
/resume in Telegram to start trading again. The drawdown kill switch (−15%) is permanent: it requires a manual restart.How do I know my API key permissions are safe?
Where is the bot hosted in production?
How do I report a bug or contribute?
type: short description format (feat, fix, refactor, test, docs, chore, risk).Ready to deploy?
Spin up the live dashboard to see every screen in action, or jump straight to the repository.