I recently built a Bitcoin trading bot using Rust that:
✅ Fetches BTC price data from the TAAPI API.
✅ Stores historical market data in PostgreSQL (AWS RDS).
✅ Uses EMA 9, EMA 50, and RSI indicators to detect trends.
✅ Executes real trades via limit orders (with stop-loss & take-profit).
✅ Runs every minute on a cron job inside an EC2 instance.
✅ Logs trade performance into AWS QuickSight for monitoring
Rust’s speed, reliability, and async capabilities made it an excellent choice for a real-time trading system. In this post, I’ll walk through how I built it, the technical decisions, and key challenges.
🛠️ Tech Stack Overview
Component | Technology Used
Programming Language: Rust 🦀
Data Source (API): TAAPI (Technical Analysis API)
Database: PostgreSQL (AWS RDS)
Order Execution: API Requests
Hosting: AWS EC2 (Ubuntu)
Scheduling: Cron Job (Runs Every Minute)
Monitoring: AWS QuickSight (Trade Log Metrics)
Async Execution: Tokio
🔍 Trading Strategy
This bot makes trading decisions using a trend-following strategy based on:
✔️ Exponential Moving Averages (EMA) → Fast (9) vs. Slow (50)
✔️ Relative Strength Index (RSI) → Measures overbought/oversold conditions
✔️ Market Trend Detection → Compares recent BTC price movements
📈 Trade Signal Logic
Condition | Action Taken
EMA 9 crosses above EMA 50 + Bullish Trend: Enter Long
EMA 9 crosses below EMA 50 + Bearish Trend: Enter Short
RSI above 70: Avoid Long (Overbought)
RSI below 30: Avoid Short (Oversold)
Risk Management:
• Stop-loss: -5%
• Take-profit: +10%
Instead of market orders, I use limit orders to reduce slippage.
📡 Data Collection: Storing Market Data in PostgreSQL
The bot fetches BTC price data from the TAAPI API every minute and stores it in PostgreSQL.
pub async fn upsert_historical_data(
client: &Client,
symbol: &str,
price: f32,
timestamp: NaiveDateTime,
ema9: f32,
ema50: f32,
rsi: f32,
) -> Result<u64, Error> {
let query = "
INSERT INTO trading_bot.stg.historical_data (
symbol, price, timestamp, ema9, ema50, rsi
)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (symbol, timestamp)
DO UPDATE SET
price = EXCLUDED.price,
ema9 = EXCLUDED.ema9,
ema50 = EXCLUDED.ema50,
rsi = EXCLUDED.rsi;
";
let rows_affected = client
.execute(query, &[&symbol, &price, ×tamp, &ema9, &ema50, &rsi])
.await?;
Ok(rows_affected)
}
📌 Why Upsert?
This ensures that historical data is updated without duplication and serves essential for evaluating long and short entry points.
📊 Trade Signal Detection: Checking Market Trends
The bot checks if a trend is forming using SQL-based logic.
Code Snippet: Detecting Buy/Sell Signals
pub async fn get_latest_signal(client: &Client) -> Result<Option<Row>, SignalError> {
let signal_query = "
with lookback_data as (
select
id,
symbol,
price as close,
timestamp,
ema9,
ema50,
rsi,
lag(price, 48) over (partition by symbol order by timestamp) as lookback_price
from
trading_bot.stg.historical_data
),
price_change as (
select
*,
((close - lookback_price) / nullif(lookback_price, 0)) * 100 as price_change_per
from
lookback_data
),
market_trend as (
select
*,
case
when price_change_per > (0.02 * 100) and close > ema50 then 'bullish'
when price_change_per < -(0.02 * 100) and close < ema50 then 'bearish'
else 'consolidating'
end as market_trend
from
price_change
),
signals as (
select
*,
case
when market_trend = 'bullish' and ema9 > ema50 and rsi < 70 then 'long'
when market_trend = 'bearish' and ema9 < ema50 and rsi > 30 then 'short'
else null
end as signal
from
market_trend
)
select id, symbol, signal, close as price, timestamp, market_trend, ema9, ema50, rsi
from signals
where signal is not null
order by timestamp desc
limit 1;
";
let result = client.query(signal_query, &[]).await.map_err(SignalError::from)?;
if let Some(row) = result.into_iter().next() {
Ok(Some(row))
} else {
Err(SignalError::NoSignalError)
}
}
📌 Key Optimizations:
• Uses window functions to analyze past data.
• Ensures signals are based on recent price movements.
💰 Executing Trades: API Request to Buy/Sell
If a trade signal is detected, the bot places a limit order via API.
Code Snippet: Executing a Trade
pub async fn execute_trade(symbol: &str, action: &str, price: f32) -> Result<(), reqwest::Error> {
let client = reqwest::Client::new();
let order_payload = json!({
"symbol": symbol,
"side": action,
"type": "LIMIT",
"price": price,
"quantity": 1.0
});
let response = client.post("https://api.exchange.com/order")
.json(&order_payload)
.send()
.await?;
println!("Trade executed: {:?}", response.text().await?);
Ok(())
}
📌 Why Limit Orders?
Market orders are vulnerable to slippage, so I set a limit price to execute only at a desired level.
🖥️ Deployment on AWS
The bot runs on an EC2 instance (Ubuntu) with a cron job:
* * * * * /usr/bin/cargo run --release >> bot.log 2>&1
📌 Why EC2?
• Persistent uptime
• Low-cost scalability
• Easy PostgreSQL RDS integration
📊 Performance Monitoring in AWS QuickSight
To track performance, I log trades into AWS QuickSight, displaying:
✅ Trade win rate
✅ Balance growth (%)
✅ Average trade duration
📌 Why QuickSight?
• Real-time performance insights
• Can export custom reports
⚠️ Challenges & Lessons Learned
1️⃣ Handling API Rate Limits
• Used exponential backoff and retry logic.
• Delayed requests when hitting rate limits.
2️⃣ Testing with Historical Data
• Backtested against 2019-2024 BTC, ETH, SOL.
• Showed ~50% performance improvement over buy-and-hold strategies.
🚀 What’s Next?
✔️ Adding multi-asset support (ETH, SOL, BNB).
✔️ Implementing machine learning-based signals.
✔️ Deploying as a serverless function (AWS Lambda).
💬 What do you think? Have you built a Rust trading bot before? Let’s discuss in the comments! 👇
Top comments (0)