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

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay