CalcuOnline
HomeTech Tutorials / ReviewsFinance & BusinessBuilding a Real-Time Algorithmic Trading Bot Using LLMs and the Alpaca Markets API
Building a Real-Time Algorithmic Trading Bot Using LLMs and the Alpaca Markets API

Building a Real-Time Algorithmic Trading Bot Using LLMs and the Alpaca Markets API

Finance & Business 5.0 Updated 24 August 2026

Scope and a necessary disclaimer

This tutorial is educational systems-engineering content, not investment advice. It builds a paper-trading bot end to end — real-time bar streaming, a rules-based technical signal, an LLM-based news-sentiment gate, and order execution through Alpaca's paper trading endpoint. Live trading with real capital introduces regulatory, risk-management, and capital-loss considerations far beyond this tutorial's scope; treat everything here as a foundation to rigorously backtest and risk-review before ever pointing it at a live account.

1. Technical architecture

┌───────────────────┐    WebSocket (bars)    ┌────────────────────────┐
│ Alpaca Market Data   │ ─────────────────────▶│ Bar aggregator             │
│ Stream (IEX/SIP)       │                       │ (in-memory OHLCV buffer)    │
└───────────────────┘                        └───────────┬────────────┘
                                                             ▼
                                              ┌────────────────────────┐
                                              │ Technical signal engine    │
                                              │ (SMA crossover + RSI)      │
                                              └───────────┬────────────┘
                                                             │ candidate signal
                                                             ▼
┌───────────────────┐    REST (poll, cached)  ┌────────────────────────┐
│ News API (headlines)  │ ───────────────────▶ │ LLM sentiment gate         │
│                        │                       │ (Claude via API, JSON out) │
└───────────────────┘                        └───────────┬────────────┘
                                                             │ confirmed signal
                                                             ▼
                                              ┌────────────────────────┐
                                              │ Risk manager                │
                                              │ (position size, daily loss  │
                                              │  cap, max concurrent trades)│
                                              └───────────┬────────────┘
                                                             ▼
                                              ┌────────────────────────┐
                                              │ Alpaca Trading API          │
                                              │ (paper endpoint, bracket    │
                                              │  orders w/ stop-loss)       │
                                              └────────────────────────┘

2. Complete code implementation

2.1 Real-time bar stream and technical signal

# trading/signal_engine.py
from collections import deque
from dataclasses import dataclass, field


@dataclass
class SymbolState:
    closes: deque = field(default_factory=lambda: deque(maxlen=50))

    def sma(self, window: int) -> float | None:
        if len(self.closes) < window:
            return None
        recent = list(self.closes)[-window:]
        return sum(recent) / window

    def rsi(self, window: int = 14) -> float | None:
        if len(self.closes) < window + 1:
            return None
        prices = list(self.closes)[-(window + 1):]
        gains, losses = [], []
        for i in range(1, len(prices)):
            delta = prices[i] - prices[i - 1]
            gains.append(max(delta, 0))
            losses.append(max(-delta, 0))
        avg_gain = sum(gains) / window
        avg_loss = sum(losses) / window
        if avg_loss == 0:
            return 100.0
        rs = avg_gain / avg_loss
        return 100 - (100 / (1 + rs))


class SignalEngine:
    """SMA(10)/SMA(30) crossover confirmed by RSI not already overbought/oversold."""

    def __init__(self):
        self.state: dict[str, SymbolState] = {}

    def on_bar(self, symbol: str, close_price: float) -> str | None:
        s = self.state.setdefault(symbol, SymbolState())
        s.closes.append(close_price)

        fast, slow, rsi = s.sma(10), s.sma(30), s.rsi(14)
        if fast is None or slow is None or rsi is None:
            return None  # not enough history yet

        if fast > slow and rsi < 70:
            return "buy"
        if fast < slow and rsi > 30:
            return "sell"
        return None

2.2 LLM sentiment gate

# trading/sentiment_gate.py
import json
import time
from anthropic import Anthropic, APIStatusError, APITimeoutError

client = Anthropic()  # reads ANTHROPIC_API_KEY from environment

SYSTEM_PROMPT = """You are a financial news sentiment classifier. Given recent
headlines about a stock ticker, respond with strict JSON only:
{"sentiment": "positive"|"neutral"|"negative", "confidence": 0.0-1.0,
"reason": "one short sentence"}. Base your answer only on the headlines given."""


