What You Are Building
A Python trading bot that connects to Alpaca’s official MCP V2 server to fetch live US stock data, calculate moving average signals, and execute paper trades through Claude Code. Alpaca released MCP V2 in April 2026 with 61 tools covering stocks, ETFs, options, and crypto. This tutorial walks you through connecting the server, writing a simple strategy, and running it against Alpaca’s paper trading environment.
Why Alpaca MCP
Alpaca is the most popular commission-free trading API for algorithmic traders. Their V2 MCP server makes AI-assisted stock trading much simpler because you no longer need to write API wrapper code. Claude Code calls the MCP tools directly. You describe what you want in plain English and the AI handles the API calls.
Here is what Alpaca MCP V2 exposes compared to V1:
| Feature | V1 (43 tools) | V2 (61 tools) |
|---|---|---|
| Stock orders | Market, limit | Market, limit, stop, trailing stop |
| Options | Not supported | Full chain exploration, multi-leg spreads |
| Crypto | Basic | Full trading support |
| Screening | Not available | Built-in market screener |
| Account | Balance only | Activity logs, order history, positions |
| Order management | Place only | Place, replace, cancel, bracket orders |
If you have followed our Bybit MCP tutorial for crypto, the workflow here is similar. The main difference is Alpaca focuses on US equities and options instead of crypto futures.
Prerequisites
- Python 3.10+
- An Alpaca account (free, paper trading enabled by default)
- Claude Code installed and working
- Node.js 18+ (for the MCP server)
- Basic Python familiarity
Step 1: Get Alpaca API Keys
Alpaca gives you separate keys for paper and live trading:
- Sign up at alpaca.markets
- Go to the Paper Trading dashboard
- Click API Keys and generate a new key pair
- Save the API key ID and secret key
Paper trading uses real market data but simulated execution. Your orders fill against real prices but no actual money moves.
Step 2: Install the Alpaca MCP Server
Alpaca’s official V2 server is on npm:
npm install -g @alpacahq/mcp-server
Or install from the GitHub repo:
git clone https://github.com/alpacahq/alpaca-mcp-server.git ~/alpaca-mcp
cd ~/alpaca-mcp
npm install
Step 3: Configure Claude Code
Add the Alpaca MCP server to your Claude Code configuration:
nano ~/.claude/.mcp.json
{
"mcpServers": {
"alpaca": {
"command": "npx",
"args": ["-y", "@alpacahq/mcp-server"],
"env": {
"ALPACA_API_KEY": "your-paper-api-key",
"ALPACA_API_SECRET": "your-paper-secret",
"ALPACA_PAPER": "true"
}
}
}
}
Keep ALPACA_PAPER set to true until you have tested your strategy thoroughly. Switching to live trading is a one-line change, but you should run paper for at least two weeks first.
If you already run other MCP servers (like TradingView MCP or Bybit MCP), add the Alpaca block alongside them in mcpServers.
Step 4: Verify the Connection
Restart Claude Code and test:
What's my Alpaca paper trading buying power?
Claude should return your account info through the MCP tools. The default paper account starts with $100,000 in buying power. If you get an error, check that your API keys are correct and that the MCP server path is valid.
The Strategy: Moving Average Crossover
We will build a classic MA crossover strategy for SPY (S&P 500 ETF):
- Fetch daily OHLCV bars for the last 100 trading days
- Calculate a 20-day simple moving average (fast) and a 50-day SMA (slow)
- Buy when the fast MA crosses above the slow MA (golden cross)
- Sell when the fast MA crosses below the slow MA (death cross)
- Use a 2% stop-loss on every position
- One position at a time, sized at 20% of portfolio value
This is a teaching strategy, not a profitable system out of the box. The point is to show how market data, signal logic, and order execution connect through Alpaca’s API. Tuning it for real returns takes separate work.
Prompt 1: Core Bot Structure
Here is what I gave Claude Code:
Build a Python stock trading bot for SPY using Alpaca’s API with these specs:
- Use the alpaca-py library to connect to Alpaca paper trading
- Fetch 100 daily bars of OHLCV data for SPY
- Calculate 20-day and 50-day simple moving averages using pandas
- Buy when the 20-day SMA crosses above the 50-day SMA
- Sell when the 20-day SMA crosses below the 50-day SMA
- Position size: 20% of portfolio equity per trade
- Set a 2% stop-loss on every buy order using a trailing stop
- Only one open position at a time — check existing positions first
- Run in a loop, checking every 5 minutes during market hours
- Log every signal, order, and fill to console and CSV
- Skip trading when market is closed
- Load API keys from environment variables
Claude Code generated a solid first draft. The main issues:
- It used the older
alpaca-trade-apilibrary instead ofalpaca-py - Market hours check did not account for half-days or holidays
- No handling for partial fills
Prompt 2: Fix Library and Market Hours
Fix these issues in the Alpaca bot:
- Switch to the alpaca-py library (from alpaca.trading.client import TradingClient)
- Use Alpaca’s built-in clock endpoint to check if the market is open instead of hardcoding hours
- Add proper error handling for InsufficientFunds, APIError, and connection timeouts
- Log the account equity and buying power on each loop iteration
Prompt 3: Risk Controls
Add risk management to the Alpaca bot:
- Maximum 2 trades per day — reset counter at market open
- Weekly loss limit: if net realized loss exceeds $500 for the week, stop trading until Monday
- Track win/loss count and log stats every 5 trades
- Add a kill switch: if file “STOP” exists in the working directory, exit gracefully
- Send a summary log entry at market close with day’s PnL
The Full Code
Here is the complete bot after three rounds of prompting with manual corrections noted in comments:
import os
import time
import logging
import csv
from datetime import datetime, timezone, timedelta
from pathlib import Path
from dataclasses import dataclass, field
import pandas as pd
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import (
MarketOrderRequest,
TrailingStopOrderRequest,
GetAssetsRequest,
)
from alpaca.trading.enums import OrderSide, TimeInForce, OrderType
from alpaca.data.historical import StockHistoricalDataClient
from alpaca.data.requests import StockBarsRequest
from alpaca.data.timeframe import TimeFrame
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("alpaca_bot.log"),
logging.StreamHandler(),
],
)
logger = logging.getLogger(__name__)
# --- Config ---
API_KEY = os.environ.get("ALPACA_API_KEY", "")
API_SECRET = os.environ.get("ALPACA_API_SECRET", "")
PAPER = os.environ.get("ALPACA_PAPER", "true").lower() == "true"
SYMBOL = "SPY"
FAST_MA = 20
SLOW_MA = 50
POSITION_SIZE_PCT = 0.20
STOP_LOSS_PCT = 0.02
MAX_TRADES_PER_DAY = 2
WEEKLY_LOSS_LIMIT = -500.0
CHECK_INTERVAL = 300 # 5 minutes
@dataclass
class BotState:
position_qty: float = 0.0
entry_price: float = 0.0
trades_today: int = 0
today_date: str = ""
weekly_pnl: float = 0.0
week_start: str = ""
total_trades: int = 0
wins: int = 0
losses: int = 0
def get_trading_client():
return TradingClient(API_KEY, API_SECRET, paper=PAPER)
def get_data_client():
return StockHistoricalDataClient(API_KEY, API_SECRET)
def fetch_bars(data_client, symbol: str, limit: int = 100):
"""Fetch daily bars and return a DataFrame."""
end = datetime.now(timezone.utc)
start = end - timedelta(days=limit * 2) # Fetch extra to account for weekends
request = StockBarsRequest(
symbol_or_symbols=symbol,
timeframe=TimeFrame.Day,
start=start,
end=end,
limit=limit,
)
bars = data_client.get_stock_bars(request)
df = bars.df.reset_index()
# Handle MultiIndex if multiple symbols
if "symbol" in df.columns:
df = df[df["symbol"] == symbol]
df = df.sort_values("timestamp").tail(limit).reset_index(drop=True)
return df
def calc_signals(df: pd.DataFrame) -> pd.DataFrame:
"""Add moving averages and crossover signals."""
df["sma_fast"] = df["close"].rolling(window=FAST_MA).mean()
df["sma_slow"] = df["close"].rolling(window=SLOW_MA).mean()
df["prev_fast"] = df["sma_fast"].shift(1)
df["prev_slow"] = df["sma_slow"].shift(1)
return df
def check_market_open(trading_client) -> bool:
"""Use Alpaca's clock endpoint to check market status."""
clock = trading_client.get_clock()
return clock.is_open
def get_account_info(trading_client) -> dict:
account = trading_client.get_account()
return {
"equity": float(account.equity),
"buying_power": float(account.buying_power),
"cash": float(account.cash),
}
def get_position(trading_client, symbol: str) -> dict:
"""Check if we hold a position in this symbol."""
try:
pos = trading_client.get_open_position(symbol)
return {
"qty": float(pos.qty),
"entry_price": float(pos.avg_entry_price),
"market_value": float(pos.market_value),
"unrealized_pnl": float(pos.unrealized_pl),
}
except Exception:
return {"qty": 0, "entry_price": 0, "market_value": 0, "unrealized_pnl": 0}
def place_buy(trading_client, symbol: str, qty: float, trail_pct: float):
"""Place a market buy with a trailing stop."""
try:
# Market buy
buy_request = MarketOrderRequest(
symbol=symbol,
qty=round(qty, 0),
side=OrderSide.BUY,
time_in_force=TimeInForce.DAY,
)
buy_order = trading_client.submit_order(buy_request)
logger.info(f"BUY order submitted: {qty:.0f} shares of {symbol}")
# Trailing stop for risk management
stop_request = TrailingStopOrderRequest(
symbol=symbol,
qty=round(qty, 0),
side=OrderSide.SELL,
time_in_force=TimeInForce.GTC,
trail_percent=trail_pct * 100,
)
stop_order = trading_client.submit_order(stop_request)
logger.info(f"Trailing stop set at {trail_pct*100:.1f}%")
return buy_order
except Exception as e:
logger.error(f"Buy order failed: {e}")
return None
def place_sell(trading_client, symbol: str, qty: float):
"""Sell entire position at market."""
try:
# Cancel any open stop orders first
trading_client.cancel_orders()
time.sleep(1)
sell_request = MarketOrderRequest(
symbol=symbol,
qty=round(qty, 0),
side=OrderSide.SELL,
time_in_force=TimeInForce.DAY,
)
order = trading_client.submit_order(sell_request)
logger.info(f"SELL order submitted: {qty:.0f} shares of {symbol}")
return order
except Exception as e:
logger.error(f"Sell order failed: {e}")
return None
def log_trade(action, price, sma_fast, sma_slow, pnl=0.0):
with open("trades.csv", "a", newline="") as f:
writer = csv.writer(f)
writer.writerow([
datetime.now(timezone.utc).isoformat(),
action,
round(price, 2),
round(sma_fast, 2),
round(sma_slow, 2),
round(pnl, 2),
])
def run():
trading_client = get_trading_client()
data_client = get_data_client()
state = BotState()
state.today_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
state.week_start = datetime.now(timezone.utc).strftime("%Y-%W")
# CSV header
if not Path("trades.csv").exists():
with open("trades.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["timestamp", "action", "price", "sma_fast", "sma_slow", "pnl"])
logger.info(f"Bot started | Symbol: {SYMBOL} | Paper: {PAPER}")
logger.info(f"Strategy: SMA({FAST_MA}/{SLOW_MA}) crossover")
while True:
# Kill switch
if Path("STOP").exists():
logger.info("Kill switch activated. Exiting.")
break
# Check market hours
if not check_market_open(trading_client):
logger.info("Market closed. Waiting.")
time.sleep(CHECK_INTERVAL)
continue
# Reset daily counters
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
if today != state.today_date:
state.trades_today = 0
state.today_date = today
# Reset weekly PnL
this_week = datetime.now(timezone.utc).strftime("%Y-%W")
if this_week != state.week_start:
logger.info(f"New week. Last week PnL: ${state.weekly_pnl:.2f}")
state.weekly_pnl = 0.0
state.week_start = this_week
# Check weekly loss limit
if state.weekly_pnl <= WEEKLY_LOSS_LIMIT:
logger.warning(f"Weekly loss limit hit (${state.weekly_pnl:.2f}). Paused until next week.")
time.sleep(CHECK_INTERVAL)
continue
# Check daily trade limit
if state.trades_today >= MAX_TRADES_PER_DAY:
logger.info(f"Max daily trades reached ({MAX_TRADES_PER_DAY}). Waiting.")
time.sleep(CHECK_INTERVAL)
continue
try:
# Get account info
account = get_account_info(trading_client)
logger.info(f"Equity: ${account['equity']:,.2f} | Buying power: ${account['buying_power']:,.2f}")
# Fetch data and calculate signals
df = fetch_bars(data_client, SYMBOL)
df = calc_signals(df)
current = df.iloc[-1]
previous = df.iloc[-2]
price = current["close"]
sma_fast = current["sma_fast"]
sma_slow = current["sma_slow"]
prev_fast = previous["sma_fast"]
prev_slow = previous["sma_slow"]
logger.info(f"{SYMBOL}: ${price:.2f} | SMA{FAST_MA}: ${sma_fast:.2f} | SMA{SLOW_MA}: ${sma_slow:.2f}")
# Check current position
pos = get_position(trading_client, SYMBOL)
has_position = pos["qty"] > 0
# Golden cross: fast MA crosses above slow MA
golden_cross = prev_fast <= prev_slow and sma_fast > sma_slow
# Death cross: fast MA crosses below slow MA
death_cross = prev_fast >= prev_slow and sma_fast < sma_slow
if not has_position and golden_cross:
trade_value = account["equity"] * POSITION_SIZE_PCT
qty = int(trade_value / price)
if qty > 0:
result = place_buy(trading_client, SYMBOL, qty, STOP_LOSS_PCT)
if result:
state.trades_today += 1
state.total_trades += 1
log_trade("BUY", price, sma_fast, sma_slow)
elif has_position and death_cross:
pnl = pos["unrealized_pnl"]
result = place_sell(trading_client, SYMBOL, pos["qty"])
if result:
state.trades_today += 1
state.total_trades += 1
state.weekly_pnl += pnl
if pnl > 0:
state.wins += 1
else:
state.losses += 1
log_trade("SELL", price, sma_fast, sma_slow, pnl)
# Log stats every 5 trades
if state.total_trades > 0 and state.total_trades % 5 == 0:
wr = state.wins / state.total_trades * 100
logger.info(f"Stats: {state.total_trades} trades | {wr:.1f}% win rate")
except Exception as e:
logger.error(f"Error in main loop: {e}")
time.sleep(CHECK_INTERVAL)
if __name__ == "__main__":
run()
What Claude Code Got Right
- The
alpaca-pylibrary usage was correct after the second prompt. TheTradingClient,StockHistoricalDataClient, and order request objects all matched Alpaca’s current API. - Using the clock endpoint for market hours is the right approach. Hardcoding “9:30 AM to 4 PM ET” breaks on holidays and half-days.
- The trailing stop order is placed separately from the market buy. Alpaca does not support bracket orders with trailing stops in a single request, so this two-order approach is correct.
What Needed Manual Fixes
- Library choice: Claude Code initially used
alpaca-trade-api(the old library). The current library isalpaca-pywith imports fromalpaca.trading.client. I corrected this in Prompt 2. - Quantity rounding: Alpaca requires whole share quantities for most stocks. Claude Code initially passed float quantities, which caused API errors. The fix was wrapping quantities in
int()orround(qty, 0). - Cancel-before-sell: When closing a position, you need to cancel existing stop orders first. Otherwise Alpaca rejects the sell because the stop order already has a sell queued for those shares. Claude Code missed this on the first pass.
Running the Bot
pip install alpaca-py pandas
export ALPACA_API_KEY="your-paper-key"
export ALPACA_API_SECRET="your-paper-secret"
export ALPACA_PAPER="true"
python alpaca_bot.py
The bot only trades during market hours (9:30 AM - 4:00 PM ET on regular trading days). Outside those hours, it logs “Market closed” and waits. To stop the bot cleanly:
touch STOP
Checking Results
Watch the console output and review trades.csv for a complete log. You can also check your paper trading dashboard at app.alpaca.markets to see orders and positions in real time.
Moving to Live Trading
Before switching to real money:
- Run on paper for at least 2 weeks to understand the strategy’s behavior across different market conditions
- Review
trades.csvfor patterns. Is the bot overtrading? Are stops getting hit right after entry? - Start with a small allocation (5-10% of your planned capital)
- Change
ALPACA_PAPERtofalseand use your live API keys - Monitor the first week closely
Warning: This bot trades real US stocks with real money when paper mode is off. The strategy in this tutorial is for educational purposes. Past performance in backtesting or paper trading does not guarantee live results. Only trade with money you can afford to lose.
What to Build Next
- Add a volume filter to avoid signals on low-volume days (holidays, pre-earnings quiet periods)
- Combine with TradingView MCP for visual chart confirmation before executing trades
- Expand to multiple symbols by running the same strategy on QQQ, IWM, and DIA alongside SPY
- Add options hedging with Alpaca’s new options support: buy protective puts when the bot enters a long position
- Try the momentum strategy instead of MA crossover for a different approach to trend following