diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index a89c72ded..519e0d123 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -126,15 +126,19 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) { // PlaceOrder returns the created order object, executed trade (if any) and error func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *types.Trade, error) { + if o.Type == types.OrderTypeMarket { + if m.LastPrice.IsZero() { + panic("unexpected error: for market order, the last price can not be zero") + } + } + + isTaker := o.Type == types.OrderTypeMarket || isLimitTakerOrder(o, m.LastPrice) + // price for checking account balance, default price price := o.Price switch o.Type { case types.OrderTypeMarket: - if m.LastPrice.IsZero() { - panic("unexpected: last price can not be zero") - } - price = m.LastPrice case types.OrderTypeLimit, types.OrderTypeStopLimit, types.OrderTypeLimitMaker: price = o.Price @@ -167,7 +171,7 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty orderID := incOrderID() order := m.newOrder(o, orderID) - if o.Type == types.OrderTypeMarket { + if isTaker { // emit the order update for Status:New m.EmitOrderUpdate(order) @@ -175,13 +179,12 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty var order2 = order // emit trade before we publish order - trade := m.newTradeFromOrder(&order2, false) + trade := m.newTradeFromOrder(&order2, false, m.LastPrice) m.executeTrade(trade) // update the order status order2.Status = types.OrderStatusFilled order2.ExecutedQuantity = order2.Quantity - order2.Price = price order2.IsWorking = false // let the exchange emit the "FILLED" order update (we need the closed order) @@ -240,26 +243,21 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) { m.EmitBalanceUpdate(m.Account.Balances()) } -func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool) types.Trade { +func (m *SimplePriceMatching) getFeeRate(isMaker bool) (feeRate fixedpoint.Value) { // BINANCE uses 0.1% for both maker and taker // MAX uses 0.050% for maker and 0.15% for taker - var feeRate fixedpoint.Value if isMaker { feeRate = m.Account.MakerFeeRate } else { feeRate = m.Account.TakerFeeRate } + return feeRate +} - price := order.Price - switch order.Type { - case types.OrderTypeMarket, types.OrderTypeStopMarket: - if m.LastPrice.IsZero() { - panic("unexpected: last price can not be zero") - } - - price = m.LastPrice - } - +func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool, price fixedpoint.Value) types.Trade { + // BINANCE uses 0.1% for both maker and taker + // MAX uses 0.050% for maker and 0.15% for taker + var feeRate = m.getFeeRate(isMaker) var quoteQuantity = order.Quantity.Mul(price) var fee fixedpoint.Value var feeCurrency string @@ -268,17 +266,7 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool feeCurrency = FeeToken fee = quoteQuantity.Mul(feeRate) } else { - switch order.Side { - - case types.SideTypeBuy: - fee = order.Quantity.Mul(feeRate) - feeCurrency = m.Market.BaseCurrency - - case types.SideTypeSell: - fee = quoteQuantity.Mul(feeRate) - feeCurrency = m.Market.QuoteCurrency - - } + fee, feeCurrency = calculateNativeOrderFee(order, m.Market, feeRate) } // update order time @@ -288,7 +276,7 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool return types.Trade{ ID: id, OrderID: order.OrderID, - Exchange: "backtest", + Exchange: types.ExchangeBacktest, Price: price, Quantity: order.Quantity, QuoteQuantity: quoteQuantity, @@ -302,8 +290,8 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool } } -// BuyToPrice means price go up and the limit sell should be triggered -func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { +// buyToPrice means price go up and the limit sell should be triggered +func (m *SimplePriceMatching) buyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { klineMatchingLogger.Debugf("kline buy to price %s", price.String()) var bidOrders []types.Order @@ -418,7 +406,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ for i := range closedOrders { o := closedOrders[i] - trade := m.newTradeFromOrder(&o, true) + trade := m.newTradeFromOrder(&o, true, o.Price) m.executeTrade(trade) closedOrders[i] = o @@ -432,9 +420,9 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ return closedOrders, trades } -// SellToPrice simulates the price trend in down direction. +// sellToPrice simulates the price trend in down direction. // When price goes down, buy orders should be executed, and the stop orders should be triggered. -func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { +func (m *SimplePriceMatching) sellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { klineMatchingLogger.Debugf("kline sell to price %s", price.String()) // in this section we handle --- the price goes lower, and we trigger the stop sell @@ -488,8 +476,10 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders switch o.Type { case types.OrderTypeStopMarket: - // should we trigger the order - if o.StopPrice.Compare(price) < 0 { + // price goes down and if the stop price is still lower than the current price + // or the stop price is not touched + // then we should skip this order + if price.Compare(o.StopPrice) > 0 { bidOrders = append(bidOrders, o) break } @@ -501,9 +491,10 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders closedOrders = append(closedOrders, o) case types.OrderTypeStopLimit: - // if the price is lower than the stop order price - // we should trigger the stop order - if o.StopPrice.Compare(price) < 0 { + // price goes down and if the stop price is still lower than the current price + // or the stop price is not touched + // then we should skip this order + if price.Compare(o.StopPrice) > 0 { bidOrders = append(bidOrders, o) break } @@ -539,7 +530,7 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders for i := range closedOrders { o := closedOrders[i] - trade := m.newTradeFromOrder(&o, true) + trade := m.newTradeFromOrder(&o, true, o.Price) m.executeTrade(trade) closedOrders[i] = o @@ -579,40 +570,40 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) { m.LastPrice = kline.Open } else { if m.LastPrice.Compare(kline.Open) > 0 { - m.SellToPrice(kline.Open) + m.sellToPrice(kline.Open) } else { - m.BuyToPrice(kline.Open) + m.buyToPrice(kline.Open) } } switch kline.Direction() { case types.DirectionDown: if kline.High.Compare(kline.Open) >= 0 { - m.BuyToPrice(kline.High) + m.buyToPrice(kline.High) } // if low is lower than close, sell to low first, and then buy up to close if kline.Low.Compare(kline.Close) < 0 { - m.SellToPrice(kline.Low) - m.BuyToPrice(kline.Close) + m.sellToPrice(kline.Low) + m.buyToPrice(kline.Close) } else { - m.SellToPrice(kline.Close) + m.sellToPrice(kline.Close) } case types.DirectionUp: if kline.Low.Compare(kline.Open) <= 0 { - m.SellToPrice(kline.Low) + m.sellToPrice(kline.Low) } if kline.High.Compare(kline.Close) > 0 { - m.BuyToPrice(kline.High) - m.SellToPrice(kline.Close) + m.buyToPrice(kline.High) + m.sellToPrice(kline.Close) } else { - m.BuyToPrice(kline.Close) + m.buyToPrice(kline.Close) } default: // no trade up or down if m.LastPrice.IsZero() { - m.BuyToPrice(kline.Close) + m.buyToPrice(kline.Close) } } @@ -631,3 +622,28 @@ func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) type UpdateTime: types.Time(m.CurrentTime), } } + +func calculateNativeOrderFee(order *types.Order, market types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) { + switch order.Side { + + case types.SideTypeBuy: + fee = order.Quantity.Mul(feeRate) + feeCurrency = market.BaseCurrency + + case types.SideTypeSell: + quoteQuantity := order.Quantity.Mul(order.Price) + fee = quoteQuantity.Mul(feeRate) + feeCurrency = market.QuoteCurrency + + } + return fee, feeCurrency +} + +func isLimitTakerOrder(o types.SubmitOrder, currentPrice fixedpoint.Value) bool { + if currentPrice.IsZero() { + return false + } + + return o.Type == types.OrderTypeLimit && ((o.Side == types.SideTypeBuy && o.Price.Compare(currentPrice) >= 0) || + (o.Side == types.SideTypeSell && o.Price.Compare(currentPrice) <= 0)) +} diff --git a/pkg/backtest/matching_test.go b/pkg/backtest/matching_test.go index 2c642c9f6..05ec31c3f 100644 --- a/pkg/backtest/matching_test.go +++ b/pkg/backtest/matching_test.go @@ -46,6 +46,7 @@ func TestSimplePriceMatching_orderUpdate(t *testing.T) { Market: market, CurrentTime: t1, closedOrders: make(map[uint64]types.Order), + LastPrice: fixedpoint.NewFromFloat(25000), } orderUpdateCnt := 0 @@ -66,11 +67,12 @@ func TestSimplePriceMatching_orderUpdate(t *testing.T) { } }) + // maker order _, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 24000.0, 0.1)) assert.NoError(t, err) - assert.Equal(t, 1, orderUpdateCnt) // should got new status - assert.Equal(t, 1, orderUpdateNewStatusCnt) // should got new status - assert.Equal(t, 0, orderUpdateFilledStatusCnt) // should got new status + assert.Equal(t, 1, orderUpdateCnt) // should get new status + assert.Equal(t, 1, orderUpdateNewStatusCnt) // should get new status + assert.Equal(t, 0, orderUpdateFilledStatusCnt) // should get new status assert.Equal(t, types.OrderStatusNew, lastOrder.Status) assert.Equal(t, fixedpoint.NewFromFloat(0.0), lastOrder.ExecutedQuantity) @@ -89,23 +91,8 @@ func TestSimplePriceMatching_orderUpdate(t *testing.T) { } func TestSimplePriceMatching_processKLine(t *testing.T) { - account := &types.Account{ - MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), - TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), - } - account.UpdateBalances(types.BalanceMap{ - "USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(10000.0)}, - }) - market := types.Market{ - Symbol: "BTCUSDT", - PricePrecision: 8, - VolumePrecision: 8, - QuoteCurrency: "USDT", - BaseCurrency: "BTC", - MinNotional: fixedpoint.MustNewFromString("0.001"), - MinAmount: fixedpoint.MustNewFromString("10.0"), - MinQuantity: fixedpoint.MustNewFromString("0.001"), - } + account := getTestAccount() + market := getTestMarket() t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC) engine := &SimplePriceMatching{ @@ -113,6 +100,7 @@ func TestSimplePriceMatching_processKLine(t *testing.T) { Market: market, CurrentTime: t1, closedOrders: make(map[uint64]types.Order), + LastPrice: fixedpoint.NewFromFloat(30000.0), } for i := 0; i <= 5; i++ { @@ -124,7 +112,7 @@ func TestSimplePriceMatching_processKLine(t *testing.T) { t2 := t1.Add(time.Minute) // should match 25000, 24000 - k := newKLine("BTCUSDT", types.Interval1m, t2, 26000, 27000, 23000, 25000) + k := newKLine("BTCUSDT", types.Interval1m, t2, 30000, 27000, 23000, 25000) assert.Equal(t, t2.Add(time.Minute-time.Millisecond), k.EndTime.Time()) engine.processKLine(k) @@ -151,6 +139,8 @@ func newKLine(symbol string, interval types.Interval, startTime time.Time, o, h, } } +// getTestMarket returns the BTCUSDT market information +// for tests, we always use BTCUSDT func getTestMarket() types.Market { market := types.Market{ Symbol: "BTCUSDT", @@ -209,13 +199,13 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) { assert.Equal(t, 2, len(engine.bidOrders)) assert.Equal(t, 1, len(engine.askOrders)) - closedOrders, trades := engine.BuyToPrice(fixedpoint.NewFromFloat(20000.0)) + closedOrders, trades := engine.buyToPrice(fixedpoint.NewFromFloat(20000.0)) assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy") assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy") assert.Equal(t, 2, len(engine.bidOrders), "bid orders should be the same") assert.Equal(t, 1, len(engine.askOrders), "ask orders should be the same") - closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(21001.0)) + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21001.0)) assert.Len(t, closedOrders, 1, "should trigger the stop buy order") assert.Len(t, trades, 1, "should have stop order trade executed") @@ -241,7 +231,7 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) { assert.NotNil(t, createdOrder, "place stop order should not trigger the stop buy") assert.Len(t, engine.bidOrders, 2) - closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(20500.0)) + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20500.0)) assert.Len(t, closedOrders, 1, "should trigger the stop buy order") assert.Len(t, trades, 1, "should have stop order trade executed") assert.Len(t, engine.bidOrders, 1, "should left one bid order") @@ -279,13 +269,13 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) { assert.Equal(t, 1, len(engine.bidOrders)) assert.Equal(t, 2, len(engine.askOrders)) - closedOrders, trades := engine.SellToPrice(fixedpoint.NewFromFloat(21500.0)) + closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0)) assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy") assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy") assert.Equal(t, 1, len(engine.bidOrders)) assert.Equal(t, 2, len(engine.askOrders)) - closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(20990.0)) + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0)) assert.Len(t, closedOrders, 1, "should trigger the stop sell order") assert.Len(t, trades, 1, "should have stop order trade executed") assert.Equal(t, 1, len(engine.bidOrders)) @@ -313,7 +303,7 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) { assert.Nil(t, trade, "place stop order should not trigger the stop sell") assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell") - closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(21000.0)) + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21000.0)) if assert.Len(t, closedOrders, 1, "should trigger the stop sell order") { assert.Len(t, trades, 1, "should have stop order trade executed") assert.Equal(t, types.SideTypeSell, closedOrders[0].Side) @@ -348,11 +338,11 @@ func TestSimplePriceMatching_StopMarketOrderSell(t *testing.T) { assert.Nil(t, trade, "place stop order should not trigger the stop sell") assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell") - closedOrders, trades := engine.SellToPrice(fixedpoint.NewFromFloat(21500.0)) + closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0)) assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy") assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy") - closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(20990.0)) + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0)) assert.Len(t, closedOrders, 1, "should trigger the stop sell order") assert.Len(t, trades, 1, "should have stop order trade executed") @@ -384,11 +374,11 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) { assert.Len(t, engine.bidOrders, 5) assert.Len(t, engine.askOrders, 5) - closedOrders, trades := engine.SellToPrice(fixedpoint.NewFromFloat(8100.0)) + closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(8100.0)) assert.Len(t, closedOrders, 0) assert.Len(t, trades, 0) - closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(8000.0)) + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(8000.0)) assert.Len(t, closedOrders, 1) assert.Len(t, trades, 1) for _, trade := range trades { @@ -399,15 +389,15 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) { assert.Equal(t, types.SideTypeBuy, o.Side) } - closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(7000.0)) + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(7000.0)) assert.Len(t, closedOrders, 4) assert.Len(t, trades, 4) - closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(8900.0)) + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(8900.0)) assert.Len(t, closedOrders, 0) assert.Len(t, trades, 0) - closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(9000.0)) + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9000.0)) assert.Len(t, closedOrders, 1) assert.Len(t, trades, 1) for _, o := range closedOrders { @@ -417,7 +407,76 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) { assert.Equal(t, types.SideTypeSell, trade.Side) } - closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(9500.0)) + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9500.0)) assert.Len(t, closedOrders, 4) assert.Len(t, trades, 4) } + +func Test_calculateNativeOrderFee(t *testing.T) { + market := getTestMarket() + + t.Run("sellOrder", func(t *testing.T) { + order := types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + TimeInForce: types.TimeInForceGTC, + }, + } + feeRate := fixedpoint.MustNewFromString("0.075%") + fee, feeCurrency := calculateNativeOrderFee(&order, market, feeRate) + assert.Equal(t, "1.5", fee.String()) + assert.Equal(t, "USDT", feeCurrency) + }) + + t.Run("buyOrder", func(t *testing.T) { + order := types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + TimeInForce: types.TimeInForceGTC, + }, + } + + feeRate := fixedpoint.MustNewFromString("0.075%") + fee, feeCurrency := calculateNativeOrderFee(&order, market, feeRate) + assert.Equal(t, "0.000075", fee.String()) + assert.Equal(t, "BTC", feeCurrency) + }) +} + +func TestSimplePriceMatching_LimitTakerOrder(t *testing.T) { + account := getTestAccount() + market := getTestMarket() + engine := &SimplePriceMatching{ + Account: account, + Market: market, + closedOrders: make(map[uint64]types.Order), + LastPrice: fixedpoint.NewFromFloat(20000.0), + } + + closedOrder, trade, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 21000.0, 1.0)) + assert.NoError(t, err) + if assert.NotNil(t, closedOrder) { + if assert.NotNil(t, trade) { + assert.Equal(t, "20000", trade.Price.String()) + assert.False(t, trade.IsMaker, "should be taker") + } + } + + closedOrder, trade, err = engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 19000.0, 1.0)) + assert.NoError(t, err) + if assert.NotNil(t, closedOrder) { + assert.Equal(t, "19000", closedOrder.Price.String()) + if assert.NotNil(t, trade) { + assert.Equal(t, "20000", trade.Price.String()) + assert.False(t, trade.IsMaker, "should be taker") + } + } +} diff --git a/pkg/types/order.go b/pkg/types/order.go index 993cd9a5a..dc4fced21 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -116,9 +116,13 @@ type SubmitOrder struct { Side SideType `json:"side" db:"side"` Type OrderType `json:"orderType" db:"order_type"` - Quantity fixedpoint.Value `json:"quantity" db:"quantity"` - Price fixedpoint.Value `json:"price" db:"price"` - StopPrice fixedpoint.Value `json:"stopPrice,omitempty" db:"stop_price"` + Quantity fixedpoint.Value `json:"quantity" db:"quantity"` + Price fixedpoint.Value `json:"price" db:"price"` + + // AveragePrice is only used in back-test currently + AveragePrice fixedpoint.Value `json:"averagePrice"` + + StopPrice fixedpoint.Value `json:"stopPrice,omitempty" db:"stop_price"` Market Market `json:"-" db:"-"`