package market import ( "fmt" "time" "github.com/alpacahq/alpaca-trade-api-go/v3/alpaca" "github.com/shopspring/decimal" ) // AlpacaExecutor bridges the organism's Executor with Alpaca's trading API. // It provides the Execute callback that the Executor calls for live trades, // and reads back positions/fills from the exchange. type AlpacaExecutor struct { client *alpaca.Client Paper bool Verbose bool } // NewAlpacaExecutor creates an executor connected to Alpaca's trading API. // Set paper=true for paper trading (https://paper-api.alpaca.markets). func NewAlpacaExecutor(apiKey, apiSecret string, paper bool) *AlpacaExecutor { baseURL := "https://api.alpaca.markets" if paper { baseURL = "https://paper-api.alpaca.markets" } client := alpaca.NewClient(alpaca.ClientOpts{ APIKey: apiKey, APISecret: apiSecret, BaseURL: baseURL, }) return &AlpacaExecutor{ client: client, Paper: paper, } } // ExecuteOrder sends a trade order to Alpaca and returns the fill. // This is the callback function that plugs into Executor.Execute. func (ae *AlpacaExecutor) ExecuteOrder(order TradeOrder) (*Fill, error) { side := alpaca.Buy if order.Side == Sell { side = alpaca.Sell } qty := decimal.NewFromFloat(order.Quantity) var orderType alpaca.OrderType var limitPrice *decimal.Decimal if order.Price == 0 { orderType = alpaca.Market } else { orderType = alpaca.Limit lp := decimal.NewFromFloat(order.Price) limitPrice = &lp } req := alpaca.PlaceOrderRequest{ Symbol: order.Asset, Qty: &qty, Side: side, Type: orderType, TimeInForce: alpaca.GTC, LimitPrice: limitPrice, } alpacaOrder, err := ae.client.PlaceOrder(req) if err != nil { return nil, fmt.Errorf("alpaca place order: %w", err) } // Convert Alpaca order to our Fill type. fillPrice := order.Price if alpacaOrder.FilledAvgPrice != nil { fillPrice, _ = alpacaOrder.FilledAvgPrice.Float64() } fillQty := order.Quantity fq, _ := alpacaOrder.FilledQty.Float64() if fq > 0 { fillQty = fq } return &Fill{ OrderID: alpacaOrder.ID, Asset: order.Asset, Side: order.Side, Venue: Venue("alpaca"), Price: fillPrice, Quantity: fillQty, Fee: 0, // Alpaca is commission-free FilledAt: time.Now(), }, nil } // WireExecutor connects an AlpacaExecutor to a market Executor, // setting the Execute callback for live trading. func (ae *AlpacaExecutor) WireExecutor(ex *Executor) { ex.Execute = ae.ExecuteOrder if ae.Paper { ex.Mode = PaperTrading // paper trades via Alpaca's paper API } else { ex.Mode = LiveTrading } }