btcmarkets: update order submission (#908)

* btcm/order: update order submission

* btcmarkets: addr nits

* btcmarkets: reinstate new order test
This commit is contained in:
Ryan O'Hara-Reid
2022-03-25 15:05:24 +11:00
committed by GitHub
parent 489e2ebade
commit 022001fecf
12 changed files with 321 additions and 97 deletions

View File

@@ -572,13 +572,14 @@ func (m *OrderManager) processSubmittedOrder(newOrder *order.Submit, result orde
if newOrder.Date.IsZero() {
newOrder.Date = time.Now()
}
msg := fmt.Sprintf("Order manager: Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v side=%v type=%v for time %v.",
msg := fmt.Sprintf("Order manager: Exchange %s submitted order ID=%v [Ours: %v] pair=%v price=%v amount=%v quoteAmount=%v side=%v type=%v for time %v.",
newOrder.Exchange,
result.OrderID,
id.String(),
newOrder.Pair,
newOrder.Price,
newOrder.Amount,
newOrder.QuoteAmount,
newOrder.Side,
newOrder.Type,
newOrder.Date)
@@ -602,7 +603,7 @@ func (m *OrderManager) processSubmittedOrder(newOrder *order.Submit, result orde
LimitPriceUpper: newOrder.LimitPriceUpper,
LimitPriceLower: newOrder.LimitPriceLower,
TriggerPrice: newOrder.TriggerPrice,
TargetAmount: newOrder.TargetAmount,
QuoteAmount: newOrder.QuoteAmount,
ExecutedAmount: newOrder.ExecutedAmount,
RemainingAmount: newOrder.RemainingAmount,
Fee: newOrder.Fee,

View File

@@ -2610,7 +2610,7 @@ func TestWsOrderExecutionReport(t *testing.T) {
Price: 52789.1,
Amount: 0.00028400,
AverageExecutedPrice: 0,
TargetAmount: 0,
QuoteAmount: 0,
ExecutedAmount: 0,
RemainingAmount: 0.00028400,
Cost: 0,

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
@@ -16,6 +17,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
@@ -57,13 +59,23 @@ const (
orderPlaced = "Placed"
orderAccepted = "Accepted"
ask = "ask"
ask = "ask"
// order types
limit = "Limit"
market = "Market"
stopLimit = "Stop Limit"
stop = "Stop"
takeProfit = "Take Profit"
// order sides
askSide = "Ask"
bidSide = "Bid"
// time in force
immediateOrCancel = "IOC"
fillOrKill = "FOK"
subscribe = "subscribe"
fundChange = "fundChange"
orderChange = "orderChange"
@@ -314,13 +326,56 @@ func (b *BTCMarkets) GetTradeByID(ctx context.Context, id string) (TradeHistoryD
request.Auth)
}
// formatOrderType conforms order type to the exchange acceptable order type
// strings
func (b *BTCMarkets) formatOrderType(o order.Type) (string, error) {
switch o {
case order.Limit:
return limit, nil
case order.Market:
return market, nil
case order.StopLimit:
return stopLimit, nil
case order.Stop:
return stop, nil
case order.TakeProfit:
return takeProfit, nil
default:
return "", fmt.Errorf("%s %s %w", b.Name, o, order.ErrTypeIsInvalid)
}
}
// formatOrderSide conforms order side to the exchange acceptable order side
// strings
func (b *BTCMarkets) formatOrderSide(o order.Side) (string, error) {
switch o {
case order.Ask:
return askSide, nil
case order.Bid:
return bidSide, nil
default:
return "", fmt.Errorf("%s %s %w", b.Name, o, order.ErrSideIsInvalid)
}
}
// getTimeInForce returns a string depending on the options in order.Submit
func (b *BTCMarkets) getTimeInForce(s *order.Submit) string {
if s.ImmediateOrCancel {
return immediateOrCancel
}
if s.FillOrKill {
return fillOrKill
}
return "" // GTC (good till cancelled, default value)
}
// NewOrder requests a new order and returns an ID
func (b *BTCMarkets) NewOrder(ctx context.Context, marketID string, price, amount float64, orderType, side string, triggerPrice,
targetAmount float64, timeInForce string, postOnly bool, selfTrade, clientOrderID string) (OrderData, error) {
var resp OrderData
func (b *BTCMarkets) NewOrder(ctx context.Context, price, amount, triggerPrice, targetAmount float64, marketID, orderType, side, timeInForce, selfTrade, clientOrderID string, postOnly bool) (OrderData, error) {
req := make(map[string]interface{})
req["marketId"] = marketID
req["price"] = strconv.FormatFloat(price, 'f', -1, 64)
if price != 0 {
req["price"] = strconv.FormatFloat(price, 'f', -1, 64)
}
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
req["type"] = orderType
req["side"] = side
@@ -333,13 +388,16 @@ func (b *BTCMarkets) NewOrder(ctx context.Context, marketID string, price, amoun
if timeInForce != "" {
req["timeInForce"] = timeInForce
}
req["postOnly"] = postOnly
if postOnly {
req["postOnly"] = postOnly
}
if selfTrade != "" {
req["selfTrade"] = selfTrade
}
if clientOrderID != "" {
req["clientOrderID"] = clientOrderID
}
var resp OrderData
return resp, b.SendAuthenticatedRequest(ctx, http.MethodPost,
btcMarketsOrders,
req,

View File

@@ -202,25 +202,57 @@ func TestGetTradeByID(t *testing.T) {
}
}
func TestNewOrder(t *testing.T) {
func TestSubmitOrder(t *testing.T) {
t.Parallel()
_, err := b.SubmitOrder(context.Background(), &order.Submit{
Price: 100,
Amount: 1,
Type: order.TrailingStop,
AssetType: asset.Spot,
Side: order.Bid,
Pair: currency.NewPair(currency.BTC, currency.AUD),
PostOnly: true,
})
if !errors.Is(err, order.ErrTypeIsInvalid) {
t.Fatalf("received: '%v' but expected: '%v'", err, order.ErrTypeIsInvalid)
}
_, err = b.SubmitOrder(context.Background(), &order.Submit{
Price: 100,
Amount: 1,
Type: order.Limit,
AssetType: asset.Spot,
Side: order.AnySide,
Pair: currency.NewPair(currency.BTC, currency.AUD),
PostOnly: true,
})
if !errors.Is(err, order.ErrSideIsInvalid) {
t.Fatalf("received: '%v' but expected: '%v'", err, order.ErrSideIsInvalid)
}
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly")
}
_, err := b.NewOrder(context.Background(),
BTCAUD, 100, 1, limit, bid, 0, 0, "", true, "", "")
_, err = b.SubmitOrder(context.Background(), &order.Submit{
Price: 100,
Amount: 1,
Type: order.Limit,
AssetType: asset.Spot,
Side: order.Bid,
Pair: currency.NewPair(currency.BTC, currency.AUD),
PostOnly: true,
})
if err != nil {
t.Error(err)
}
_, err = b.NewOrder(context.Background(),
BTCAUD, 100, 1, "invalid", bid, 0, 0, "", true, "", "")
if err == nil {
t.Error("expected an error due to invalid ordertype")
}
func TestNewOrder(t *testing.T) {
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly")
}
_, err = b.NewOrder(context.Background(),
BTCAUD, 100, 1, limit, "invalid", 0, 0, "", true, "", "")
if err == nil {
t.Error("expected an error due to invalid orderside")
_, err := b.NewOrder(context.Background(), 100, 1, 0, 0, BTCAUD, limit, bidSide, "", "", "", true)
if err != nil {
t.Error(err)
}
}
@@ -899,3 +931,100 @@ func TestTrim(t *testing.T) {
})
}
}
func TestFormatOrderType(t *testing.T) {
t.Parallel()
_, err := b.formatOrderType(order.Type("SWOOON"))
if !errors.Is(err, order.ErrTypeIsInvalid) {
t.Fatalf("received: '%v' but expected: '%v'", err, order.ErrTypeIsInvalid)
}
r, err := b.formatOrderType(order.Limit)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if r != limit {
t.Fatal("unexpected value")
}
r, err = b.formatOrderType(order.Market)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if r != market {
t.Fatal("unexpected value")
}
r, err = b.formatOrderType(order.StopLimit)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if r != stopLimit {
t.Fatal("unexpected value")
}
r, err = b.formatOrderType(order.Stop)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if r != stop {
t.Fatal("unexpected value")
}
r, err = b.formatOrderType(order.TakeProfit)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if r != takeProfit {
t.Fatal("unexpected value")
}
}
func TestFormatOrderSide(t *testing.T) {
t.Parallel()
_, err := b.formatOrderSide("invalid")
if !errors.Is(err, order.ErrSideIsInvalid) {
t.Fatalf("received: '%v' but expected: '%v'", err, order.ErrSideIsInvalid)
}
f, err := b.formatOrderSide(order.Bid)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if f != bidSide {
t.Fatal("unexpected value")
}
f, err = b.formatOrderSide(order.Ask)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if f != askSide {
t.Fatal("unexpected value")
}
}
func TestGetTimeInForce(t *testing.T) {
t.Parallel()
f := b.getTimeInForce(&order.Submit{})
if f != "" {
t.Fatal("unexpected value")
}
f = b.getTimeInForce(&order.Submit{ImmediateOrCancel: true})
if f != immediateOrCancel {
t.Fatalf("received: '%v' but expected: '%v'", f, immediateOrCancel)
}
f = b.getTimeInForce(&order.Submit{FillOrKill: true})
if f != fillOrKill {
t.Fatalf("received: '%v' but expected: '%v'", f, fillOrKill)
}
}

View File

@@ -156,6 +156,8 @@ type OrderData struct {
Amount float64 `json:"amount,string"`
OpenAmount float64 `json:"openAmount,string"`
Status string `json:"status"`
TargetAmount float64 `json:"targetAmount,string"`
TimeInForce string `json:"timeInForce"`
}
// CancelOrderResp stores data for cancelled orders

View File

@@ -423,16 +423,14 @@ func (b *BTCMarkets) UpdateAccountInfo(ctx context.Context, assetType asset.Item
return resp, err
}
var acc account.SubAccount
for key := range data {
c := currency.NewCode(data[key].AssetName)
hold := data[key].Locked
total := data[key].Balance
acc.Currencies = append(acc.Currencies,
account.Balance{CurrencyName: c,
Total: total,
Hold: hold,
Free: total - hold,
})
acc.AssetType = assetType
for x := range data {
acc.Currencies = append(acc.Currencies, account.Balance{
CurrencyName: currency.NewCode(data[x].AssetName),
Total: data[x].Balance,
Hold: data[x].Locked,
Free: data[x].Available,
})
}
resp.Accounts = append(resp.Accounts, acc)
resp.Exchange = b.Name
@@ -532,18 +530,28 @@ func (b *BTCMarkets) SubmitOrder(ctx context.Context, s *order.Submit) (order.Su
return resp, err
}
fOrderType, err := b.formatOrderType(s.Type)
if err != nil {
return resp, err
}
fOrderSide, err := b.formatOrderSide(s.Side)
if err != nil {
return resp, err
}
tempResp, err := b.NewOrder(ctx,
fpair.String(),
s.Price,
s.Amount,
s.Type.String(),
s.Side.String(),
s.TriggerPrice,
s.TargetAmount,
s.QuoteAmount,
fpair.String(),
fOrderType,
fOrderSide,
b.getTimeInForce(s),
"",
false,
"",
s.ClientID)
s.ClientID,
s.PostOnly)
if err != nil {
return resp, err
}

View File

@@ -40,6 +40,17 @@ func TestValidate(t *testing.T) {
ExpectedErr: ErrSideIsInvalid,
Submit: &Submit{Pair: testPair, AssetType: asset.Spot},
}, // valid pair but invalid order side
{
ExpectedErr: errTimeInForceConflict,
Submit: &Submit{
Pair: testPair,
AssetType: asset.Spot,
Side: Ask,
Type: Market,
ImmediateOrCancel: true,
FillOrKill: true,
},
},
{
ExpectedErr: ErrTypeIsInvalid,
Submit: &Submit{Pair: testPair,
@@ -703,7 +714,7 @@ func TestUpdateOrderFromModify(t *testing.T) {
LimitPriceUpper: 0,
LimitPriceLower: 0,
TriggerPrice: 0,
TargetAmount: 0,
QuoteAmount: 0,
ExecutedAmount: 0,
RemainingAmount: 0,
Fee: 0,
@@ -739,7 +750,7 @@ func TestUpdateOrderFromModify(t *testing.T) {
LimitPriceUpper: 1,
LimitPriceLower: 1,
TriggerPrice: 1,
TargetAmount: 1,
QuoteAmount: 1,
ExecutedAmount: 1,
RemainingAmount: 1,
Fee: 1,
@@ -792,7 +803,7 @@ func TestUpdateOrderFromModify(t *testing.T) {
if od.TriggerPrice != 1 {
t.Error("Failed to update")
}
if od.TargetAmount != 1 {
if od.QuoteAmount != 1 {
t.Error("Failed to update")
}
if od.ExecutedAmount != 1 {
@@ -895,7 +906,7 @@ func TestUpdateOrderFromDetail(t *testing.T) {
LimitPriceUpper: 0,
LimitPriceLower: 0,
TriggerPrice: 0,
TargetAmount: 0,
QuoteAmount: 0,
ExecutedAmount: 0,
RemainingAmount: 0,
Fee: 0,
@@ -931,7 +942,7 @@ func TestUpdateOrderFromDetail(t *testing.T) {
LimitPriceUpper: 1,
LimitPriceLower: 1,
TriggerPrice: 1,
TargetAmount: 1,
QuoteAmount: 1,
ExecutedAmount: 1,
RemainingAmount: 1,
Fee: 1,
@@ -984,7 +995,7 @@ func TestUpdateOrderFromDetail(t *testing.T) {
if od.TriggerPrice != 1 {
t.Error("Failed to update")
}
if od.TargetAmount != 1 {
if od.QuoteAmount != 1 {
t.Error("Failed to update")
}
if od.ExecutedAmount != 1 {

View File

@@ -35,31 +35,36 @@ type Submit struct {
ReduceOnly bool
Leverage float64
Price float64
Amount float64
StopPrice float64
LimitPriceUpper float64
LimitPriceLower float64
TriggerPrice float64
TargetAmount float64
ExecutedAmount float64
RemainingAmount float64
Fee float64
Exchange string
InternalOrderID string
ID string
AccountID string
ClientID string
ClientOrderID string
WalletAddress string
Offset string
Type Type
Side Side
Status Status
AssetType asset.Item
Date time.Time
LastUpdated time.Time
Pair currency.Pair
Trades []TradeHistory
// Amount in base terms
Amount float64
// QuoteAmount is the max amount in quote currency when purchasing base.
// This is only used in Market orders.
QuoteAmount float64
StopPrice float64
LimitPriceUpper float64
LimitPriceLower float64
TriggerPrice float64
ExecutedAmount float64
RemainingAmount float64
Fee float64
Exchange string
InternalOrderID string
ID string
AccountID string
ClientID string
ClientOrderID string
WalletAddress string
Offset string
Type Type
Side Side
Status Status
AssetType asset.Item
Date time.Time
LastUpdated time.Time
Pair currency.Pair
Trades []TradeHistory
}
// SubmitResponse is what is returned after submitting an order to an exchange
@@ -88,7 +93,7 @@ type Modify struct {
LimitPriceUpper float64
LimitPriceLower float64
TriggerPrice float64
TargetAmount float64
QuoteAmount float64
ExecutedAmount float64
RemainingAmount float64
Fee float64
@@ -129,7 +134,7 @@ type Detail struct {
LimitPriceLower float64
TriggerPrice float64
AverageExecutedPrice float64
TargetAmount float64
QuoteAmount float64
ExecutedAmount float64
RemainingAmount float64
Cost float64

View File

@@ -13,6 +13,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/validate"
)
var errTimeInForceConflict = errors.New("multiple time in force options applied")
// Validate checks the supplied data and returns whether or not it's valid
func (s *Submit) Validate(opt ...validate.Checker) error {
if s == nil {
@@ -38,8 +40,20 @@ func (s *Submit) Validate(opt ...validate.Checker) error {
return ErrTypeIsInvalid
}
if s.Amount <= 0 {
return fmt.Errorf("submit validation error %w, suppled: %.8f", ErrAmountIsInvalid, s.Amount)
if s.ImmediateOrCancel && s.FillOrKill {
return errTimeInForceConflict
}
if s.Amount == 0 && s.QuoteAmount == 0 {
return fmt.Errorf("submit validation error %w, amount and quote amount cannot be zero", ErrAmountIsInvalid)
}
if s.Amount < 0 {
return fmt.Errorf("submit validation error base %w, suppled: %v", ErrAmountIsInvalid, s.Amount)
}
if s.QuoteAmount < 0 {
return fmt.Errorf("submit validation error quote %w, suppled: %v", ErrAmountIsInvalid, s.QuoteAmount)
}
if s.Type == Limit && s.Price <= 0 {
@@ -92,8 +106,8 @@ func (d *Detail) UpdateOrderFromDetail(m *Detail) {
d.TriggerPrice = m.TriggerPrice
updated = true
}
if m.TargetAmount > 0 && m.TargetAmount != d.TargetAmount {
d.TargetAmount = m.TargetAmount
if m.QuoteAmount > 0 && m.QuoteAmount != d.QuoteAmount {
d.QuoteAmount = m.QuoteAmount
updated = true
}
if m.ExecutedAmount > 0 && m.ExecutedAmount != d.ExecutedAmount {
@@ -256,8 +270,8 @@ func (d *Detail) UpdateOrderFromModify(m *Modify) {
d.TriggerPrice = m.TriggerPrice
updated = true
}
if m.TargetAmount > 0 && m.TargetAmount != d.TargetAmount {
d.TargetAmount = m.TargetAmount
if m.QuoteAmount > 0 && m.QuoteAmount != d.QuoteAmount {
d.QuoteAmount = m.QuoteAmount
updated = true
}
if m.ExecutedAmount > 0 && m.ExecutedAmount != d.ExecutedAmount {

View File

@@ -688,7 +688,7 @@ func (p *Poloniex) GetOrderInfo(ctx context.Context, orderID string, pair curren
orderInfo.Amount = resp.Amount
orderInfo.Cost = resp.Total
orderInfo.Fee = resp.Fee
orderInfo.TargetAmount = resp.StartingAmount
orderInfo.QuoteAmount = resp.StartingAmount
orderInfo.Side, err = order.StringToOrderSide(resp.Type)
if err != nil {

View File

@@ -157,16 +157,14 @@ func TestExchange_SubmitOrder(t *testing.T) {
t.Fatal(err)
}
tempOrder := &order.Submit{
Pair: c,
Type: orderType,
Side: orderSide,
TriggerPrice: 0,
TargetAmount: 0,
Price: orderPrice,
Amount: orderAmount,
ClientID: orderClientID,
Exchange: exchName,
AssetType: asset.Spot,
Pair: c,
Type: orderType,
Side: orderSide,
Price: orderPrice,
Amount: orderAmount,
ClientID: orderClientID,
Exchange: exchName,
AssetType: asset.Spot,
}
_, err = exchangeTest.SubmitOrder(context.Background(), tempOrder)
if err != nil {

View File

@@ -182,16 +182,14 @@ func TestWrapper_SubmitOrder(t *testing.T) {
t.Fatal(err)
}
tempOrder := &order.Submit{
Pair: c,
Type: orderType,
Side: orderSide,
TriggerPrice: 0,
TargetAmount: 0,
Price: orderPrice,
Amount: orderAmount,
ClientID: orderClientID,
Exchange: "true",
AssetType: asset.Spot,
Pair: c,
Type: orderType,
Side: orderSide,
Price: orderPrice,
Amount: orderAmount,
ClientID: orderClientID,
Exchange: "true",
AssetType: asset.Spot,
}
_, err = testWrapper.SubmitOrder(context.Background(), tempOrder)
if err != nil {