mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-15 23:16:48 +00:00
* orderbook/buffer: data integrity and resubscription pass * btcmarkets: REMOVE THAT LIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIINE!!!!!!!!!!!!!!!!! * buffer: reinstate publish, refaactor, invalidate more and comments * buffer/orderbook: improve update and snapshot performance. Move Update type to orderbook package to util. pointer through entire function calls. (cleanup). Change action string to uint8 for easier comparison. Add parsing helper. Update current test benchmark comments. * dispatch: change publish func to variadic id param * dispatch: remove sender receiver wait time as this adds overhead and complexity. update tests. * dispatch: don't create pointers for every job container * rpcserver: fix assertion issues with data publishing change * linter: fixes * glorious: nits addr * depth: change validation handling to incorporate and store err * linter: fix more issues * dispatch: fix race * travis: update before fetching * depth: wrap and return wrapped error in invalidate call and fix tests * btcmarkets: fix commenting * workflow: check * workflow: check * orderbook: check error * buffer/depth: return invalidation error and fix tests * gctcli: display errors on orderbook streams * buffer: remove unused types * orderbook/bitmex: shift function to bitmex * orderbook: Add specific comments to unexported functions that don't have locking require locking. * orderbook: restrict published data functionality to orderbook.Outbound interface * common: add assertion failure helper for error * dispatch: remove atomics, add mutex protection, remove add/remove worker, redo main tests * dispatch: export function * engine: revert and change sub logger to manager * engine: remove old test * dispatch: add common variable ;) * btcmarket: don't overflow int in tests on 32bit systems * ci: force 1.17.7 usage for go * Revert "ci: force 1.17.7 usage for go" This reverts commit af2f95563bf218cf2b9f36a9fcf3258e2c6a2d91. * golangci: bump version add and remove linter items * Revert "golangci: bump version add and remove linter items" This reverts commit 3c98bffc9d030e39faca0387ea40c151df2ab06b. * dispatch: remove unsused mutex from mux * order: slight optimizations * nits: glorious * dispatch: fix regression on uuid generation and input inline with master * linter: fix * linter: fix * glorious: nit - rm slice segration * account: fix test after merge * coinbasepro: revert change * account: close channel instead of needing a receiver, push alert in routine to prepare for waiter. Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
940 lines
23 KiB
Go
940 lines
23 KiB
Go
package order
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"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 {
|
|
return ErrSubmissionIsNil
|
|
}
|
|
|
|
if s.Pair.IsEmpty() {
|
|
return ErrPairIsEmpty
|
|
}
|
|
|
|
if s.AssetType == asset.Empty {
|
|
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.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 {
|
|
return ErrPriceMustBeSetIfLimitOrder
|
|
}
|
|
|
|
for _, o := range opt {
|
|
err := o.Check()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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.QuoteAmount > 0 && m.QuoteAmount != d.QuoteAmount {
|
|
d.QuoteAmount = m.QuoteAmount
|
|
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.Equal(d.Pair) {
|
|
// TODO: Add a check to see if the original pair is empty as well, but
|
|
// error if it is changing from BTC-USD -> LTC-USD.
|
|
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 != asset.Empty && 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.Equal(m.LastUpdated) {
|
|
d.LastUpdated = time.Now()
|
|
} else {
|
|
d.LastUpdated = m.LastUpdated
|
|
}
|
|
}
|
|
if d.Exchange == "" {
|
|
d.Exchange = m.Exchange
|
|
}
|
|
if d.ID == "" {
|
|
d.ID = m.ID
|
|
}
|
|
if d.InternalOrderID == "" {
|
|
d.InternalOrderID = m.InternalOrderID
|
|
}
|
|
}
|
|
|
|
// 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 m.ID != "" && d.ID != m.ID {
|
|
d.ID = m.ID
|
|
updated = true
|
|
}
|
|
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.QuoteAmount > 0 && m.QuoteAmount != d.QuoteAmount {
|
|
d.QuoteAmount = m.QuoteAmount
|
|
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.Equal(d.Pair) {
|
|
// TODO: Add a check to see if the original pair is empty as well, but
|
|
// error if it is changing from BTC-USD -> LTC-USD.
|
|
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 != asset.Empty && 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.Equal(m.LastUpdated) {
|
|
d.LastUpdated = time.Now()
|
|
} else {
|
|
d.LastUpdated = m.LastUpdated
|
|
}
|
|
}
|
|
}
|
|
|
|
// MatchFilter will return true if a detail matches the filter criteria
|
|
// empty elements are ignored
|
|
func (d *Detail) MatchFilter(f *Filter) bool {
|
|
if f.Exchange != "" && !strings.EqualFold(d.Exchange, f.Exchange) {
|
|
return false
|
|
}
|
|
if f.AssetType != asset.Empty && d.AssetType != f.AssetType {
|
|
return false
|
|
}
|
|
if !f.Pair.IsEmpty() && !d.Pair.Equal(f.Pair) {
|
|
return false
|
|
}
|
|
if f.ID != "" && d.ID != f.ID {
|
|
return false
|
|
}
|
|
if f.Type != "" && f.Type != AnyType && d.Type != f.Type {
|
|
return false
|
|
}
|
|
if f.Side != "" && f.Side != AnySide && d.Side != f.Side {
|
|
return false
|
|
}
|
|
if f.Status != "" && f.Status != AnyStatus && d.Status != f.Status {
|
|
return false
|
|
}
|
|
if f.ClientOrderID != "" && d.ClientOrderID != f.ClientOrderID {
|
|
return false
|
|
}
|
|
if f.ClientID != "" && d.ClientID != f.ClientID {
|
|
return false
|
|
}
|
|
if f.InternalOrderID != "" && d.InternalOrderID != f.InternalOrderID {
|
|
return false
|
|
}
|
|
if f.AccountID != "" && d.AccountID != f.AccountID {
|
|
return false
|
|
}
|
|
if f.WalletAddress != "" && d.WalletAddress != f.WalletAddress {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// IsActive returns true if an order has a status that indicates it is
|
|
// currently available on the exchange
|
|
func (d *Detail) IsActive() bool {
|
|
if d.Amount <= 0 || d.Amount <= d.ExecutedAmount {
|
|
return false
|
|
}
|
|
return d.Status == Active || d.Status == Open || d.Status == PartiallyFilled || d.Status == New ||
|
|
d.Status == AnyStatus || d.Status == PendingCancel || d.Status == Hidden || d.Status == UnknownStatus ||
|
|
d.Status == AutoDeleverage || d.Status == Pending
|
|
}
|
|
|
|
// IsInactive returns true if an order has a status that indicates it is
|
|
// currently not available on the exchange
|
|
func (d *Detail) IsInactive() bool {
|
|
if d.Amount <= 0 || d.Amount <= d.ExecutedAmount {
|
|
return true
|
|
}
|
|
return d.Status == Filled || d.Status == Cancelled || d.Status == InsufficientBalance || d.Status == MarketUnavailable ||
|
|
d.Status == Rejected || d.Status == PartiallyCancelled || d.Status == Expired || d.Status == Closed
|
|
}
|
|
|
|
// GenerateInternalOrderID sets a new V4 order ID or a V5 order ID if
|
|
// the V4 function returns an error
|
|
func (d *Detail) GenerateInternalOrderID() {
|
|
if d.InternalOrderID == "" {
|
|
var id uuid.UUID
|
|
id, err := uuid.NewV4()
|
|
if err != nil {
|
|
id = uuid.NewV5(uuid.UUID{}, d.ID)
|
|
}
|
|
d.InternalOrderID = id.String()
|
|
}
|
|
}
|
|
|
|
// Copy will return a copy of Detail
|
|
func (d *Detail) Copy() Detail {
|
|
c := *d
|
|
if len(d.Trades) > 0 {
|
|
c.Trades = make([]TradeHistory, len(d.Trades))
|
|
copy(c.Trades, d.Trades)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// 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))) // nolint:staticcheck // Ignore Title usage warning
|
|
}
|
|
|
|
// 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))) // nolint:staticcheck // Ignore Title usage warning
|
|
}
|
|
|
|
// IsShort returns if the side is short
|
|
func (s Side) IsShort() bool {
|
|
return s == Short || s == Sell
|
|
}
|
|
|
|
// IsLong returns if the side is long
|
|
func (s Side) IsLong() bool {
|
|
return s == Long || s == Buy
|
|
}
|
|
|
|
// String implements the stringer interface
|
|
func (s Status) String() string {
|
|
return string(s)
|
|
}
|
|
|
|
// InferCostsAndTimes infer order costs using execution information and times when available
|
|
func (d *Detail) InferCostsAndTimes() {
|
|
if d.CostAsset.IsEmpty() {
|
|
d.CostAsset = d.Pair.Quote
|
|
}
|
|
|
|
if d.LastUpdated.IsZero() {
|
|
if d.CloseTime.IsZero() {
|
|
d.LastUpdated = d.Date
|
|
} else {
|
|
d.LastUpdated = d.CloseTime
|
|
}
|
|
}
|
|
|
|
if d.ExecutedAmount <= 0 {
|
|
return
|
|
}
|
|
|
|
if d.AverageExecutedPrice == 0 {
|
|
if d.Cost != 0 {
|
|
d.AverageExecutedPrice = d.Cost / d.ExecutedAmount
|
|
} else {
|
|
d.AverageExecutedPrice = d.Price
|
|
}
|
|
}
|
|
if d.Cost == 0 {
|
|
d.Cost = d.AverageExecutedPrice * d.ExecutedAmount
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
for _, c := range currencies {
|
|
if (*orders)[i].Pair.EqualIncludeReciprocal(c) {
|
|
filteredOrders = append(filteredOrders, (*orders)[i])
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
*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) {
|
|
side = strings.ToUpper(side)
|
|
switch Side(side) {
|
|
case Buy:
|
|
return Buy, nil
|
|
case Sell:
|
|
return Sell, nil
|
|
case Bid:
|
|
return Bid, nil
|
|
case Ask:
|
|
return Ask, nil
|
|
case Long:
|
|
return Long, nil
|
|
case Short:
|
|
return Short, nil
|
|
case AnySide:
|
|
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) {
|
|
oType = strings.ToUpper(oType)
|
|
switch oType {
|
|
case Limit.String(), "EXCHANGE LIMIT":
|
|
return Limit, nil
|
|
case Market.String(), "EXCHANGE MARKET":
|
|
return Market, nil
|
|
case ImmediateOrCancel.String(), "IMMEDIATE OR CANCEL", "IOC", "EXCHANGE IOC":
|
|
return ImmediateOrCancel, nil
|
|
case Stop.String(), "STOP LOSS", "STOP_LOSS", "EXCHANGE STOP":
|
|
return Stop, nil
|
|
case StopLimit.String(), "EXCHANGE STOP LIMIT":
|
|
return StopLimit, nil
|
|
case TrailingStop.String(), "TRAILING STOP", "EXCHANGE TRAILING STOP":
|
|
return TrailingStop, nil
|
|
case FillOrKill.String(), "EXCHANGE FOK":
|
|
return FillOrKill, nil
|
|
case IOS.String():
|
|
return IOS, nil
|
|
case PostOnly.String():
|
|
return PostOnly, nil
|
|
case AnyType.String():
|
|
return AnyType, nil
|
|
case 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) {
|
|
status = strings.ToUpper(status)
|
|
switch status {
|
|
case AnyStatus.String():
|
|
return AnyStatus, nil
|
|
case New.String(), "PLACED":
|
|
return New, nil
|
|
case Active.String(), "STATUS_ACTIVE":
|
|
return Active, nil
|
|
case PartiallyFilled.String(), "PARTIALLY MATCHED", "PARTIALLY FILLED":
|
|
return PartiallyFilled, nil
|
|
case Filled.String(), "FULLY MATCHED", "FULLY FILLED", "ORDER_FULLY_TRANSACTED":
|
|
return Filled, nil
|
|
case PartiallyCancelled.String(), "PARTIALLY CANCELLED", "ORDER_PARTIALLY_TRANSACTED":
|
|
return PartiallyCancelled, nil
|
|
case Open.String():
|
|
return Open, nil
|
|
case Closed.String():
|
|
return Closed, nil
|
|
case Cancelled.String(), "CANCELED", "ORDER_CANCELLED":
|
|
return Cancelled, nil
|
|
case PendingCancel.String(), "PENDING CANCEL", "PENDING CANCELLATION":
|
|
return PendingCancel, nil
|
|
case Rejected.String():
|
|
return Rejected, nil
|
|
case Expired.String():
|
|
return Expired, nil
|
|
case Hidden.String():
|
|
return Hidden, nil
|
|
case InsufficientBalance.String():
|
|
return InsufficientBalance, nil
|
|
case 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
|
|
})
|
|
}
|
|
|
|
// PairAssetRequired is a validation check for when a cancel request
|
|
// requires an asset type and currency pair to be present
|
|
func (c *Cancel) PairAssetRequired() validate.Checker {
|
|
return validate.Check(func() error {
|
|
if c.Pair.IsEmpty() {
|
|
return ErrPairIsEmpty
|
|
}
|
|
|
|
if c.AssetType == asset.Empty {
|
|
return ErrAssetNotSet
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Validate checks internal struct requirements
|
|
func (c *Cancel) Validate(opt ...validate.Checker) error {
|
|
if c == nil {
|
|
return ErrCancelOrderIsNil
|
|
}
|
|
|
|
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
|
|
}
|