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