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! 👇

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more