alpaca_exec.go raw
1 package market
2
3 import (
4 "fmt"
5 "time"
6
7 "github.com/alpacahq/alpaca-trade-api-go/v3/alpaca"
8 "github.com/shopspring/decimal"
9 )
10
11 // AlpacaExecutor bridges the organism's Executor with Alpaca's trading API.
12 // It provides the Execute callback that the Executor calls for live trades,
13 // and reads back positions/fills from the exchange.
14 type AlpacaExecutor struct {
15 client *alpaca.Client
16 Paper bool
17 Verbose bool
18 }
19
20 // NewAlpacaExecutor creates an executor connected to Alpaca's trading API.
21 // Set paper=true for paper trading (https://paper-api.alpaca.markets).
22 func NewAlpacaExecutor(apiKey, apiSecret string, paper bool) *AlpacaExecutor {
23 baseURL := "https://api.alpaca.markets"
24 if paper {
25 baseURL = "https://paper-api.alpaca.markets"
26 }
27
28 client := alpaca.NewClient(alpaca.ClientOpts{
29 APIKey: apiKey,
30 APISecret: apiSecret,
31 BaseURL: baseURL,
32 })
33
34 return &AlpacaExecutor{
35 client: client,
36 Paper: paper,
37 }
38 }
39
40 // ExecuteOrder sends a trade order to Alpaca and returns the fill.
41 // This is the callback function that plugs into Executor.Execute.
42 func (ae *AlpacaExecutor) ExecuteOrder(order TradeOrder) (*Fill, error) {
43 side := alpaca.Buy
44 if order.Side == Sell {
45 side = alpaca.Sell
46 }
47
48 qty := decimal.NewFromFloat(order.Quantity)
49
50 var orderType alpaca.OrderType
51 var limitPrice *decimal.Decimal
52 if order.Price == 0 {
53 orderType = alpaca.Market
54 } else {
55 orderType = alpaca.Limit
56 lp := decimal.NewFromFloat(order.Price)
57 limitPrice = &lp
58 }
59
60 req := alpaca.PlaceOrderRequest{
61 Symbol: order.Asset,
62 Qty: &qty,
63 Side: side,
64 Type: orderType,
65 TimeInForce: alpaca.GTC,
66 LimitPrice: limitPrice,
67 }
68
69 alpacaOrder, err := ae.client.PlaceOrder(req)
70 if err != nil {
71 return nil, fmt.Errorf("alpaca place order: %w", err)
72 }
73
74 // Convert Alpaca order to our Fill type.
75 fillPrice := order.Price
76 if alpacaOrder.FilledAvgPrice != nil {
77 fillPrice, _ = alpacaOrder.FilledAvgPrice.Float64()
78 }
79
80 fillQty := order.Quantity
81 fq, _ := alpacaOrder.FilledQty.Float64()
82 if fq > 0 {
83 fillQty = fq
84 }
85
86 return &Fill{
87 OrderID: alpacaOrder.ID,
88 Asset: order.Asset,
89 Side: order.Side,
90 Venue: Venue("alpaca"),
91 Price: fillPrice,
92 Quantity: fillQty,
93 Fee: 0, // Alpaca is commission-free
94 FilledAt: time.Now(),
95 }, nil
96 }
97
98 // WireExecutor connects an AlpacaExecutor to a market Executor,
99 // setting the Execute callback for live trading.
100 func (ae *AlpacaExecutor) WireExecutor(ex *Executor) {
101 ex.Execute = ae.ExecuteOrder
102 if ae.Paper {
103 ex.Mode = PaperTrading // paper trades via Alpaca's paper API
104 } else {
105 ex.Mode = LiveTrading
106 }
107 }
108