def classify_sentiment(symbol: str, headlines: list[str], max_retries: int = 3) -> dict:
    if not headlines:
        return {"sentiment": "neutral", "confidence": 0.0, "reason": "no recent headlines"}

    user_content = f"Ticker: {symbol}\nHeadlines:\n" + "\n".join(f"- {h}" for h in headlines[:10])

    for attempt in range(max_retries):
        try:
            resp = client.messages.create(
                model="claude-sonnet-4-5",
                max_tokens=200,
                system=SYSTEM_PROMPT,
                messages=[{"role": "user", "content": user_content}],
            )
            text = resp.content[0].text
            return json.loads(text)
        except (APIStatusError, APITimeoutError) as exc:
            if attempt == max_retries - 1:
                # Fail safe: on repeated API failure, treat as neutral rather
                # than blocking trading entirely or defaulting to "positive".
                return {"sentiment": "neutral", "confidence": 0.0, "reason": f"sentiment API error: {exc}"}
            time.sleep(2 ** attempt)
        except (json.JSONDecodeError, IndexError):
            # Model didn't return valid JSON — do not guess, fail safe to neutral.
            return {"sentiment": "neutral", "confidence": 0.0, "reason": "unparseable model response"}

2.3 Risk manager and order execution

# trading/risk_manager.py
from dataclasses import dataclass


@dataclass
class RiskLimits:
    max_position_pct: float = 0.05      # max 5% of equity per position
    max_concurrent_positions: int = 8
    daily_loss_limit_pct: float = 0.03  # halt trading after 3% daily drawdown


