Files
gocryptotrader/exchanges/order/orders.go
Scott 50bbdabf43 BugFix: RPCServer cannot retrieve open orders/getOrder due to unset asset type (#634)
* Fixes issue where getorders could not work due to unset asset type in rpcserver.go. Adds test. Also adds start and end date to the cli.

* A few fixes

* lint

* fixes oopsie that affected doopsie

* Ensures dates are set for all open order implementations. Adds new filter to ensure orders without dates are returned rather than filtered. Fixes up Binance OpenOrders implementation. Adds some extra typeconverts for binance

* Add updated time to Binance GetActiveOrders. Update rpcserver.go to only set the time if its not empty. Also addressed bad expected value

* Actually fixes things this time

* Improves recvWindow to process openOrders

* Adds asset type to getOrder as well

* Fixes tests

* Adds missing date fields

* Fixes default time, updates default errors

* Default start to last month, instead of last year
2021-02-25 17:13:21 +11:00

798 lines
19 KiB
Go

package order
import (
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/validate"
)
// Validate checks the supplied data and returns whether or not it's valid
func (s *Submit) Validate(opt ...validate.Checker) error {
if s == nil {
return ErrSubmissionIsNil
}
if s.Pair.IsEmpty() {
return ErrPairIsEmpty
}
if s.AssetType == "" {
return ErrAssetNotSet
}
if s.Side != Buy &&
s.Side != Sell &&
s.Side != Bid &&
s.Side != Ask {
return ErrSideIsInvalid
}
if s.Type != Market && s.Type != Limit {
return ErrTypeIsInvalid
}
if s.Amount <= 0 {
return ErrAmountIsInvalid
}
if s.Type == Limit && s.Price <= 0 {
return ErrPriceMustBeSetIfLimitOrder
}
var errs common.Errors
for _, o := range opt {
err := o.Check()
if err != nil {
errs = append(errs, err)
}
}
if errs != nil {
return errs
}
return nil
}
// UpdateOrderFromDetail Will update an order detail (used in order management)
// by comparing passed in and existing values
func (d *Detail) UpdateOrderFromDetail(m *Detail) {
var updated bool
if d.ImmediateOrCancel != m.ImmediateOrCancel {
d.ImmediateOrCancel = m.ImmediateOrCancel
updated = true
}
if d.HiddenOrder != m.HiddenOrder {
d.HiddenOrder = m.HiddenOrder
updated = true
}
if d.FillOrKill != m.FillOrKill {
d.FillOrKill = m.FillOrKill
updated = true
}
if m.Price > 0 && m.Price != d.Price {
d.Price = m.Price
updated = true
}
if m.Amount > 0 && m.Amount != d.Amount {
d.Amount = m.Amount
updated = true
}
if m.LimitPriceUpper > 0 && m.LimitPriceUpper != d.LimitPriceUpper {
d.LimitPriceUpper = m.LimitPriceUpper
updated = true
}
if m.LimitPriceLower > 0 && m.LimitPriceLower != d.LimitPriceLower {
d.LimitPriceLower = m.LimitPriceLower
updated = true
}
if m.TriggerPrice > 0 && m.TriggerPrice != d.TriggerPrice {
d.TriggerPrice = m.TriggerPrice
updated = true
}
if m.TargetAmount > 0 && m.TargetAmount != d.TargetAmount {
d.TargetAmount = m.TargetAmount
updated = true
}
if m.ExecutedAmount > 0 && m.ExecutedAmount != d.ExecutedAmount {
d.ExecutedAmount = m.ExecutedAmount
updated = true
}
if m.Fee > 0 && m.Fee != d.Fee {
d.Fee = m.Fee
updated = true
}
if m.AccountID != "" && m.AccountID != d.AccountID {
d.AccountID = m.AccountID
updated = true
}
if m.PostOnly != d.PostOnly {
d.PostOnly = m.PostOnly
updated = true
}
if !m.Pair.IsEmpty() && m.Pair != d.Pair {
d.Pair = m.Pair
updated = true
}
if m.Leverage != 0 && m.Leverage != d.Leverage {
d.Leverage = m.Leverage
updated = true
}
if m.ClientID != "" && m.ClientID != d.ClientID {
d.ClientID = m.ClientID
updated = true
}
if m.WalletAddress != "" && m.WalletAddress != d.WalletAddress {
d.WalletAddress = m.WalletAddress
updated = true
}
if m.Type != "" && m.Type != d.Type {
d.Type = m.Type
updated = true
}
if m.Side != "" && m.Side != d.Side {
d.Side = m.Side
updated = true
}
if m.Status != "" && m.Status != d.Status {
d.Status = m.Status
updated = true
}
if m.AssetType != "" && m.AssetType != d.AssetType {
d.AssetType = m.AssetType
updated = true
}
if m.Trades != nil {
for x := range m.Trades {
var found bool
for y := range d.Trades {
if d.Trades[y].TID != m.Trades[x].TID {
continue
}
found = true
if d.Trades[y].Fee != m.Trades[x].Fee {
d.Trades[y].Fee = m.Trades[x].Fee
updated = true
}
if m.Trades[x].Price != 0 && d.Trades[y].Price != m.Trades[x].Price {
d.Trades[y].Price = m.Trades[x].Price
updated = true
}
if d.Trades[y].Side != m.Trades[x].Side {
d.Trades[y].Side = m.Trades[x].Side
updated = true
}
if d.Trades[y].Type != m.Trades[x].Type {
d.Trades[y].Type = m.Trades[x].Type
updated = true
}
if d.Trades[y].Description != m.Trades[x].Description {
d.Trades[y].Description = m.Trades[x].Description
updated = true
}
if m.Trades[x].Amount != 0 && d.Trades[y].Amount != m.Trades[x].Amount {
d.Trades[y].Amount = m.Trades[x].Amount
updated = true
}
if d.Trades[y].Timestamp != m.Trades[x].Timestamp {
d.Trades[y].Timestamp = m.Trades[x].Timestamp
updated = true
}
if d.Trades[y].IsMaker != m.Trades[x].IsMaker {
d.Trades[y].IsMaker = m.Trades[x].IsMaker
updated = true
}
}
if !found {
d.Trades = append(d.Trades, m.Trades[x])
updated = true
}
m.RemainingAmount -= m.Trades[x].Amount
}
}
if m.RemainingAmount > 0 && m.RemainingAmount != d.RemainingAmount {
d.RemainingAmount = m.RemainingAmount
updated = true
}
if updated {
if d.LastUpdated == m.LastUpdated {
d.LastUpdated = time.Now()
} else {
d.LastUpdated = m.LastUpdated
}
}
}
// UpdateOrderFromModify Will update an order detail (used in order management)
// by comparing passed in and existing values
func (d *Detail) UpdateOrderFromModify(m *Modify) {
var updated bool
if d.ImmediateOrCancel != m.ImmediateOrCancel {
d.ImmediateOrCancel = m.ImmediateOrCancel
updated = true
}
if d.HiddenOrder != m.HiddenOrder {
d.HiddenOrder = m.HiddenOrder
updated = true
}
if d.FillOrKill != m.FillOrKill {
d.FillOrKill = m.FillOrKill
updated = true
}
if m.Price > 0 && m.Price != d.Price {
d.Price = m.Price
updated = true
}
if m.Amount > 0 && m.Amount != d.Amount {
d.Amount = m.Amount
updated = true
}
if m.LimitPriceUpper > 0 && m.LimitPriceUpper != d.LimitPriceUpper {
d.LimitPriceUpper = m.LimitPriceUpper
updated = true
}
if m.LimitPriceLower > 0 && m.LimitPriceLower != d.LimitPriceLower {
d.LimitPriceLower = m.LimitPriceLower
updated = true
}
if m.TriggerPrice > 0 && m.TriggerPrice != d.TriggerPrice {
d.TriggerPrice = m.TriggerPrice
updated = true
}
if m.TargetAmount > 0 && m.TargetAmount != d.TargetAmount {
d.TargetAmount = m.TargetAmount
updated = true
}
if m.ExecutedAmount > 0 && m.ExecutedAmount != d.ExecutedAmount {
d.ExecutedAmount = m.ExecutedAmount
updated = true
}
if m.Fee > 0 && m.Fee != d.Fee {
d.Fee = m.Fee
updated = true
}
if m.AccountID != "" && m.AccountID != d.AccountID {
d.AccountID = m.AccountID
updated = true
}
if m.PostOnly != d.PostOnly {
d.PostOnly = m.PostOnly
updated = true
}
if !m.Pair.IsEmpty() && m.Pair != d.Pair {
d.Pair = m.Pair
updated = true
}
if m.Leverage != 0 && m.Leverage != d.Leverage {
d.Leverage = m.Leverage
updated = true
}
if m.ClientID != "" && m.ClientID != d.ClientID {
d.ClientID = m.ClientID
updated = true
}
if m.WalletAddress != "" && m.WalletAddress != d.WalletAddress {
d.WalletAddress = m.WalletAddress
updated = true
}
if m.Type != "" && m.Type != d.Type {
d.Type = m.Type
updated = true
}
if m.Side != "" && m.Side != d.Side {
d.Side = m.Side
updated = true
}
if m.Status != "" && m.Status != d.Status {
d.Status = m.Status
updated = true
}
if m.AssetType != "" && m.AssetType != d.AssetType {
d.AssetType = m.AssetType
updated = true
}
if m.Trades != nil {
for x := range m.Trades {
var found bool
for y := range d.Trades {
if d.Trades[y].TID != m.Trades[x].TID {
continue
}
found = true
if d.Trades[y].Fee != m.Trades[x].Fee {
d.Trades[y].Fee = m.Trades[x].Fee
updated = true
}
if m.Trades[x].Price != 0 && d.Trades[y].Price != m.Trades[x].Price {
d.Trades[y].Price = m.Trades[x].Price
updated = true
}
if d.Trades[y].Side != m.Trades[x].Side {
d.Trades[y].Side = m.Trades[x].Side
updated = true
}
if d.Trades[y].Type != m.Trades[x].Type {
d.Trades[y].Type = m.Trades[x].Type
updated = true
}
if d.Trades[y].Description != m.Trades[x].Description {
d.Trades[y].Description = m.Trades[x].Description
updated = true
}
if m.Trades[x].Amount != 0 && d.Trades[y].Amount != m.Trades[x].Amount {
d.Trades[y].Amount = m.Trades[x].Amount
updated = true
}
if d.Trades[y].Timestamp != m.Trades[x].Timestamp {
d.Trades[y].Timestamp = m.Trades[x].Timestamp
updated = true
}
if d.Trades[y].IsMaker != m.Trades[x].IsMaker {
d.Trades[y].IsMaker = m.Trades[x].IsMaker
updated = true
}
}
if !found {
d.Trades = append(d.Trades, m.Trades[x])
updated = true
}
m.RemainingAmount -= m.Trades[x].Amount
}
}
if m.RemainingAmount > 0 && m.RemainingAmount != d.RemainingAmount {
d.RemainingAmount = m.RemainingAmount
updated = true
}
if updated {
if d.LastUpdated == m.LastUpdated {
d.LastUpdated = time.Now()
} else {
d.LastUpdated = m.LastUpdated
}
}
}
// String implements the stringer interface
func (t Type) String() string {
return string(t)
}
// Lower returns the type lower case string
func (t Type) Lower() string {
return strings.ToLower(string(t))
}
// Title returns the type titleized, eg "Limit"
func (t Type) Title() string {
return strings.Title(strings.ToLower(string(t)))
}
// String implements the stringer interface
func (s Side) String() string {
return string(s)
}
// Lower returns the side lower case string
func (s Side) Lower() string {
return strings.ToLower(string(s))
}
// Title returns the side titleized, eg "Buy"
func (s Side) Title() string {
return strings.Title(strings.ToLower(string(s)))
}
// String implements the stringer interface
func (s Status) String() string {
return string(s)
}
// FilterOrdersBySide removes any order details that don't match the
// order status provided
func FilterOrdersBySide(orders *[]Detail, side Side) {
if side == "" || side == AnySide {
return
}
var filteredOrders []Detail
for i := range *orders {
if strings.EqualFold(string((*orders)[i].Side), string(side)) {
filteredOrders = append(filteredOrders, (*orders)[i])
}
}
*orders = filteredOrders
}
// FilterOrdersByType removes any order details that don't match the order type
// provided
func FilterOrdersByType(orders *[]Detail, orderType Type) {
if orderType == "" || orderType == AnyType {
return
}
var filteredOrders []Detail
for i := range *orders {
if strings.EqualFold(string((*orders)[i].Type), string(orderType)) {
filteredOrders = append(filteredOrders, (*orders)[i])
}
}
*orders = filteredOrders
}
// FilterOrdersByTimeRange removes any OrderDetails outside of the time range
func FilterOrdersByTimeRange(orders *[]Detail, startTime, endTime time.Time) {
if startTime.IsZero() ||
endTime.IsZero() ||
startTime.Unix() == 0 ||
endTime.Unix() == 0 ||
endTime.Before(startTime) {
return
}
var filteredOrders []Detail
for i := range *orders {
if ((*orders)[i].Date.Unix() >= startTime.Unix() && (*orders)[i].Date.Unix() <= endTime.Unix()) ||
(*orders)[i].Date.IsZero() {
filteredOrders = append(filteredOrders, (*orders)[i])
}
}
*orders = filteredOrders
}
// FilterOrdersByCurrencies removes any order details that do not match the
// provided currency list. It is forgiving in that the provided currencies can
// match quote or base currencies
func FilterOrdersByCurrencies(orders *[]Detail, currencies []currency.Pair) {
if len(currencies) == 0 {
return
}
if len(currencies) == 1 && currencies[0].IsEmpty() {
return
}
var filteredOrders []Detail
for i := range *orders {
matchFound := false
for _, c := range currencies {
if !matchFound && (*orders)[i].Pair.EqualIncludeReciprocal(c) {
matchFound = true
}
}
if matchFound {
filteredOrders = append(filteredOrders, (*orders)[i])
}
}
*orders = filteredOrders
}
func (b ByPrice) Len() int {
return len(b)
}
func (b ByPrice) Less(i, j int) bool {
return b[i].Price < b[j].Price
}
func (b ByPrice) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
// SortOrdersByPrice the caller function to sort orders
func SortOrdersByPrice(orders *[]Detail, reverse bool) {
if reverse {
sort.Sort(sort.Reverse(ByPrice(*orders)))
} else {
sort.Sort(ByPrice(*orders))
}
}
func (b ByOrderType) Len() int {
return len(b)
}
func (b ByOrderType) Less(i, j int) bool {
return b[i].Type.String() < b[j].Type.String()
}
func (b ByOrderType) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
// SortOrdersByType the caller function to sort orders
func SortOrdersByType(orders *[]Detail, reverse bool) {
if reverse {
sort.Sort(sort.Reverse(ByOrderType(*orders)))
} else {
sort.Sort(ByOrderType(*orders))
}
}
func (b ByCurrency) Len() int {
return len(b)
}
func (b ByCurrency) Less(i, j int) bool {
return b[i].Pair.String() < b[j].Pair.String()
}
func (b ByCurrency) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
// SortOrdersByCurrency the caller function to sort orders
func SortOrdersByCurrency(orders *[]Detail, reverse bool) {
if reverse {
sort.Sort(sort.Reverse(ByCurrency(*orders)))
} else {
sort.Sort(ByCurrency(*orders))
}
}
func (b ByDate) Len() int {
return len(b)
}
func (b ByDate) Less(i, j int) bool {
return b[i].Date.Unix() < b[j].Date.Unix()
}
func (b ByDate) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
// SortOrdersByDate the caller function to sort orders
func SortOrdersByDate(orders *[]Detail, reverse bool) {
if reverse {
sort.Sort(sort.Reverse(ByDate(*orders)))
} else {
sort.Sort(ByDate(*orders))
}
}
func (b ByOrderSide) Len() int {
return len(b)
}
func (b ByOrderSide) Less(i, j int) bool {
return b[i].Side.String() < b[j].Side.String()
}
func (b ByOrderSide) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
// SortOrdersBySide the caller function to sort orders
func SortOrdersBySide(orders *[]Detail, reverse bool) {
if reverse {
sort.Sort(sort.Reverse(ByOrderSide(*orders)))
} else {
sort.Sort(ByOrderSide(*orders))
}
}
// StringToOrderSide for converting case insensitive order side
// and returning a real Side
func StringToOrderSide(side string) (Side, error) {
switch {
case strings.EqualFold(side, Buy.String()):
return Buy, nil
case strings.EqualFold(side, Sell.String()):
return Sell, nil
case strings.EqualFold(side, Bid.String()):
return Bid, nil
case strings.EqualFold(side, Ask.String()):
return Ask, nil
case strings.EqualFold(side, AnySide.String()):
return AnySide, nil
default:
return UnknownSide, errors.New(side + " not recognised as order side")
}
}
// StringToOrderType for converting case insensitive order type
// and returning a real Type
func StringToOrderType(oType string) (Type, error) {
switch {
case strings.EqualFold(oType, Limit.String()),
strings.EqualFold(oType, "EXCHANGE LIMIT"):
return Limit, nil
case strings.EqualFold(oType, Market.String()),
strings.EqualFold(oType, "EXCHANGE MARKET"):
return Market, nil
case strings.EqualFold(oType, ImmediateOrCancel.String()),
strings.EqualFold(oType, "immediate or cancel"),
strings.EqualFold(oType, "IOC"),
strings.EqualFold(oType, "EXCHANGE IOC"):
return ImmediateOrCancel, nil
case strings.EqualFold(oType, Stop.String()),
strings.EqualFold(oType, "stop loss"),
strings.EqualFold(oType, "stop_loss"),
strings.EqualFold(oType, "EXCHANGE STOP"):
return Stop, nil
case strings.EqualFold(oType, StopLimit.String()),
strings.EqualFold(oType, "EXCHANGE STOP LIMIT"):
return StopLimit, nil
case strings.EqualFold(oType, TrailingStop.String()),
strings.EqualFold(oType, "trailing stop"),
strings.EqualFold(oType, "EXCHANGE TRAILING STOP"):
return TrailingStop, nil
case strings.EqualFold(oType, FillOrKill.String()),
strings.EqualFold(oType, "EXCHANGE FOK"):
return FillOrKill, nil
case strings.EqualFold(oType, IOS.String()):
return IOS, nil
case strings.EqualFold(oType, PostOnly.String()):
return PostOnly, nil
case strings.EqualFold(oType, AnyType.String()):
return AnyType, nil
case strings.EqualFold(oType, Trigger.String()):
return Trigger, nil
default:
return UnknownType, errors.New(oType + " not recognised as order type")
}
}
// StringToOrderStatus for converting case insensitive order status
// and returning a real Status
func StringToOrderStatus(status string) (Status, error) {
switch {
case strings.EqualFold(status, AnyStatus.String()):
return AnyStatus, nil
case strings.EqualFold(status, New.String()),
strings.EqualFold(status, "placed"):
return New, nil
case strings.EqualFold(status, Active.String()):
return Active, nil
case strings.EqualFold(status, PartiallyFilled.String()),
strings.EqualFold(status, "partially matched"),
strings.EqualFold(status, "partially filled"):
return PartiallyFilled, nil
case strings.EqualFold(status, Filled.String()),
strings.EqualFold(status, "fully matched"),
strings.EqualFold(status, "fully filled"):
return Filled, nil
case strings.EqualFold(status, PartiallyCancelled.String()),
strings.EqualFold(status, "partially cancelled"):
return PartiallyCancelled, nil
case strings.EqualFold(status, Open.String()):
return Open, nil
case strings.EqualFold(status, Closed.String()):
return Closed, nil
case strings.EqualFold(status, Cancelled.String()):
return Cancelled, nil
case strings.EqualFold(status, "CANCELED"): // Kraken case
return Cancelled, nil
case strings.EqualFold(status, PendingCancel.String()),
strings.EqualFold(status, "pending cancel"),
strings.EqualFold(status, "pending cancellation"):
return PendingCancel, nil
case strings.EqualFold(status, Rejected.String()):
return Rejected, nil
case strings.EqualFold(status, Expired.String()):
return Expired, nil
case strings.EqualFold(status, Hidden.String()):
return Hidden, nil
case strings.EqualFold(status, InsufficientBalance.String()):
return InsufficientBalance, nil
case strings.EqualFold(status, MarketUnavailable.String()):
return MarketUnavailable, nil
default:
return UnknownStatus, errors.New(status + " not recognised as order status")
}
}
func (o *ClassificationError) Error() string {
if o.OrderID != "" {
return fmt.Sprintf("%s - OrderID: %s classification error: %v",
o.Exchange,
o.OrderID,
o.Err)
}
return fmt.Sprintf("%s - classification error: %v",
o.Exchange,
o.Err)
}
// StandardCancel defines an option in the validator to make sure an ID is set
// for a standard cancel
func (c *Cancel) StandardCancel() validate.Checker {
return validate.Check(func() error {
if c.ID == "" {
return errors.New("ID not set")
}
return nil
})
}
// Validate checks internal struct requirements
func (c *Cancel) Validate(opt ...validate.Checker) error {
if c == nil {
return ErrCancelOrderIsNil
}
if c.Pair.IsEmpty() {
return ErrPairIsEmpty
}
if c.AssetType == "" {
return ErrAssetNotSet
}
var errs common.Errors
for _, o := range opt {
err := o.Check()
if err != nil {
errs = append(errs, err)
}
}
if errs != nil {
return errs
}
return nil
}
// Validate checks internal struct requirements
func (g *GetOrdersRequest) Validate(opt ...validate.Checker) error {
if g == nil {
return ErrGetOrdersRequestIsNil
}
if !g.AssetType.IsValid() {
return fmt.Errorf("assetType %v not supported", g.AssetType)
}
var errs common.Errors
for _, o := range opt {
err := o.Check()
if err != nil {
errs = append(errs, err)
}
}
if errs != nil {
return errs
}
return nil
}
// Validate checks internal struct requirements
func (m *Modify) Validate(opt ...validate.Checker) error {
if m == nil {
return ErrModifyOrderIsNil
}
if m.Pair.IsEmpty() {
return ErrPairIsEmpty
}
if m.AssetType.String() == "" {
return ErrAssetNotSet
}
var errs common.Errors
for _, o := range opt {
err := o.Check()
if err != nil {
errs = append(errs, err)
}
}
if errs != nil {
return errs
}
if m.ClientOrderID == "" && m.ID == "" {
return ErrOrderIDNotSet
}
return nil
}