Trading Strategies with alpa_ex
View SourceOverview
This guide covers common trading patterns using the alpa_ex SDK against the Alpaca paper trading API. All examples use paper mode by default for safety.
Setup
# Ensure credentials are configured
export APCA_API_KEY_ID="your-key"
export APCA_API_SECRET_KEY="your-secret"
export APCA_USE_PAPER="true"
Strategy 1: Simple Buy-and-Hold
Buy shares and hold until a target price or stop-loss is hit.
defmodule Strategy.BuyAndHold do
def execute(symbol, qty, opts \\ []) do
take_profit = Keyword.get(opts, :take_profit)
stop_loss = Keyword.get(opts, :stop_loss)
# Place a bracket order with take-profit and stop-loss
order_params = %{
symbol: symbol,
qty: qty,
side: "buy",
type: "market",
time_in_force: "day"
}
order_params =
if take_profit && stop_loss do
Map.merge(order_params, %{
order_class: "bracket",
take_profit: %{limit_price: take_profit},
stop_loss: %{stop_price: stop_loss}
})
else
order_params
end
Alpa.Trading.Orders.place(Map.to_list(order_params))
end
end
# Usage:
# Strategy.BuyAndHold.execute("AAPL", 10, take_profit: "200.00", stop_loss: "170.00")Strategy 2: Dollar-Cost Averaging
Spread purchases over time to reduce volatility impact.
defmodule Strategy.DCA do
def schedule(symbol, total_amount, num_buys) do
per_buy = Decimal.div(Decimal.new(total_amount), Decimal.new(num_buys))
Enum.map(1..num_buys, fn i ->
# Each buy is a notional (dollar) amount
{:ok, order} = Alpa.Trading.Orders.place(
symbol: symbol,
notional: Decimal.to_string(per_buy),
side: "buy",
type: "market",
time_in_force: "day"
)
IO.puts("Buy #{i}/#{num_buys}: Order #{order.id} for $#{per_buy}")
# In production, you'd schedule these across days/weeks
Process.sleep(1000)
order
end)
end
end
# Usage:
# Strategy.DCA.schedule("AAPL", "10000", 10)Strategy 3: Mean Reversion
Buy when price drops below moving average, sell when above.
defmodule Strategy.MeanReversion do
def analyze(symbol, opts \\ []) do
period = Keyword.get(opts, :period, 20)
threshold = Keyword.get(opts, :threshold, Decimal.new("0.02"))
# Fetch historical bars
{:ok, bars} = Alpa.MarketData.Bars.get(symbol,
timeframe: "1Day",
limit: period + 5
)
# Calculate simple moving average
closes = Enum.map(bars, & &1.close)
sma = calculate_sma(closes, period)
# Get current price
{:ok, quote} = Alpa.MarketData.Quotes.latest(symbol)
current = quote.ask_price
# Compare to SMA
deviation = Decimal.div(Decimal.sub(current, sma), sma)
cond do
Decimal.compare(deviation, Decimal.negate(threshold)) == :lt ->
{:buy, symbol, deviation}
Decimal.compare(deviation, threshold) == :gt ->
{:sell, symbol, deviation}
true ->
{:hold, symbol, deviation}
end
end
defp calculate_sma(prices, period) do
prices
|> Enum.take(period)
|> Enum.reduce(Decimal.new(0), &Decimal.add/2)
|> Decimal.div(Decimal.new(period))
end
endStrategy 4: Momentum with Streaming
Use real-time data to detect momentum shifts.
defmodule Strategy.Momentum do
use GenServer
def start_link(symbols, opts \\ []) do
GenServer.start_link(__MODULE__, {symbols, opts}, name: __MODULE__)
end
def init({symbols, _opts}) do
# Start market data stream
{:ok, stream_pid} = Alpa.Stream.MarketData.start_link(
callback: fn event -> GenServer.cast(__MODULE__, {:market_event, event}) end,
feed: "iex"
)
# Subscribe to trades for our symbols
Alpa.Stream.MarketData.subscribe(stream_pid, trades: symbols)
state = %{
stream_pid: stream_pid,
symbols: symbols,
price_history: %{},
positions: %{}
}
{:ok, state}
end
def handle_cast({:market_event, %{type: :trade, data: trade}}, state) do
symbol = trade.symbol
price = trade.price
# Track last N prices
history = Map.get(state.price_history, symbol, [])
history = [price | Enum.take(history, 19)]
# Check momentum (5-period vs 20-period)
if length(history) >= 20 do
short_avg = avg(Enum.take(history, 5))
long_avg = avg(history)
if Decimal.compare(short_avg, long_avg) == :gt do
# Upward momentum - consider buying
IO.puts("[Momentum] #{symbol}: bullish crossover #{short_avg} > #{long_avg}")
end
end
{:noreply, put_in(state, [:price_history, symbol], history)}
end
defp avg(prices) do
sum = Enum.reduce(prices, Decimal.new(0), &Decimal.add/2)
Decimal.div(sum, Decimal.new(length(prices)))
end
endStrategy 5: Crypto Arbitrage Monitor
Monitor crypto prices across timeframes.
defmodule Strategy.CryptoMonitor do
def scan(symbols \\ ["BTC/USD", "ETH/USD", "SOL/USD"]) do
# Get snapshots for all symbols at once
{:ok, snapshots} = Alpa.Crypto.MarketData.snapshots(symbols)
Enum.each(snapshots, fn {symbol, snapshot} ->
daily_bar = snapshot.daily_bar
latest = snapshot.latest_trade
if daily_bar && latest do
open = daily_bar.open
current = latest.price
change_pct = Decimal.mult(
Decimal.div(Decimal.sub(current, open), open),
Decimal.new(100)
)
IO.puts("#{symbol}: $#{current} (#{change_pct}% today)")
end
end)
end
endPortfolio Management
Rebalancing
defmodule Portfolio.Rebalancer do
@target_allocations %{
"AAPL" => Decimal.new("0.30"),
"MSFT" => Decimal.new("0.25"),
"GOOGL" => Decimal.new("0.20"),
"AMZN" => Decimal.new("0.15"),
"BTC/USD" => Decimal.new("0.10")
}
def check_drift do
{:ok, account} = Alpa.Trading.Account.get()
{:ok, positions} = Alpa.Trading.Positions.list()
portfolio_value = account.portfolio_value
Enum.map(positions, fn pos ->
target = Map.get(@target_allocations, pos.symbol, Decimal.new(0))
actual = Decimal.div(pos.market_value, portfolio_value)
drift = Decimal.sub(actual, target)
%{
symbol: pos.symbol,
target: target,
actual: actual,
drift: drift,
action: if(Decimal.compare(drift, Decimal.new("0.05")) == :gt, do: :sell,
else: if(Decimal.compare(drift, Decimal.new("-0.05")) == :lt, do: :buy, else: :hold))
}
end)
end
endRisk Management
defmodule Portfolio.Risk do
def check do
{:ok, account} = Alpa.Trading.Account.get()
{:ok, positions} = Alpa.Trading.Positions.list()
total_exposure = Enum.reduce(positions, Decimal.new(0), fn pos, acc ->
Decimal.add(acc, Decimal.abs(pos.market_value))
end)
concentration = Enum.map(positions, fn pos ->
weight = Decimal.div(Decimal.abs(pos.market_value), total_exposure)
{pos.symbol, weight}
end)
|> Enum.sort_by(fn {_, w} -> Decimal.to_float(w) end, :desc)
%{
equity: account.equity,
buying_power: account.buying_power,
total_exposure: total_exposure,
leverage: Decimal.div(total_exposure, account.equity),
top_holdings: Enum.take(concentration, 5),
day_pl: account.equity |> Decimal.sub(account.last_equity)
}
end
endMonitoring with Telemetry
# Attach telemetry handlers for trade monitoring
:telemetry.attach_many("trading-monitor", [
[:alpa, :request, :stop],
[:alpa, :request, :exception]
], fn
[:alpa, :request, :stop], %{duration: duration}, %{method: method, path: path}, _config ->
ms = System.convert_time_unit(duration, :native, :millisecond)
Logger.info("[API] #{method} #{path} completed in #{ms}ms")
[:alpa, :request, :exception], %{duration: duration}, %{error: error}, _config ->
ms = System.convert_time_unit(duration, :native, :millisecond)
Logger.error("[API] Request failed after #{ms}ms: #{inspect(error)}")
end, nil)Next Steps
- [ ] Add backtesting framework using historical bars
- [ ] Implement options strategies (covered calls, spreads)
- [ ] Add alerting via webhook/SMS when signals trigger
- [ ] Build LiveView dashboard for real-time monitoring
- [ ] Add position sizing based on Kelly criterion