exchanges: Order types update (#1850)

* 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

* update order type to support time-in-force and hybrid order type representation

* added tests for type and time-in-force check from order type

* fix time-in-force error in gateio

* fix minor unit test issues

* 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

* linter issues fix

* update order types declaration and handling by endpoints

* 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

* fix types to string conversion

* fix build errors

* update on order types handling and unit tests

* fix linter issue

* order type unit tests update

* order types string as constant replacement and unit tests update

* update order type-string functions unit test
This commit is contained in:
Samuael A.
2025-06-12 08:39:17 +03:00
committed by GitHub
parent d5ba674fc4
commit a8a3bc4ee2
9 changed files with 265 additions and 148 deletions

View File

@@ -24,49 +24,64 @@ func orderTypeFromString(orderType string) (order.Type, order.TimeInForce, error
case orderIOC:
return order.Limit, order.ImmediateOrCancel, nil
case orderOptimalLimitIOC:
return order.OptimalLimitIOC, order.ImmediateOrCancel, nil
case "mmp":
return order.OptimalLimit, order.ImmediateOrCancel, nil
case orderMarketMakerProtection:
return order.MarketMakerProtection, order.UnknownTIF, nil
case "mmp_and_post_only":
return order.MarketMakerProtectionAndPostOnly, order.PostOnly, nil
case "twap":
case orderMarketMakerProtectionAndPostOnly:
return order.MarketMakerProtection, order.PostOnly, nil
case orderTWAP:
return order.TWAP, order.UnknownTIF, nil
case "move_order_stop":
case orderMoveOrderStop:
return order.TrailingStop, order.UnknownTIF, nil
case "chase":
case orderChase:
return order.Chase, order.UnknownTIF, nil
default:
return order.UnknownType, order.UnknownTIF, fmt.Errorf("%w %v", order.ErrTypeIsInvalid, orderType)
return order.UnknownType, order.UnknownTIF, fmt.Errorf("%w %q", order.ErrTypeIsInvalid, orderType)
}
}
// orderTypeString returns a string representation of order.Type instance
func orderTypeString(orderType order.Type, tif order.TimeInForce) (string, error) {
switch tif {
case order.PostOnly:
return orderPostOnly, nil
case order.FillOrKill:
return orderFOK, nil
case order.ImmediateOrCancel:
return orderIOC, nil
}
switch orderType {
case order.Market,
order.Limit,
order.Trigger,
order.OptimalLimitIOC,
order.MarketMakerProtection,
order.MarketMakerProtectionAndPostOnly,
case order.MarketMakerProtection:
if tif == order.PostOnly {
return orderMarketMakerProtectionAndPostOnly, nil
}
return orderMarketMakerProtection, nil
case order.OptimalLimit:
return orderOptimalLimitIOC, nil
case order.Limit:
if tif == order.PostOnly {
return orderPostOnly, nil
}
return orderLimit, nil
case order.Market:
switch tif {
case order.FillOrKill:
return orderFOK, nil
case order.ImmediateOrCancel:
return orderIOC, nil
}
return orderMarket, nil
case order.Trigger,
order.Chase,
order.TWAP,
order.OCO:
return orderType.Lower(), nil
case order.ConditionalStop:
return "conditional", nil
return orderConditional, nil
case order.TrailingStop:
return "move_order_stop", nil
return orderMoveOrderStop, nil
default:
return "", fmt.Errorf("%w: `%v`", order.ErrUnsupportedOrderType, orderType)
switch tif {
case order.PostOnly:
return orderPostOnly, nil
case order.FillOrKill:
return orderFOK, nil
case order.ImmediateOrCancel:
return orderIOC, nil
}
return "", fmt.Errorf("%w: %q", order.ErrUnsupportedOrderType, orderType)
}
}
@@ -152,15 +167,15 @@ func assetTypeFromInstrumentType(instrumentType string) (asset.Item, error) {
func assetTypeString(assetType asset.Item) (string, error) {
switch assetType {
case asset.Spot:
return "SPOT", nil
return instTypeSpot, nil
case asset.Margin:
return "MARGIN", nil
return instTypeMargin, nil
case asset.Futures:
return "FUTURES", nil
return instTypeFutures, nil
case asset.Options:
return "OPTION", nil
return instTypeOption, nil
case asset.PerpetualSwap:
return "SWAP", nil
return instTypeSwap, nil
default:
return "", asset.ErrNotSupported
}

View File

@@ -462,7 +462,7 @@ func (ok *Okx) PlaceTakeProfitStopLossOrder(ctx context.Context, arg *AlgoOrderP
if *arg == (AlgoOrderParams{}) {
return nil, common.ErrEmptyParams
}
if arg.OrderType != "conditional" {
if arg.OrderType != orderConditional {
return nil, fmt.Errorf("%w for TPSL: %q", order.ErrTypeIsInvalid, arg.OrderType)
}
if arg.StopLossTriggerPrice <= 0 {
@@ -481,7 +481,7 @@ func (ok *Okx) PlaceChaseAlgoOrder(ctx context.Context, arg *AlgoOrderParams) (*
if *arg == (AlgoOrderParams{}) {
return nil, common.ErrEmptyParams
}
if arg.OrderType != "chase" {
if arg.OrderType != orderChase {
return nil, fmt.Errorf("%w: order type value 'chase' is only supported for chase orders", order.ErrTypeIsInvalid)
}
if (arg.MaxChaseType == "" || arg.MaxChaseValue == 0) &&
@@ -496,7 +496,7 @@ func (ok *Okx) PlaceTriggerAlgoOrder(ctx context.Context, arg *AlgoOrderParams)
if *arg == (AlgoOrderParams{}) {
return nil, common.ErrEmptyParams
}
if arg.OrderType != "trigger" {
if arg.OrderType != orderTrigger {
return nil, fmt.Errorf("%w for Trigger: %q", order.ErrTypeIsInvalid, arg.OrderType)
}
if arg.TriggerPrice <= 0 {

View File

@@ -1222,7 +1222,7 @@ func TestPlaceChaseAlgoOrder(t *testing.T) {
_, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg)
require.ErrorIs(t, err, order.ErrTypeIsInvalid)
arg.OrderType = "chase"
arg.OrderType = orderChase
arg.MaxChaseType = "percentage"
_, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg)
require.ErrorIs(t, err, errPriceTrackingNotSet)
@@ -1250,7 +1250,7 @@ func TestPlaceChaseAlgoOrder(t *testing.T) {
AlgoClientOrderID: "681096944655273984",
InstrumentID: mainPair.String(),
LimitPrice: 100.22,
OrderType: "chase",
OrderType: orderChase,
TradeMode: "cross",
Side: order.Sell.Lower(),
MaxChaseType: "distance",
@@ -1297,14 +1297,14 @@ func TestPlaceTrailingStopOrder(t *testing.T) {
assert.ErrorIs(t, err, common.ErrEmptyParams)
_, err = ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{Size: 2})
assert.ErrorIs(t, err, order.ErrTypeIsInvalid)
_, err = ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{Size: 2, OrderType: "move_order_stop"})
_, err = ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{Size: 2, OrderType: orderMoveOrderStop})
assert.ErrorIs(t, err, errPriceTrackingNotSet)
// Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder.
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
result, err := ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{
AlgoClientOrderID: "681096944655273984", CallbackRatio: 0.01,
InstrumentID: mainPair.String(), OrderType: "move_order_stop",
InstrumentID: mainPair.String(), OrderType: orderMoveOrderStop,
Side: order.Buy.Lower(), TradeMode: "isolated",
Size: 2, ActivePrice: 1234,
})
@@ -5843,26 +5843,35 @@ func TestOrderTypeString(t *testing.T) {
Expected string
Error error
}{
{OrderType: order.Market, TIF: order.UnknownTIF}: {Expected: orderMarket},
{OrderType: order.Limit, TIF: order.UnknownTIF}: {Expected: orderLimit},
{OrderType: order.Limit, TIF: order.PostOnly}: {Expected: orderPostOnly},
{OrderType: order.Limit, TIF: order.FillOrKill}: {Expected: orderFOK},
{OrderType: order.Limit, TIF: order.ImmediateOrCancel}: {Expected: orderIOC},
{OrderType: order.OptimalLimitIOC, TIF: order.UnknownTIF}: {Expected: orderOptimalLimitIOC},
{OrderType: order.MarketMakerProtection, TIF: order.UnknownTIF}: {Expected: "mmp"},
{OrderType: order.MarketMakerProtectionAndPostOnly, TIF: order.UnknownTIF}: {Expected: "mmp_and_post_only"},
{OrderType: order.Liquidation, TIF: order.UnknownTIF}: {Error: order.ErrUnsupportedOrderType},
{OrderType: order.OCO, TIF: order.UnknownTIF}: {Expected: "oco"},
{OrderType: order.TrailingStop, TIF: order.UnknownTIF}: {Expected: "move_order_stop"},
{OrderType: order.Chase, TIF: order.UnknownTIF}: {Expected: "chase"},
{OrderType: order.TWAP, TIF: order.UnknownTIF}: {Expected: "twap"},
{OrderType: order.ConditionalStop, TIF: order.UnknownTIF}: {Expected: "conditional"},
{OrderType: order.Trigger, TIF: order.UnknownTIF}: {Expected: "trigger"},
{OrderType: order.Market, TIF: order.UnknownTIF}: {Expected: orderMarket},
{OrderType: order.Limit, TIF: order.UnknownTIF}: {Expected: orderLimit},
{OrderType: order.Limit, TIF: order.PostOnly}: {Expected: orderPostOnly},
{OrderType: order.Market, TIF: order.FillOrKill}: {Expected: orderFOK},
{OrderType: order.Market, TIF: order.ImmediateOrCancel}: {Expected: orderIOC},
{OrderType: order.OptimalLimit, TIF: order.ImmediateOrCancel}: {Expected: orderOptimalLimitIOC},
{OrderType: order.MarketMakerProtection, TIF: order.UnknownTIF}: {Expected: orderMarketMakerProtection},
{OrderType: order.MarketMakerProtection, TIF: order.PostOnly}: {Expected: orderMarketMakerProtectionAndPostOnly},
{OrderType: order.Liquidation, TIF: order.UnknownTIF}: {Error: order.ErrUnsupportedOrderType},
{OrderType: order.OCO, TIF: order.UnknownTIF}: {Expected: orderOCO},
{OrderType: order.TrailingStop, TIF: order.UnknownTIF}: {Expected: orderMoveOrderStop},
{OrderType: order.Chase, TIF: order.UnknownTIF}: {Expected: orderChase},
{OrderType: order.TWAP, TIF: order.UnknownTIF}: {Expected: orderTWAP},
{OrderType: order.ConditionalStop, TIF: order.UnknownTIF}: {Expected: orderConditional},
{OrderType: order.Chase, TIF: order.GoodTillCancel}: {Expected: orderChase},
{OrderType: order.TWAP, TIF: order.ImmediateOrCancel}: {Expected: orderTWAP},
{OrderType: order.ConditionalStop, TIF: order.GoodTillDay}: {Expected: orderConditional},
{OrderType: order.Trigger, TIF: order.UnknownTIF}: {Expected: orderTrigger},
{OrderType: order.UnknownType, TIF: order.PostOnly}: {Expected: orderPostOnly},
{OrderType: order.UnknownType, TIF: order.FillOrKill}: {Expected: orderFOK},
{OrderType: order.UnknownType, TIF: order.ImmediateOrCancel}: {Expected: orderIOC},
}
for tc, val := range orderTypesToStringMap {
orderTypeString, err := orderTypeString(tc.OrderType, tc.TIF)
require.ErrorIs(t, err, val.Error)
assert.Equal(t, val.Expected, orderTypeString)
t.Run(tc.OrderType.String()+"/"+tc.TIF.String(), func(t *testing.T) {
t.Parallel()
orderTypeString, err := orderTypeString(tc.OrderType, tc.TIF)
require.ErrorIs(t, err, val.Error)
assert.Equal(t, val.Expected, orderTypeString)
})
}
}
@@ -6127,7 +6136,6 @@ func TestWsProcessSpreadTradesJSON(t *testing.T) {
func TestOrderTypeFromString(t *testing.T) {
t.Parallel()
orderTypeStrings := map[string]struct {
OType order.Type
TIF order.TimeInForce
@@ -6139,9 +6147,9 @@ func TestOrderTypeFromString(t *testing.T) {
"post_only": {OType: order.Limit, TIF: order.PostOnly},
"fok": {OType: order.Limit, TIF: order.FillOrKill},
"ioc": {OType: order.Limit, TIF: order.ImmediateOrCancel},
"optimal_limit_ioc": {OType: order.OptimalLimitIOC, TIF: order.ImmediateOrCancel},
"optimal_limit_ioc": {OType: order.OptimalLimit, TIF: order.ImmediateOrCancel},
"mmp": {OType: order.MarketMakerProtection},
"mmp_and_post_only": {OType: order.MarketMakerProtectionAndPostOnly, TIF: order.PostOnly},
"mmp_and_post_only": {OType: order.MarketMakerProtection, TIF: order.PostOnly},
"trigger": {OType: order.UnknownType, Error: order.ErrTypeIsInvalid},
"chase": {OType: order.Chase},
"move_order_stop": {OType: order.TrailingStop},
@@ -6149,10 +6157,13 @@ func TestOrderTypeFromString(t *testing.T) {
"abcd": {OType: order.UnknownType, Error: order.ErrTypeIsInvalid},
}
for s, exp := range orderTypeStrings {
oType, tif, err := orderTypeFromString(s)
require.ErrorIs(t, err, exp.Error)
assert.Equal(t, exp.OType, oType)
assert.Equal(t, exp.TIF.String(), tif.String(), s)
t.Run(s, func(t *testing.T) {
t.Parallel()
oType, tif, err := orderTypeFromString(s)
require.ErrorIs(t, err, exp.Error)
assert.Equal(t, exp.OType, oType)
assert.Equal(t, exp.TIF.String(), tif.String())
})
}
}

View File

@@ -41,19 +41,22 @@ const (
positionSideNet = "net"
)
// order types, margin balance types, and instrument types constants
const (
// orderLimit Limit order
orderLimit = "limit"
// orderMarket Market order
orderMarket = "market"
// orderPostOnly POST_ONLY order type
orderPostOnly = "post_only"
// orderFOK fill or kill order type
orderFOK = "fok"
// orderIOC IOC (immediate or cancel)
orderIOC = "ioc"
// orderOptimalLimitIOC OPTIMAL_LIMIT_IOC
orderOptimalLimitIOC = "optimal_limit_ioc"
orderLimit = "limit"
orderMarket = "market"
orderPostOnly = "post_only"
orderFOK = "fok"
orderIOC = "ioc"
orderOptimalLimitIOC = "optimal_limit_ioc"
orderConditional = "conditional"
orderMoveOrderStop = "move_order_stop"
orderChase = "chase"
orderTWAP = "twap"
orderTrigger = "trigger"
orderMarketMakerProtectionAndPostOnly = "mmp_and_post_only"
orderMarketMakerProtection = "mmp"
orderOCO = "oco"
// represents a margin balance type
marginBalanceReduce = "reduce"

View File

@@ -958,7 +958,7 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
return nil, err
}
return s.DeriveSubmitResponse(placeOrderResponse.OrderID)
case "trigger":
case orderTrigger:
result, err = ok.PlaceTriggerAlgoOrder(ctx, &AlgoOrderParams{
InstrumentID: pairString,
TradeMode: tradeMode,
@@ -970,7 +970,7 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
TriggerPrice: s.TriggerPrice,
TriggerPriceType: priceTypeString(s.TriggerPriceType),
})
case "conditional":
case orderConditional:
// Trigger Price and type are used as a stop losss trigger price and type.
result, err = ok.PlaceTakeProfitStopLossOrder(ctx, &AlgoOrderParams{
InstrumentID: pairString,
@@ -984,7 +984,7 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
StopLossOrderPrice: s.Price,
StopLossTriggerPriceType: priceTypeString(s.TriggerPriceType),
})
case "chase":
case orderChase:
if s.TrackingMode == order.UnknownTrackingMode {
return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode)
}
@@ -1002,7 +1002,7 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
MaxChaseType: s.TrackingMode.String(),
MaxChaseValue: s.TrackingValue,
})
case "move_order_stop":
case orderMoveOrderStop:
if s.TrackingMode == order.UnknownTrackingMode {
return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode)
}
@@ -1025,7 +1025,7 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
CallbackSpreadVariance: callbackSpread,
ActivePrice: s.TriggerPrice,
})
case "twap":
case orderTWAP:
if s.TrackingMode == order.UnknownTrackingMode {
return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode)
}
@@ -1050,7 +1050,7 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
LimitPrice: s.Price,
TimeInterval: kline.FifteenMin,
})
case "oco":
case orderOCO:
switch {
case s.RiskManagementModes.TakeProfit.Price <= 0:
return nil, fmt.Errorf("%w, take profit price is required", order.ErrPriceBelowMin)
@@ -1144,7 +1144,7 @@ func (ok *Okx) ModifyOrder(ctx context.Context, action *order.Modify) (*order.Mo
return nil, currency.ErrCurrencyPairEmpty
}
switch action.Type {
case order.UnknownType, order.Market, order.Limit, order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly:
case order.UnknownType, order.Market, order.Limit, order.OptimalLimit, order.MarketMakerProtection:
amendRequest := AmendOrderRequestParams{
InstrumentID: pairFormat.Format(action.Pair),
NewQuantity: action.Amount,
@@ -1248,7 +1248,7 @@ func (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error {
}
instrumentID := pairFormat.Format(ord.Pair)
switch ord.Type {
case order.UnknownType, order.Market, order.Limit, order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly:
case order.UnknownType, order.Market, order.Limit, order.OptimalLimit, order.MarketMakerProtection:
req := CancelOrderRequestParam{
InstrumentID: instrumentID,
OrderID: ord.OrderID,
@@ -1301,7 +1301,7 @@ func (ok *Okx) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.
return nil, currency.ErrCurrencyPairsEmpty
}
switch ord.Type {
case order.UnknownType, order.Market, order.Limit, order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly:
case order.UnknownType, order.Market, order.Limit, order.OptimalLimit, order.MarketMakerProtection:
if o[x].ClientID == "" && o[x].OrderID == "" {
return nil, fmt.Errorf("%w, order ID required for order of type %v", order.ErrOrderIDNotSet, o[x].Type)
}