The original version of this post called functions like alpaca.PlaceOrder, alpaca.GetQuote, and alpaca.ListTrades as package-level functions. None of these exist in the current SDK. The real library uses a client struct. This post uses the actual github.com/alpacahq/alpaca-trade-api-go/v3/alpaca package.
Always start with paper trading. The paper environment (https://paper-api.alpaca.markets) uses real market data with simulated money. Test your strategy thoroughly before switching to https://api.alpaca.markets.
Setup: Account and Client Initialization#
Sign up at alpaca.markets, go to Paper Trading, and generate API keys. The paper and live environments use separate key pairs.
package main
import (
"fmt"
"log"
"os"
"github.com/alpacahq/alpaca-trade-api-go/v3/alpaca"
)
func newClient() *alpaca.Client {
return alpaca.NewClient(alpaca.ClientOpts{
APIKey: os.Getenv("APCA_API_KEY_ID"),
APISecret: os.Getenv("APCA_API_SECRET_KEY"),
BaseURL: "https://paper-api.alpaca.markets",
})
}
func printAccount(client *alpaca.Client) error {
account, err := client.GetAccount()
if err != nil {
return fmt.Errorf("get account: %w", err)
}
fmt.Printf("equity: $%s\n", account.Equity)
fmt.Printf("buying power: $%s\n", account.BuyingPower)
fmt.Printf("cash: $%s\n", account.Cash)
return nil
}
func main() {
client := newClient()
if err := printAccount(client); err != nil {
log.Fatal(err)
}
}Never hardcode API keys. Read them from environment variables or a secrets manager.
Account and Portfolio State#
package main
import (
"fmt"
"strings"
"github.com/alpacahq/alpaca-trade-api-go/v3/alpaca"
)
func printPortfolio(client *alpaca.Client) error {
positions, err := client.GetPositions()
if err != nil {
return fmt.Errorf("get positions: %w", err)
}
if len(positions) == 0 {
fmt.Println("no open positions")
return nil
}
fmt.Printf("%-8s %8s %12s %12s %10s\n", "SYMBOL", "QTY", "ENTRY", "CURRENT", "P&L")
fmt.Println(strings.Repeat("-", 54))
for _, p := range positions {
fmt.Printf(
"%-8s %8s %12s %12s %10s\n",
p.Symbol,
p.Qty,
p.AvgEntryPrice,
p.CurrentPrice,
p.UnrealizedPL,
)
}
return nil
}Market Data: Historical Bars#
The market data client is a separate struct in the marketdata package with its own base URL.
package main
import (
"fmt"
"os"
"time"
"github.com/alpacahq/alpaca-trade-api-go/v3/marketdata"
)
func getHistoricalBars(symbol string, days int) ([]marketdata.Bar, error) {
mdClient := marketdata.NewClient(marketdata.ClientOpts{
APIKey: os.Getenv("APCA_API_KEY_ID"),
APISecret: os.Getenv("APCA_API_SECRET_KEY"),
})
end := time.Now()
start := end.AddDate(0, 0, -days)
bars, err := mdClient.GetBars(symbol, marketdata.GetBarsRequest{
TimeFrame: marketdata.OneDay,
Start: start,
End: end,
})
if err != nil {
return nil, fmt.Errorf("get bars for %s: %w", symbol, err)
}
return bars, nil
}Placing Orders#
package main
import (
"fmt"
"github.com/alpacahq/alpaca-trade-api-go/v3/alpaca"
)
func placeMarketOrder(client *alpaca.Client, symbol string, qty float64, side alpaca.Side) (*alpaca.Order, error) {
order, err := client.PlaceOrder(alpaca.PlaceOrderRequest{
Symbol: symbol,
Qty: alpaca.RoundToFractionOrWhole(qty),
Side: side,
Type: alpaca.Market,
TimeInForce: alpaca.Day,
})
if err != nil {
return nil, fmt.Errorf("place market order %s %s: %w", side, symbol, err)
}
return order, nil
}
func placeLimitOrder(client *alpaca.Client, symbol string, qty, limitPrice float64, side alpaca.Side) (*alpaca.Order, error) {
lp := alpaca.RoundToFractionOrWhole(limitPrice)
order, err := client.PlaceOrder(alpaca.PlaceOrderRequest{
Symbol: symbol,
Qty: alpaca.RoundToFractionOrWhole(qty),
Side: side,
Type: alpaca.Limit,
LimitPrice: &lp,
TimeInForce: alpaca.GTC,
})
if err != nil {
return nil, fmt.Errorf("place limit order %s %s: %w", side, symbol, err)
}
return order, nil
}
func cancelAllOrders(client *alpaca.Client) error {
status := "open"
orders, err := client.GetOrders(alpaca.GetOrdersRequest{
Status: &status,
Limit: 50,
})
if err != nil {
return fmt.Errorf("get orders: %w", err)
}
for _, o := range orders {
if err := client.CancelOrder(o.ID); err != nil {
fmt.Printf("warn: cancel order %s: %v\n", o.ID, err)
}
}
return nil
}A Simple Momentum Strategy#
Moving average crossover: buy when the 5-day SMA crosses above the 20-day SMA, sell when it crosses below. This is a standard signal used to demonstrate a complete strategy loop – test it in paper before drawing any conclusions about its edge.
package main
import (
"fmt"
"log"
"math"
"github.com/alpacahq/alpaca-trade-api-go/v3/alpaca"
"github.com/alpacahq/alpaca-trade-api-go/v3/marketdata"
)
func sma(bars []marketdata.Bar, period int) float64 {
if len(bars) < period {
return 0
}
var sum float64
for _, b := range bars[len(bars)-period:] {
sum += b.Close
}
return sum / float64(period)
}
func positionSize(buyingPower, price, riskFraction float64) float64 {
capital := buyingPower * riskFraction
return capital / price
}
func runMomentumStrategy(client *alpaca.Client, symbol string) error {
bars, err := getHistoricalBars(symbol, 30)
if err != nil {
return err
}
if len(bars) < 20 {
return fmt.Errorf("not enough data: got %d bars, need 20", len(bars))
}
sma5 := sma(bars, 5)
sma20 := sma(bars, 20)
currentPrice := bars[len(bars)-1].Close
log.Printf("%s: price=%.2f sma5=%.2f sma20=%.2f", symbol, currentPrice, sma5, sma20)
account, err := client.GetAccount()
if err != nil {
return fmt.Errorf("get account: %w", err)
}
buyingPower, _ := account.BuyingPower.Float64()
equity, _ := account.Equity.Float64()
lastEquity, _ := account.LastEquity.Float64()
// Drawdown guard: stop trading if down more than 5% from last session
if lastEquity > 0 && (equity/lastEquity-1) < -0.05 {
return fmt.Errorf("drawdown limit hit, skipping trade")
}
positions, _ := client.GetPositions()
hasPosition := false
for _, p := range positions {
if p.Symbol == symbol {
hasPosition = true
break
}
}
switch {
case sma5 > sma20 && !hasPosition:
qty := math.Floor(positionSize(buyingPower, currentPrice, 0.02))
if qty < 1 {
log.Printf("position size too small, skipping")
return nil
}
order, err := placeMarketOrder(client, symbol, qty, alpaca.Buy)
if err != nil {
return err
}
log.Printf("buy order placed: %s", order.ID)
case sma5 < sma20 && hasPosition:
order, err := placeMarketOrder(client, symbol, 0, alpaca.Sell)
if err != nil {
return err
}
log.Printf("sell order placed: %s", order.ID)
default:
log.Println("no signal, holding")
}
return nil
}Risk Management#
Before every trading session, verify the account state and market hours:
package main
import (
"fmt"
"github.com/alpacahq/alpaca-trade-api-go/v3/alpaca"
)
func preTradeChecks(client *alpaca.Client) error {
clock, err := client.GetClock()
if err != nil {
return fmt.Errorf("get clock: %w", err)
}
if !clock.IsOpen {
return fmt.Errorf("market is closed, next open: %s", clock.NextOpen)
}
account, err := client.GetAccount()
if err != nil {
return err
}
if account.TradingBlocked {
return fmt.Errorf("account trading is blocked")
}
return nil
}Always attach a stop-loss order immediately after a buy fills. A day order without a stop can be left open through extended hours, creating losses larger than intended.
Paper Trading Discipline#
The correct sequence before putting real money at risk:
- Run the strategy in paper for at least 30 trading days
- Record every trade: entry price, exit price, size, reason
- Calculate Sharpe ratio and maximum drawdown over the test period
- Validate that the edge holds across different market conditions (trending, ranging, volatile)
- Only then switch to live with a small initial allocation
A strategy that looks good in 5 days of paper trading has not been tested – it has gotten lucky.
Common mistakes and how to avoid them
Skipping paper trading Paper trading is not optional. The paper environment executes against real market data with simulated fills. Run your strategy for at least 30 trading days before committing real capital. Backtesting alone is insufficient – live execution has slippage, partial fills, and latency that backtests hide.
No position sizing Placing a fixed share count regardless of account size is not risk management. Always size positions as a fraction of equity or buying power. 1-2% of buying power per trade is a common starting point.
Ignoring market hours
Orders placed outside market hours queue until the next open by default. A flood of queued orders from an overnight run can trigger unexpected fills at the open. Always check clock.IsOpen before trading.
Not handling partial fills
A market order for 100 shares may fill in three separate lots at slightly different prices. Your order management code must handle partially_filled status and track the average fill price, not just the requested price.
Hardcoding API keys Alpaca API keys give full account access including withdrawals (on some configurations). Store them in environment variables or a secrets manager, never in source code or version control.
If you want to go deeper on any of this, I offer 1:1 coaching sessions for engineers working on AI integration, cloud architecture, and platform engineering. Book a session (50 EUR / 60 min) or reach out at manuel.fedele+website@gmail.com.