exchanges/order: Add TimeInForce type (#1382)

* Added TimeInForce type and updated related files

* Linter issue fix and minor coinbasepro type update

* Bitrex consts update

* added unit test and minor changes in bittrex

* Unit tests update

* Fix minor linter issues

* Update TestStringToTimeInForce unit test

* fix conflict with gateio timeInForce

* Update order tests

* Complete updating the order unit tests

* update kucoin and deribit wrapper to match the time in force change

* fix time-in-force related test errors

* linter issue fix

* time in force constants, functions and unit tests update

* shift tif policies to TimeInForce

* Update time-in-force, related functions, and unit tests

* fix linter issue and time-in-force processing

* added a good till crossing tif value

* order type fix and fix related tim-in-force entries

* update time-in-force unmarshaling and unit test

* fix time-in-force error in gateio

* linter issue fix

* update based on review comments

* add unit test and fix missing issues

* minor fix and added benchmark unit test

* change GTT to GTC for limit

* fix linter issue

* added time-in-force value to place order param

* fix minor issues based on review comment and move tif code to separate files

* update on exchanges linked to time-in-force

* resolve missing review comments

* minor linter issues fix

* added time-in-force handler and update timeInForce parametered endpoint

* minor fixes based on review

* nits fix

* update based on review

* linter fix

* rm getTimeInForce func and minor change to time-in-force

* minor change

* update based on review comments

* wrappers and time-in-force calling approach

* minor change

* update gateio string to timeInForce conversion and unit test

* updated order test unit tes functions

* minor fixes on unit tests

* nits fix based on feedback

* update TestDeriveCancel unit test assert messages

* update TestDeriveCancel unit test assert messages

* update timeInForceFromString method to return formatted error and update functions using it

* restructure and fix minor exchanges time-in-force handling issues

* replaced unused getTypeFromTimeInForce with inline switch-based order type check

* separated the repeated timeInForce conversion code to  a function

* update exchanges time-in-force handling based on review comments

* limter fix

* edded comment to validTimesInForce var

* added comment to gateio's timeInForceString func

* added goodTillCancel switch case to gateio timeInForceString func
This commit is contained in:
Samuael A.
2025-05-23 02:07:09 +03:00
committed by GitHub
parent 8c678063b5
commit 640960aec1
46 changed files with 1201 additions and 1424 deletions

View File

@@ -1,10 +1,11 @@
package order
import (
"errors"
"testing"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
@@ -19,9 +20,7 @@ func TestLoadLimits(t *testing.T) {
t.Parallel()
e := ExecutionLimits{}
err := e.LoadLimits(nil)
if !errors.Is(err, errCannotLoadLimit) {
t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err)
}
assert.ErrorIs(t, err, errCannotLoadLimit)
invalidAsset := []MinMaxLevel{
{
@@ -33,11 +32,7 @@ func TestLoadLimits(t *testing.T) {
},
}
err = e.LoadLimits(invalidAsset)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("expected error %v but received %v",
asset.ErrNotSupported,
err)
}
require.ErrorIs(t, err, asset.ErrNotSupported)
invalidPairLoading := []MinMaxLevel{
{
@@ -50,9 +45,7 @@ func TestLoadLimits(t *testing.T) {
}
err = e.LoadLimits(invalidPairLoading)
if !errors.Is(err, currency.ErrCurrencyPairEmpty) {
t.Fatalf("expected error %v but received %v", currency.ErrCurrencyPairEmpty, err)
}
assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
newLimits := []MinMaxLevel{
{
@@ -66,9 +59,7 @@ func TestLoadLimits(t *testing.T) {
}
err = e.LoadLimits(newLimits)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
require.NoError(t, err)
badLimit := []MinMaxLevel{
{
@@ -82,9 +73,7 @@ func TestLoadLimits(t *testing.T) {
}
err = e.LoadLimits(badLimit)
if !errors.Is(err, errInvalidPriceLevels) {
t.Fatalf("expected error %v but received %v", errInvalidPriceLevels, err)
}
require.ErrorIs(t, err, errInvalidPriceLevels)
badLimit = []MinMaxLevel{
{
@@ -98,9 +87,7 @@ func TestLoadLimits(t *testing.T) {
}
err = e.LoadLimits(badLimit)
if !errors.Is(err, errInvalidAmountLevels) {
t.Fatalf("expected error %v but received %v", errInvalidPriceLevels, err)
}
require.ErrorIs(t, err, errInvalidAmountLevels)
goodLimit := []MinMaxLevel{
{
@@ -110,9 +97,7 @@ func TestLoadLimits(t *testing.T) {
}
err = e.LoadLimits(goodLimit)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
require.NoError(t, err)
noCompare := []MinMaxLevel{
{
@@ -123,9 +108,7 @@ func TestLoadLimits(t *testing.T) {
}
err = e.LoadLimits(noCompare)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
require.NoError(t, err)
noCompare = []MinMaxLevel{
{
@@ -136,18 +119,14 @@ func TestLoadLimits(t *testing.T) {
}
err = e.LoadLimits(noCompare)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
assert.NoError(t, err)
}
func TestGetOrderExecutionLimits(t *testing.T) {
t.Parallel()
e := ExecutionLimits{}
_, err := e.GetOrderExecutionLimits(asset.Spot, btcusd)
if !errors.Is(err, ErrExchangeLimitNotLoaded) {
t.Fatalf("expected error %v but received %v", ErrExchangeLimitNotLoaded, err)
}
require.ErrorIs(t, err, ErrExchangeLimitNotLoaded)
newLimits := []MinMaxLevel{
{
@@ -161,45 +140,30 @@ func TestGetOrderExecutionLimits(t *testing.T) {
}
err = e.LoadLimits(newLimits)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err)
}
require.NoError(t, err)
_, err = e.GetOrderExecutionLimits(asset.Futures, ltcusd)
if !errors.Is(err, ErrCannotValidateAsset) {
t.Fatalf("expected error %v but received %v", ErrCannotValidateAsset, err)
}
require.ErrorIs(t, err, ErrCannotValidateAsset)
_, err = e.GetOrderExecutionLimits(asset.Spot, ltcusd)
if !errors.Is(err, errExchangeLimitBase) {
t.Fatalf("expected error %v but received %v", errExchangeLimitBase, err)
}
require.ErrorIs(t, err, errExchangeLimitBase)
_, err = e.GetOrderExecutionLimits(asset.Spot, btcltc)
if !errors.Is(err, errExchangeLimitQuote) {
t.Fatalf("expected error %v but received %v", errExchangeLimitQuote, err)
}
require.ErrorIs(t, err, errExchangeLimitQuote)
tt, err := e.GetOrderExecutionLimits(asset.Spot, btcusd)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
if tt.MaximumBaseAmount != newLimits[0].MaximumBaseAmount ||
tt.MinimumBaseAmount != newLimits[0].MinimumBaseAmount ||
tt.MaxPrice != newLimits[0].MaxPrice ||
tt.MinPrice != newLimits[0].MinPrice {
t.Fatal("unexpected values")
}
require.NoError(t, err)
assert.Equal(t, newLimits[0].MaximumBaseAmount, tt.MaximumBaseAmount)
assert.Equal(t, newLimits[0].MinimumBaseAmount, tt.MinimumBaseAmount)
assert.Equal(t, newLimits[0].MaxPrice, tt.MaxPrice)
assert.Equal(t, newLimits[0].MinPrice, tt.MinPrice)
}
func TestCheckLimit(t *testing.T) {
t.Parallel()
e := ExecutionLimits{}
err := e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 1337, Limit)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
require.NoError(t, err)
newLimits := []MinMaxLevel{
{
@@ -213,97 +177,63 @@ func TestCheckLimit(t *testing.T) {
}
err = e.LoadLimits(newLimits)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err)
}
require.NoError(t, err)
err = e.CheckOrderExecutionLimits(asset.Futures, ltcusd, 1337, 1337, Limit)
if !errors.Is(err, ErrCannotValidateAsset) {
t.Fatalf("expected error %v but received %v", ErrCannotValidateAsset, err)
}
require.ErrorIs(t, err, ErrCannotValidateAsset)
err = e.CheckOrderExecutionLimits(asset.Spot, ltcusd, 1337, 1337, Limit)
if !errors.Is(err, ErrCannotValidateBaseCurrency) {
t.Fatalf("expected error %v but received %v", ErrCannotValidateBaseCurrency, err)
}
require.ErrorIs(t, err, ErrCannotValidateBaseCurrency)
err = e.CheckOrderExecutionLimits(asset.Spot, btcltc, 1337, 1337, Limit)
if !errors.Is(err, ErrCannotValidateQuoteCurrency) {
t.Fatalf("expected error %v but received %v", ErrCannotValidateQuoteCurrency, err)
}
require.ErrorIs(t, err, ErrCannotValidateQuoteCurrency)
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 9, Limit)
if !errors.Is(err, ErrPriceBelowMin) {
t.Fatalf("expected error %v but received %v", ErrPriceBelowMin, err)
}
require.ErrorIs(t, err, ErrPriceBelowMin)
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1000001, 9, Limit)
if !errors.Is(err, ErrPriceExceedsMax) {
t.Fatalf("expected error %v but received %v", ErrPriceExceedsMax, err)
}
require.ErrorIs(t, err, ErrPriceExceedsMax)
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, .5, Limit)
if !errors.Is(err, ErrAmountBelowMin) {
t.Fatalf("expected error %v but received %v", ErrAmountBelowMin, err)
}
require.ErrorIs(t, err, ErrAmountBelowMin)
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 11, Limit)
if !errors.Is(err, ErrAmountExceedsMax) {
t.Fatalf("expected error %v but received %v", ErrAmountExceedsMax, err)
}
require.ErrorIs(t, err, ErrAmountExceedsMax)
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Limit)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
require.NoError(t, err)
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Market)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
assert.NoError(t, err)
}
func TestConforms(t *testing.T) {
t.Parallel()
var tt MinMaxLevel
err := tt.Conforms(0, 0, Limit)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
tt = MinMaxLevel{
MinNotional: 100,
}
err = tt.Conforms(1, 1, Limit)
if !errors.Is(err, ErrNotionalValue) {
t.Fatalf("expected error %v but received %v", ErrNotionalValue, err)
}
require.ErrorIs(t, err, ErrNotionalValue)
err = tt.Conforms(200, .5, Limit)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
require.NoError(t, err)
tt.PriceStepIncrementSize = 0.001
err = tt.Conforms(200.0001, .5, Limit)
if !errors.Is(err, ErrPriceExceedsStep) {
t.Fatalf("expected error %v but received %v", ErrPriceExceedsStep, err)
}
require.ErrorIs(t, err, ErrPriceExceedsStep)
err = tt.Conforms(200.004, .5, Limit)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
require.NoError(t, err)
tt.AmountStepIncrementSize = 0.001
err = tt.Conforms(200, .0002, Limit)
if !errors.Is(err, ErrAmountExceedsStep) {
t.Fatalf("expected error %v but received %v", ErrAmountExceedsStep, err)
}
require.ErrorIs(t, err, ErrAmountExceedsStep)
err = tt.Conforms(200000, .003, Limit)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received %v", nil, err)
}
require.NoError(t, err)
tt.MinimumBaseAmount = 1
tt.MaximumBaseAmount = 10
@@ -311,115 +241,73 @@ func TestConforms(t *testing.T) {
tt.MarketMaxQty = 9.9
err = tt.Conforms(200000, 1, Market)
if !errors.Is(err, ErrMarketAmountBelowMin) {
t.Fatalf("expected error %v but received: %v", ErrMarketAmountBelowMin, err)
}
require.ErrorIs(t, err, ErrMarketAmountBelowMin)
err = tt.Conforms(200000, 10, Market)
if !errors.Is(err, ErrMarketAmountExceedsMax) {
t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsMax, err)
}
require.ErrorIs(t, err, ErrMarketAmountExceedsMax)
tt.MarketStepIncrementSize = 10
err = tt.Conforms(200000, 9.1, Market)
if !errors.Is(err, ErrMarketAmountExceedsStep) {
t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsStep, err)
}
require.ErrorIs(t, err, ErrMarketAmountExceedsStep)
tt.MarketStepIncrementSize = 1
err = tt.Conforms(200000, 9.1, Market)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received: %v", nil, err)
}
assert.NoError(t, err)
}
func TestConformToDecimalAmount(t *testing.T) {
t.Parallel()
var tt MinMaxLevel
if !tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001)).Equal(decimal.NewFromFloat(1.001)) {
t.Fatal("value should not be changed")
}
require.True(t, tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001)).Equal(decimal.NewFromFloat(1.001)))
tt = MinMaxLevel{}
val := tt.ConformToDecimalAmount(decimal.NewFromInt(1))
if !val.Equal(decimal.NewFromInt(1)) { // If there is no step amount set this should not change
// the inputted amount
t.Fatal("unexpected amount")
}
assert.True(t, val.Equal(decimal.NewFromInt(1))) // If there is no step amount set, this should not change the inputted amount
tt.AmountStepIncrementSize = 0.001
val = tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001))
if !val.Equal(decimal.NewFromFloat(1.001)) {
t.Error("unexpected amount", val)
}
assert.True(t, val.Equal(decimal.NewFromFloat(1.001)))
val = tt.ConformToDecimalAmount(decimal.NewFromFloat(0.0001))
if !val.IsZero() {
t.Error("unexpected amount", val)
}
assert.True(t, val.IsZero())
val = tt.ConformToDecimalAmount(decimal.NewFromFloat(0.7777))
if !val.Equal(decimal.NewFromFloat(0.777)) {
t.Error("unexpected amount", val)
}
assert.True(t, val.Equal(decimal.NewFromFloat(0.777)))
tt.AmountStepIncrementSize = 100
val = tt.ConformToDecimalAmount(decimal.NewFromInt(100))
if !val.Equal(decimal.NewFromInt(100)) {
t.Fatal("unexpected amount", val)
}
assert.True(t, val.Equal(decimal.NewFromInt(100)))
val = tt.ConformToDecimalAmount(decimal.NewFromInt(200))
if !val.Equal(decimal.NewFromInt(200)) {
t.Fatal("unexpected amount", val)
}
assert.True(t, val.Equal(decimal.NewFromInt(200)))
val = tt.ConformToDecimalAmount(decimal.NewFromInt(150))
if !val.Equal(decimal.NewFromInt(100)) {
t.Fatal("unexpected amount", val)
}
assert.True(t, val.Equal(decimal.NewFromInt(100)))
}
func TestConformToAmount(t *testing.T) {
t.Parallel()
var tt MinMaxLevel
if tt.ConformToAmount(1.001) != 1.001 {
t.Fatal("value should not be changed")
}
require.Equal(t, 1.001, tt.ConformToAmount(1.001))
tt = MinMaxLevel{}
val := tt.ConformToAmount(1)
if val != 1 { // If there is no step amount set this should not change
// the inputted amount
t.Fatal("unexpected amount")
}
assert.Equal(t, 1.0, val, "ConformToAmount should return the same value with no step amount set")
tt.AmountStepIncrementSize = 0.001
val = tt.ConformToAmount(1.001)
if val != 1.001 {
t.Error("unexpected amount", val)
}
assert.Equal(t, 1.001, val)
val = tt.ConformToAmount(0.0001)
if val != 0 {
t.Error("unexpected amount", val)
}
assert.Zero(t, val)
val = tt.ConformToAmount(0.7777)
if val != 0.777 {
t.Error("unexpected amount", val)
}
assert.Equal(t, 0.777, val)
tt.AmountStepIncrementSize = 100
val = tt.ConformToAmount(100)
if val != 100 {
t.Fatal("unexpected amount", val)
}
assert.Equal(t, 100.0, val)
val = tt.ConformToAmount(200)
if val != 200 {
t.Fatal("unexpected amount", val)
}
require.Equal(t, 200.0, val)
val = tt.ConformToAmount(150)
if val != 100 {
t.Fatal("unexpected amount", val)
}
assert.Equal(t, 100.0, val)
}

