diff --git a/engine/order_manager.go b/engine/order_manager.go index 443cb96a..0864fe98 100644 --- a/engine/order_manager.go +++ b/engine/order_manager.go @@ -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, diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index eb3da595..f6b18f44 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -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, diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index affe115e..0ab80ca6 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -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, diff --git a/exchanges/btcmarkets/btcmarkets_test.go b/exchanges/btcmarkets/btcmarkets_test.go index 80521ad7..f937c12a 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -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) + } +} diff --git a/exchanges/btcmarkets/btcmarkets_types.go b/exchanges/btcmarkets/btcmarkets_types.go index e3df79fd..deccaf78 100644 --- a/exchanges/btcmarkets/btcmarkets_types.go +++ b/exchanges/btcmarkets/btcmarkets_types.go @@ -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 diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 1246957a..b6140f2d 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -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 } diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index b7ef4dfe..e25cd54f 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -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 { diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 5ef124db..b08217aa 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -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 diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 5bfeabe2..faae18ea 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -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 { diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 3a37aab7..91dc8bf8 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -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 { diff --git a/gctscript/wrappers/gct/exchange/exchange_test.go b/gctscript/wrappers/gct/exchange/exchange_test.go index 13b51be1..3ed96604 100644 --- a/gctscript/wrappers/gct/exchange/exchange_test.go +++ b/gctscript/wrappers/gct/exchange/exchange_test.go @@ -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 { diff --git a/gctscript/wrappers/validator/validator_test.go b/gctscript/wrappers/validator/validator_test.go index 76a28eec..a9722fbe 100644 --- a/gctscript/wrappers/validator/validator_test.go +++ b/gctscript/wrappers/validator/validator_test.go @@ -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 {