Skip to content

Commit

Permalink
Round limit-price to avoid sub-penny issue
Browse files Browse the repository at this point in the history
Fixes #260
  • Loading branch information
gnvk committed Sep 25, 2023
1 parent d9db58f commit b0ce46f
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 30 deletions.
28 changes: 28 additions & 0 deletions alpaca/trading.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package alpaca

import (
"github.com/shopspring/decimal"
)

// RoundLimitPrice calculates the limit price that respects the minimum price variance rule.
//
// Orders received in excess of the minimum price variance will be rejected.
//
// Limit price >= $1.00: Max Decimals = 2
// Limit price < $1.00: Max Decimals = 4
//
// https://docs.alpaca.markets/docs/orders-at-alpaca#sub-penny-increments-for-limit-orders
func RoundLimitPrice(price float64, side Side) *decimal.Decimal {
maxDecimals := int32(2)
if price < 1 {
maxDecimals = 4
}
limitPrice := decimal.NewFromFloat(price)
switch side {
case Buy:
limitPrice = limitPrice.RoundCeil(maxDecimals)
case Sell:
limitPrice = limitPrice.RoundFloor(maxDecimals)
}
return &limitPrice
}
48 changes: 48 additions & 0 deletions alpaca/trading_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package alpaca

import (
"testing"

"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
)

func Test_calculateLimitPrice(t *testing.T) {
tests := []struct {
name string
price float64
side Side
exp decimal.Decimal
}{
{
name: "buy expensive",
price: 41.085,
side: Buy,
exp: decimal.RequireFromString("41.09"),
},
{
name: "buy cheap no rounding",
price: 0.9999,
side: Buy,
exp: decimal.RequireFromString("0.9999"),
},
{
name: "buy cheap rounding",
price: 0.12182,
side: Buy,
exp: decimal.RequireFromString("0.1219"),
},
{
name: "sell expensive",
price: 41.085,
side: Sell,
exp: decimal.RequireFromString("41.08"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := RoundLimitPrice(tt.price, tt.side)
assert.True(t, tt.exp.Equal(*got), "expected: %s, got: %s", tt.exp.String(), got.String())
})
}
}
15 changes: 5 additions & 10 deletions examples/martingale/martingale.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,20 +279,15 @@ func (alp alpacaClientContainer) sendOrder(targetQty int) (string, error) {
fmt.Printf("Selling %d shares.\n", int64(qty))
}

// Follow [L] instructions to use limit orders
if qty > 0 {
// [L] Uncomment line below
limitPrice := decimal.NewFromFloat(alp.lastPrice)

alp.currOrder = randomString()
decimalQty := decimal.NewFromFloat(qty)
alp.client.PlaceOrder(alpaca.PlaceOrderRequest{
Symbol: alp.stock,
Qty: &decimalQty,
Side: side,
Type: alpaca.Limit, // [L] Change to alpaca.Limit
// [L] Uncomment line below
LimitPrice: &limitPrice,
Symbol: alp.stock,
Qty: &decimalQty,
Side: side,
Type: alpaca.Limit,
LimitPrice: alpaca.RoundLimitPrice(alp.lastPrice, side),
TimeInForce: alpaca.Day,
ClientOrderID: alp.currOrder,
})
Expand Down
37 changes: 17 additions & 20 deletions examples/mean-reversion/mean-reversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,26 +253,23 @@ func (alp alpacaClientContainer) rebalance(currPrice, avg float64) error {

// Submit a limit order if quantity is above 0.
func (alp alpacaClientContainer) submitLimitOrder(qty int, symbol string, price float64, side string) error {
if qty > 0 {
adjSide := alpaca.Side(side)
limPrice := decimal.NewFromFloat(price)
decimalQty := decimal.NewFromInt(int64(qty))
order, err := alp.tradeClient.PlaceOrder(alpaca.PlaceOrderRequest{
Symbol: symbol,
Qty: &decimalQty,
Side: adjSide,
Type: "limit",
LimitPrice: &limPrice,
TimeInForce: "day",
})
if err == nil {
fmt.Printf("Limit order of | %d %s %s | sent.\n", qty, symbol, side)
} else {
fmt.Printf("Order of | %d %s %s | did not go through: %v.\n", qty, symbol, side, err)
}
algo.lastOrder = order.ID
return err
if qty <= 0 {
fmt.Printf("Quantity is <= 0, order of | %d %s %s | not sent.\n", qty, symbol, side)
}
adjSide := alpaca.Side(side)
decimalQty := decimal.NewFromInt(int64(qty))
order, err := alp.tradeClient.PlaceOrder(alpaca.PlaceOrderRequest{
Symbol: symbol,
Qty: &decimalQty,
Side: adjSide,
Type: "limit",
LimitPrice: alpaca.RoundLimitPrice(price, adjSide),
TimeInForce: "day",
})
if err != nil {
return fmt.Errorf("qty=%d symbol=%s side=%s: %w", qty, symbol, side, err)
}
fmt.Printf("Quantity is <= 0, order of | %d %s %s | not sent.\n", qty, symbol, side)
fmt.Printf("Limit order of | %d %s %s | sent.\n", qty, symbol, side)
algo.lastOrder = order.ID
return nil
}

0 comments on commit b0ce46f

Please sign in to comment.