class RiskManager:
    def __init__(self, limits: RiskLimits):
        self.limits = limits
        self.starting_equity: float | None = None
        self.trading_halted = False

    def check_daily_loss(self, current_equity: float) -> bool:
        if self.starting_equity is None:
            self.starting_equity = current_equity
            return True
        drawdown = (self.starting_equity - current_equity) / self.starting_equity
        if drawdown >= self.limits.daily_loss_limit_pct:
            self.trading_halted = True
        return not self.trading_halted

    def position_size(self, equity: float, price: float) -> int:
        max_dollar_amount = equity * self.limits.max_position_pct
        qty = int(max_dollar_amount // price)
        return max(qty, 0)
# trading/executor.py
import logging
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import MarketOrderRequest, StopLossRequest, TakeProfitRequest
from alpaca.trading.enums import OrderSide, TimeInForce
from alpaca.common.exceptions import APIError

logger = logging.getLogger("executor")


class OrderExecutor:
    def __init__(self, api_key: str, secret_key: str, paper: bool = True):
        self.client = TradingClient(api_key, secret_key, paper=paper)

    def place_bracket_order(self, symbol: str, qty: int, side: str,
                             stop_loss_pct: float = 0.02, take_profit_pct: float = 0.04) -> dict | None:
        if qty <= 0:
            logger.info("Skipping order for %s: computed quantity is 0", symbol)
            return None

        try:
            quote = self.client.get_latest_quote(symbol)  # last traded price reference
            ref_price = float(quote.ask_price or quote.bid_price)
        except APIError as exc:
            logger.error("Failed to fetch quote for %s: %s", symbol, exc)
            return None

        order_side = OrderSide.BUY if side == "buy" else OrderSide.SELL
        stop_price = round(ref_price * (1 - stop_loss_pct), 2) if side == "buy" else round(ref_price * (1 + stop_loss_pct), 2)
        take_price = round(ref_price * (1 + take_profit_pct), 2) if side == "buy" else round(ref_price * (1 - take_profit_pct), 2)

        request = MarketOrderRequest(
            symbol=symbol,
            qty=qty,
            side=order_side,
            time_in_force=TimeInForce.DAY,
            order_class="bracket",
            stop_loss=StopLossRequest(stop_price=stop_price),
            take_profit=TakeProfitRequest(limit_price=take_price),
        )

        try:
            order = self.client.submit_order(request)
        except APIError as exc:
            status = getattr(exc, "status_code", None)
            if status == 403:
                logger.error("Order rejected — insufficient buying power or PDT restriction for %s", symbol)
            elif status == 429:
                logger.error("Alpaca rate limit hit submitting order for %s", symbol)
            else:
                logger.error("Order submission failed for %s: %s", symbol, exc)
            return None

        logger.info("Submitted %s bracket order for %d shares of %s (order id=%s)", side, qty, symbol, order.id)
        return {"id": str(order.id), "symbol": symbol, "qty": qty, "side": side}

2.4 Main event loop

# trading/main.py
import os
import logging
from alpaca.data.live import StockDataStream
from alpaca.trading.client import TradingClient

from signal_engine import SignalEngine
from sentiment_gate import classify_sentiment
from risk_manager import RiskManager, RiskLimits
from executor import OrderExecutor

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("main")

API_KEY = os.environ["ALPACA_API_KEY"]
SECRET_KEY = os.environ["ALPACA_SECRET_KEY"]
WATCHLIST = ["AAPL", "MSFT", "NVDA"]

signal_engine = SignalEngine()
risk_manager = RiskManager(RiskLimits())
executor = OrderExecutor(API_KEY, SECRET_KEY, paper=True)
trading_client = TradingClient(API_KEY, SECRET_KEY, paper=True)


async def on_bar(bar):
    account = trading_client.get_account()
    if not risk_manager.check_daily_loss(float(account.equity)):
        logger.warning("Daily loss limit hit — trading halted for the session")
        return

    action = signal_engine.on_bar(bar.symbol, bar.close)
    if action is None:
        return

    headlines = fetch_recent_headlines(bar.symbol)  # implement via your news provider
    sentiment = classify_sentiment(bar.symbol, headlines)

    if action == "buy" and sentiment["sentiment"] == "negative" and sentiment["confidence"] > 0.6:
        logger.info("Technical buy signal for %s overridden by negative sentiment", bar.symbol)
        return
    if action == "sell" and sentiment["sentiment"] == "positive" and sentiment["confidence"] > 0.6:
        logger.info("Technical sell signal for %s overridden by positive sentiment", bar.symbol)
        return

    qty = risk_manager.position_size(float(account.equity), bar.close)
    executor.place_bracket_order(bar.symbol, qty, action)


def fetch_recent_headlines(symbol: str) -> list[str]:
    # Plug in a news provider (e.g. Alpaca News API, Polygon, NewsAPI) here.
    return []


def run():
    stream = StockDataStream(API_KEY, SECRET_KEY)
    stream.subscribe_bars(on_bar, *WATCHLIST)
    stream.run()


if __name__ == "__main__":
    run()

3. Step-by-step configuration guide

  1. Create an Alpaca paper trading account at alpaca.markets and generate an API key/secret from the dashboard (Paper Trading tab).
  2. Install dependencies:
    pip install alpaca-py anthropic --break-system-packages
    
  3. Set environment variables:
    export ALPACA_API_KEY="your_paper_key"
    export ALPACA_SECRET_KEY="your_paper_secret"
    export ANTHROPIC_API_KEY="your_anthropic_key"
    
  4. Wire in a news provider in fetch_recent_headlines — Alpaca's News API, Polygon.io, or NewsAPI.org all work; cache results per symbol for a few minutes to avoid rate-limit and cost blowup.
  5. Run the bot against paper trading: python trading/main.py.
  6. Monitor fills in the Alpaca dashboard's Paper Trading → Orders tab, and tail your process logs for RiskManager halt events.
  7. Backtest before any live consideration — replay historical bars through SignalEngine and classify_sentiment offline, and evaluate Sharpe ratio, max drawdown, and win rate over multiple market regimes before trusting the strategy with capital.

4. Error handling and edge cases

Related Reviews

Bank of India Car Loan Review: Consistently Among the Lowest Advertised Rates
Bank of India Car Loan Review: Consistently Among the Lowest Advertised Rates
4.0
Bank of India regularly appears among the cheapest car loan quotes in India at around 7.60% p.a. entry, though its top-end range (up to ~12.55%) is wider than most PSU peers, making profile-specific quotes especially important here.
Read Review → Finance & Business
Axis Bank Car Loan Review: A Reasonable Private-Bank Middle Ground
Axis Bank Car Loan Review: A Reasonable Private-Bank Middle Ground
3.0
Axis Bank prices car loans from roughly 8.75%-9.40% p.a., broadly in line with HDFC Bank and ICICI Bank, with a fast "Xpress"-style digital approval flow but no standout rate or fee advantage over its private-bank peers.
Read Review → Finance & Business
Punjab National Bank Car Loan Review: A Steady, Wide-Reach PSU Option
Punjab National Bank Car Loan Review: A Steady, Wide-Reach PSU Option
4.0
PNB combines a competitive entry rate (roughly 7.55%-10.70% p.a.) with low processing fees starting around Rs.1,000 and up to 100% on-road funding, backed by one of the largest PSU branch networks in India.
Read Review → Finance & Business