DEV Community

Andrew
Andrew

Posted on

How I Built a Bitcoin Trading Bot in Rust (With Live Execution on AWS)

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, &timestamp, &ema9, &ema50, &rsi])
        .await?;

    Ok(rows_affected)
}
Enter fullscreen mode Exit fullscreen mode

📌 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

📌 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(())
}
Enter fullscreen mode Exit fullscreen mode

📌 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)