File diff suppressed because it is too large Load Diff

View File

@@ -46,11 +46,9 @@ type Submit struct {
Pair currency.Pair
AssetType asset.Item
// Time in force values ------ TODO: Time In Force uint8
ImmediateOrCancel bool
FillOrKill bool
// TimeInForce holds time in force values
TimeInForce TimeInForce
PostOnly bool
// ReduceOnly reduces a position instead of opening an opposing
// position; this also equates to closing the position in huobi_wrapper.go
// swaps.
@@ -109,19 +107,17 @@ type SubmitResponse struct {
Pair currency.Pair
AssetType asset.Item
ImmediateOrCancel bool
FillOrKill bool
PostOnly bool
TimeInForce TimeInForce
ReduceOnly bool
Leverage float64
Price float64
AverageExecutedPrice float64
Amount float64
QuoteAmount float64
RemainingAmount float64
TriggerPrice float64
ClientID string
ClientOrderID string
AverageExecutedPrice float64
LastUpdated time.Time
Date time.Time
@@ -164,11 +160,10 @@ type Modify struct {
Pair currency.Pair
// Change fields
ImmediateOrCancel bool
PostOnly bool
Price float64
Amount float64
TriggerPrice float64
TimeInForce TimeInForce
Price float64
Amount float64
TriggerPrice float64
// added to represent a unified trigger price type information such as LastPrice, MarkPrice, and IndexPrice
// https://bybit-exchange.github.io/docs/v5/order/create-order
@@ -190,11 +185,10 @@ type ModifyResponse struct {
AssetType asset.Item
// Fields that will be copied over from Modify
ImmediateOrCancel bool
PostOnly bool
Price float64
Amount float64
TriggerPrice float64
TimeInForce TimeInForce
Price float64
Amount float64
TriggerPrice float64
// Fields that need to be handled in scope after DeriveModifyResponse()
// if applicable
@@ -206,10 +200,8 @@ type ModifyResponse struct {
// Detail contains all properties of an order
// Each exchange has their own requirements, so not all fields are required to be populated
type Detail struct {
ImmediateOrCancel bool
HiddenOrder bool
FillOrKill bool
PostOnly bool
TimeInForce TimeInForce
ReduceOnly bool
Leverage float64
Price float64
@@ -276,6 +268,7 @@ type Cancel struct {
AssetType asset.Item
Pair currency.Pair
MarginType margin.Type
TimeInForce TimeInForce
}
// CancelAllResponse returns the status from attempting to
@@ -311,12 +304,13 @@ type TradeHistory struct {
type MultiOrderRequest struct {
// Currencies Empty array = all currencies. Some endpoints only support
// singular currency enquiries
Pairs currency.Pairs
AssetType asset.Item
Type Type
Side Side
StartTime time.Time
EndTime time.Time
Pairs currency.Pairs
AssetType asset.Item
Type Type
Side Side
TimeInForce TimeInForce
StartTime time.Time
EndTime time.Time
// FromOrderID for some APIs require order history searching
// from a specific orderID rather than via timestamps
FromOrderID string
@@ -361,15 +355,12 @@ const (
UnknownType Type = 0
Limit Type = 1 << iota
Market
PostOnly
ImmediateOrCancel
Stop
StopLimit
StopMarket
TakeProfit
TakeProfitMarket
TrailingStop
FillOrKill
IOS
AnyType
Liquidation

View File

@@ -43,7 +43,6 @@ var (
)
var (
errTimeInForceConflict = errors.New("multiple time in force options applied")
errUnrecognisedOrderType = errors.New("unrecognised order type")
errUnrecognisedOrderStatus = errors.New("unrecognised order status")
errExchangeNameUnset = errors.New("exchange name unset")
@@ -88,8 +87,8 @@ func (s *Submit) Validate(requirements protocol.TradingRequirements, opt ...vali
return ErrTypeIsInvalid
}
if s.ImmediateOrCancel && s.FillOrKill {
return errTimeInForceConflict
if !s.TimeInForce.IsValid() {
return ErrInvalidTimeInForce
}
if s.Amount == 0 && s.QuoteAmount == 0 {
@@ -159,18 +158,14 @@ func (d *Detail) UpdateOrderFromDetail(m *Detail) error {
}
var updated bool
if d.ImmediateOrCancel != m.ImmediateOrCancel {
d.ImmediateOrCancel = m.ImmediateOrCancel
if m.TimeInForce != UnknownTIF && d.TimeInForce != m.TimeInForce {
d.TimeInForce = m.TimeInForce
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
@@ -207,10 +202,6 @@ func (d *Detail) UpdateOrderFromDetail(m *Detail) error {
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.
@@ -322,8 +313,8 @@ func (d *Detail) UpdateOrderFromModifyResponse(m *ModifyResponse) {
d.OrderID = m.OrderID
updated = true
}
if d.ImmediateOrCancel != m.ImmediateOrCancel {
d.ImmediateOrCancel = m.ImmediateOrCancel
if d.TimeInForce != m.TimeInForce && m.TimeInForce != UnknownTIF {
d.TimeInForce = m.TimeInForce
updated = true
}
if m.Price > 0 && m.Price != d.Price {
@@ -338,10 +329,6 @@ func (d *Detail) UpdateOrderFromModifyResponse(m *ModifyResponse) {
d.TriggerPrice = m.TriggerPrice
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.
@@ -496,18 +483,16 @@ func (s *Submit) DeriveSubmitResponse(orderID string) (*SubmitResponse, error) {
Pair: s.Pair,
AssetType: s.AssetType,
ImmediateOrCancel: s.ImmediateOrCancel,
FillOrKill: s.FillOrKill,
PostOnly: s.PostOnly,
ReduceOnly: s.ReduceOnly,
Leverage: s.Leverage,
Price: s.Price,
Amount: s.Amount,
QuoteAmount: s.QuoteAmount,
TriggerPrice: s.TriggerPrice,
ClientID: s.ClientID,
ClientOrderID: s.ClientOrderID,
MarginType: s.MarginType,
TimeInForce: s.TimeInForce,
ReduceOnly: s.ReduceOnly,
Leverage: s.Leverage,
Price: s.Price,
Amount: s.Amount,
QuoteAmount: s.QuoteAmount,
TriggerPrice: s.TriggerPrice,
ClientID: s.ClientID,
ClientOrderID: s.ClientOrderID,
MarginType: s.MarginType,
LastUpdated: time.Now(),
Date: time.Now(),
@@ -589,17 +574,15 @@ func (s *SubmitResponse) DeriveDetail(internal uuid.UUID) (*Detail, error) {
Pair: s.Pair,
AssetType: s.AssetType,
ImmediateOrCancel: s.ImmediateOrCancel,
FillOrKill: s.FillOrKill,
PostOnly: s.PostOnly,
ReduceOnly: s.ReduceOnly,
Leverage: s.Leverage,
Price: s.Price,
Amount: s.Amount,
QuoteAmount: s.QuoteAmount,
TriggerPrice: s.TriggerPrice,
ClientID: s.ClientID,
ClientOrderID: s.ClientOrderID,
TimeInForce: s.TimeInForce,
ReduceOnly: s.ReduceOnly,
Leverage: s.Leverage,
Price: s.Price,
Amount: s.Amount,
QuoteAmount: s.QuoteAmount,
TriggerPrice: s.TriggerPrice,
ClientID: s.ClientID,
ClientOrderID: s.ClientOrderID,
InternalOrderID: internal,
@@ -649,18 +632,17 @@ func (m *Modify) DeriveModifyResponse() (*ModifyResponse, error) {
return nil, errOrderDetailIsNil
}
return &ModifyResponse{
Exchange: m.Exchange,
OrderID: m.OrderID,
ClientOrderID: m.ClientOrderID,
Type: m.Type,
Side: m.Side,
AssetType: m.AssetType,
Pair: m.Pair,
ImmediateOrCancel: m.ImmediateOrCancel,
PostOnly: m.PostOnly,
Price: m.Price,
Amount: m.Amount,
TriggerPrice: m.TriggerPrice,
Exchange: m.Exchange,
OrderID: m.OrderID,
ClientOrderID: m.ClientOrderID,
Type: m.Type,
Side: m.Side,
AssetType: m.AssetType,
Pair: m.Pair,
TimeInForce: m.TimeInForce,
Price: m.Price,
Amount: m.Amount,
TriggerPrice: m.TriggerPrice,
}, nil
}
@@ -691,10 +673,6 @@ func (t Type) String() string {
return "LIMIT"
case Market:
return "MARKET"
case PostOnly:
return "POST_ONLY"
case ImmediateOrCancel:
return "IMMEDIATE_OR_CANCEL"
case Stop:
return "STOP"
case ConditionalStop:
@@ -717,8 +695,6 @@ func (t Type) String() string {
return "TAKE PROFIT MARKET"
case TrailingStop:
return "TRAILING_STOP"
case FillOrKill:
return "FOK"
case IOS:
return "IOS"
case Liquidation:
@@ -1130,8 +1106,6 @@ func StringToOrderType(oType string) (Type, error) {
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", "STOP_LIMIT":
@@ -1140,12 +1114,8 @@ func StringToOrderType(oType string) (Type, error) {
return StopMarket, nil
case TrailingStop.String(), "TRAILING STOP", "EXCHANGE TRAILING STOP", "MOVE_ORDER_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():

View File

@@ -0,0 +1,140 @@
package order
import (
"errors"
"fmt"
"strings"
)
// var error definitions
var (
ErrInvalidTimeInForce = errors.New("invalid time in force value provided")
ErrUnsupportedTimeInForce = errors.New("unsupported time in force value")
)
// TimeInForce enforces a standard for time-in-force values across the code base.
type TimeInForce uint8
// TimeInForce types
const (
UnknownTIF TimeInForce = 0
GoodTillCancel TimeInForce = 1 << iota
GoodTillDay
GoodTillTime
GoodTillCrossing
FillOrKill
ImmediateOrCancel
PostOnly
supportedTimeInForceFlag = GoodTillCancel | GoodTillDay | GoodTillTime | GoodTillCrossing | FillOrKill | ImmediateOrCancel | PostOnly
)
// time-in-force string representations
const (
gtcStr = "GTC"
gtdStr = "GTD"
gttStr = "GTT"
gtxStr = "GTX"
fokStr = "FOK"
iocStr = "IOC"
postonlyStr = "POSTONLY"
)
// Is checks to see if the enum contains the flag
func (t TimeInForce) Is(in TimeInForce) bool {
return in != 0 && t&in == in
}
// StringToTimeInForce converts time in force string value to TimeInForce instance.
func StringToTimeInForce(timeInForce string) (TimeInForce, error) {
var result TimeInForce
timeInForce = strings.ToUpper(timeInForce)
switch timeInForce {
case "IMMEDIATEORCANCEL", "IMMEDIATE_OR_CANCEL", iocStr:
result = ImmediateOrCancel
case "GOODTILLCANCEL", "GOODTILCANCEL", "GOOD_TIL_CANCELLED", "GOOD_TILL_CANCELLED", "GOOD_TILL_CANCELED", gtcStr:
result = GoodTillCancel
case "GOODTILLDAY", "GOOD_TIL_DAY", "GOOD_TILL_DAY", gtdStr:
result = GoodTillDay
case "GOODTILLTIME", "GOOD_TIL_TIME", gttStr:
result = GoodTillTime
case "GOODTILLCROSSING", "GOOD_TIL_CROSSING", "GOOD TIL CROSSING", "GOOD_TILL_CROSSING", gtxStr:
result = GoodTillCrossing
case "FILLORKILL", "FILL_OR_KILL", fokStr:
result = FillOrKill
case "POC", "POST_ONLY", "PENDINGORCANCEL", postonlyStr:
result = PostOnly
}
if result == UnknownTIF && timeInForce != "" {
return UnknownTIF, fmt.Errorf("%w: tif=%s", ErrInvalidTimeInForce, timeInForce)
}
return result, nil
}
// IsValid returns whether or not the supplied time in force value is valid or
// not
func (t TimeInForce) IsValid() bool {
// Neither ImmediateOrCancel nor FillOrKill can coexist with anything else
// If either bit is set then it must be the only bit set
isIOCorFOK := t&(ImmediateOrCancel|FillOrKill) != 0
hasTwoBitsSet := t&(t-1) != 0
if isIOCorFOK && hasTwoBitsSet {
return false
}
return t == UnknownTIF || supportedTimeInForceFlag&t == t
}
// String implements the stringer interface.
func (t TimeInForce) String() string {
if t == UnknownTIF {
return ""
}
var tifStrings []string
if t.Is(ImmediateOrCancel) {
tifStrings = append(tifStrings, iocStr)
}
if t.Is(GoodTillCancel) {
tifStrings = append(tifStrings, gtcStr)
}
if t.Is(GoodTillDay) {
tifStrings = append(tifStrings, gtdStr)
}
if t.Is(GoodTillTime) {
tifStrings = append(tifStrings, gttStr)
}
if t.Is(GoodTillCrossing) {
tifStrings = append(tifStrings, gtxStr)
}
if t.Is(FillOrKill) {
tifStrings = append(tifStrings, fokStr)
}
if t.Is(PostOnly) {
tifStrings = append(tifStrings, postonlyStr)
}
if len(tifStrings) == 0 {
return "UNKNOWN"
}
return strings.Join(tifStrings, ",")
}
// Lower returns a lower case string representation of time-in-force
func (t TimeInForce) Lower() string {
return strings.ToLower(t.String())
}
// UnmarshalJSON deserializes a string data into TimeInForce instance.
func (t *TimeInForce) UnmarshalJSON(data []byte) error {
for val := range strings.SplitSeq(strings.Trim(string(data), `"`), ",") {
tif, err := StringToTimeInForce(val)
if err != nil {
return err
}
*t |= tif
}
return nil
}
// MarshalJSON returns the JSON-encoded order time-in-force value
func (t TimeInForce) MarshalJSON() ([]byte, error) {
return []byte(`"` + t.String() + `"`), nil
}

View File

@@ -0,0 +1,168 @@
package order
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
)
func TestTimeInForceIs(t *testing.T) {
t.Parallel()
tifValuesMap := map[TimeInForce][]TimeInForce{
GoodTillCancel | PostOnly: {GoodTillCancel, PostOnly},
GoodTillCancel: {GoodTillCancel},
GoodTillCrossing | PostOnly: {GoodTillCrossing, PostOnly},
GoodTillDay: {GoodTillDay},
GoodTillTime: {GoodTillTime},
GoodTillTime | PostOnly: {GoodTillTime, PostOnly},
ImmediateOrCancel: {ImmediateOrCancel},
FillOrKill: {FillOrKill},
PostOnly: {PostOnly},
GoodTillCrossing: {GoodTillCrossing},
}
for tif, exps := range tifValuesMap {
for _, v := range exps {
require.Truef(t, tif.Is(v), "%s should be %s", tif, v)
}
}
}
func TestIsValid(t *testing.T) {
t.Parallel()
timeInForceValidityMap := map[TimeInForce]bool{
TimeInForce(1): false,
ImmediateOrCancel: true,
GoodTillTime: true,
GoodTillCancel: true,
GoodTillDay: true,
FillOrKill: true,
PostOnly: true,
FillOrKill | ImmediateOrCancel: false,
FillOrKill | GoodTillCancel: false,
FillOrKill | PostOnly: false,
ImmediateOrCancel | GoodTillCancel: false,
ImmediateOrCancel | PostOnly: false,
GoodTillTime | PostOnly: true,
GoodTillDay | PostOnly: true,
GoodTillCrossing | PostOnly: true,
GoodTillCancel | PostOnly: true,
UnknownTIF: true,
}
for tif, value := range timeInForceValidityMap {
assert.Equal(t, value, tif.IsValid())
}
}
var timeInForceStringToValueMap = map[string]struct {
TIF TimeInForce
Error error
}{
"GoodTillCancel": {TIF: GoodTillCancel},
"GOOD_TILL_CANCELED": {TIF: GoodTillCancel},
"GTT": {TIF: GoodTillTime},
"GOOD_TIL_TIME": {TIF: GoodTillTime},
"FILLORKILL": {TIF: FillOrKill},
"immedIate_Or_Cancel": {TIF: ImmediateOrCancel},
"IOC": {TIF: ImmediateOrCancel},
"immediate_or_cancel": {TIF: ImmediateOrCancel},
"IMMEDIATE_OR_CANCEL": {TIF: ImmediateOrCancel},
"IMMEDIATEORCANCEL": {TIF: ImmediateOrCancel},
"GOOD_TILL_CANCELLED": {TIF: GoodTillCancel},
"good_till_day": {TIF: GoodTillDay},
"GOOD_TILL_DAY": {TIF: GoodTillDay},
"GTD": {TIF: GoodTillDay},
"GOODtillday": {TIF: GoodTillDay},
"PoC": {TIF: PostOnly},
"PendingORCANCEL": {TIF: PostOnly},
"GTX": {TIF: GoodTillCrossing},
"GOOD_TILL_CROSSING": {TIF: GoodTillCrossing},
"Good Til crossing": {TIF: GoodTillCrossing},
"abcdfeg": {TIF: UnknownTIF, Error: ErrInvalidTimeInForce},
}
func TestStringToTimeInForce(t *testing.T) {
t.Parallel()
for tk, exp := range timeInForceStringToValueMap {
t.Run(tk, func(t *testing.T) {
t.Parallel()
result, err := StringToTimeInForce(tk)
if exp.Error != nil {
require.ErrorIs(t, err, exp.Error)
} else {
require.NoError(t, err)
}
assert.Equal(t, exp.TIF, result)
})
}
}
func TestString(t *testing.T) {
t.Parallel()
valMap := map[TimeInForce]string{
ImmediateOrCancel: "IOC",
GoodTillCancel: "GTC",
GoodTillTime: "GTT",
GoodTillDay: "GTD",
FillOrKill: "FOK",
UnknownTIF: "",
PostOnly: "POSTONLY",
GoodTillCancel | PostOnly: "GTC,POSTONLY",
GoodTillTime | PostOnly: "GTT,POSTONLY",
GoodTillDay | PostOnly: "GTD,POSTONLY",
FillOrKill | ImmediateOrCancel: "IOC,FOK",
TimeInForce(1): "UNKNOWN",
}
for x := range valMap {
assert.Equal(t, valMap[x], x.String())
assert.Equal(t, strings.ToLower(valMap[x]), x.Lower())
}
}
func TestUnmarshalJSON(t *testing.T) {
t.Parallel()
targets := []TimeInForce{
GoodTillCancel | PostOnly | ImmediateOrCancel, GoodTillCancel | PostOnly, GoodTillCancel, UnknownTIF, PostOnly | ImmediateOrCancel,
GoodTillCancel, GoodTillCancel, PostOnly, PostOnly, ImmediateOrCancel, GoodTillDay, GoodTillDay, GoodTillTime, FillOrKill, FillOrKill,
}
data := `{"tifs": ["GTC,POSTONLY,IOC", "GTC,POSTONLY", "GTC", "", "POSTONLY,IOC", "GoodTilCancel", "GoodTILLCANCEL", "POST_ONLY", "POC","IOC", "GTD", "gtd","gtt", "fok", "fillOrKill"]}`
target := &struct {
TIFs []TimeInForce `json:"tifs"`
}{}
err := json.Unmarshal([]byte(data), &target)
require.NoError(t, err)
require.Equal(t, targets, target.TIFs)
data = `{"tifs": ["abcd,POSTONLY,IOC", "GTC,POSTONLY", "GTC", "", "POSTONLY,IOC", "GoodTilCancel", "GoodTILLCANCEL", "POST_ONLY", "POC","IOC", "GTD", "gtd","gtt", "fok", "fillOrKill"]}`
target = &struct {
TIFs []TimeInForce `json:"tifs"`
}{}
err = json.Unmarshal([]byte(data), &target)
require.ErrorIs(t, err, ErrInvalidTimeInForce)
}
func TestMarshalJSON(t *testing.T) {
t.Parallel()
data, err := json.Marshal(GoodTillCrossing)
require.NoError(t, err)
assert.Equal(t, []byte(`"GTX"`), data)
data = []byte(`{"tif":"IOC"}`)
target := &struct {
TimeInForce TimeInForce `json:"tif"`
}{}
err = json.Unmarshal(data, &target)
require.NoError(t, err)
assert.Equal(t, "IOC", target.TimeInForce.String())
}
// BenchmarkStringToTimeInForce-8 416595 2834 ns/op 1368 B/op 81 allocs/op
func BenchmarkStringToTimeInForce(b *testing.B) {
for b.Loop() {
for k := range timeInForceStringToValueMap {
_, _ = StringToTimeInForce(k)
}
}
}