Okx: Fix Instrument unmarshal (#1225)

* Okx: Fix Instrument unmarshal

This fixes: `json: invalid use of ,string struct tag, trying to unmarshal "" into float64`
when TwapSz or other fields are empty, which happened briefly today.

As a DriveBy it also simplifies the exposed Instrument type to native
types only.

* Okx: Add missing InstrumentFamily to Instrument

* Okx: Add Instrument Unmarshal tests

DriveBy: Removes stray newline in type conversions

* Okx: Fix empty line upsetting linter
This commit is contained in:
Gareth Kirwan
2023-06-13 06:27:01 +01:00
committed by GitHub
parent d6f0f6243a
commit 1f89048ddb
4 changed files with 194 additions and 53 deletions

View File

@@ -3125,3 +3125,100 @@ func TestGetIntervalEnum(t *testing.T) {
})
}
}
const instrumentJSON = `{"alias":"","baseCcy":"","category":"1","ctMult":"1","ctType":"linear","ctVal":"0.0001","ctValCcy":"BTC","expTime":"","instFamily":"BTC-USDC","instId":"BTC-USDC-SWAP","instType":"SWAP","lever":"125","listTime":"1666076190000","lotSz":"1","maxIcebergSz":"100000000.0000000000000000","maxLmtSz":"100000000","maxMktSz":"85000","maxStopSz":"85000","maxTriggerSz":"100000000.0000000000000000","maxTwapSz":"","minSz":"1","optType":"","quoteCcy":"","settleCcy":"USDC","state":"live","stk":"","tickSz":"0.1","uly":"BTC-USDC"}`
func TestInstrument(t *testing.T) {
t.Parallel()
var i Instrument
err := json.Unmarshal([]byte(instrumentJSON), &i)
if err != nil {
t.Error(err)
}
if i.Alias != "" {
t.Error("expected empty alias")
}
if i.BaseCurrency != "" {
t.Error("expected empty base currency")
}
if i.Category != "1" {
t.Error("expected 1 category")
}
if i.ContractMultiplier != "1" {
t.Error("expected 1 contract multiplier")
}
if i.ContractType != "linear" {
t.Error("expected linear contract type")
}
if i.ContractValue != "0.0001" {
t.Error("expected 0.0001 contract value")
}
if i.ContractValueCurrency != currency.BTC.String() {
t.Error("expected BTC contract value currency")
}
if !i.ExpTime.IsZero() {
t.Error("expected empty expiry time")
}
if i.InstrumentFamily != "BTC-USDC" {
t.Error("expected BTC-USDC instrument family")
}
if i.InstrumentID != "BTC-USDC-SWAP" {
t.Error("expected BTC-USDC-SWAP instrument ID")
}
if i.InstrumentType != asset.PerpetualSwap {
t.Error("expected SWAP instrument type")
}
if i.MaxLeverage != 125 {
t.Error("expected 125 leverage")
}
if i.ListTime.UnixMilli() != 1666076190000 {
t.Error("expected 1666076190000 listing time")
}
if i.LotSize != 1 {
t.Error("expected 1 lot size")
}
if i.MaxSpotIcebergSize != 100000000.0000000000000000 {
t.Error("expected 100000000.0000000000000000 max iceberg order size")
}
if i.MaxQuantityOfSpotLimitOrder != 100000000 {
t.Error("expected 100000000 max limit order size")
}
if i.MaxQuantityOfMarketLimitOrder != 85000 {
t.Error("expected 85000 max market order size")
}
if i.MaxStopSize != 85000 {
t.Error("expected 85000 max stop order size")
}
if i.MaxTriggerSize != 100000000.0000000000000000 {
t.Error("expected 100000000.0000000000000000 max trigger order size")
}
if i.MaxQuantityOfSpotTwapLimitOrder != 0 {
t.Error("expected empty max TWAP size")
}
if i.MinimumOrderSize != 1 {
t.Error("expected 1 min size")
}
if i.OptionType != "" {
t.Error("expected empty option type")
}
if i.QuoteCurrency != "" {
t.Error("expected empty quote currency")
}
if i.SettlementCurrency != currency.USDC.String() {
t.Error("expected USDC settlement currency")
}
if i.State != "live" {
t.Error("expected live state")
}
if i.StrikePrice != "" {
t.Error("expected empty strike price")
}
if i.TickSize != 0.1 {
t.Error("expected 0.1 tick size")
}
if i.Underlying != "BTC-USDC" {
t.Error("expected BTC-USDC underlying")
}
}

View File

@@ -2,7 +2,6 @@ package okx
import (
"encoding/json"
"regexp"
"strconv"
"strings"
"time"
@@ -65,40 +64,84 @@ func (a *okxUnixMilliTime) Time() time.Time {
return time.UnixMilli(int64(*a))
}
// numbersOnlyRegexp for checking the value is numerics only
var numbersOnlyRegexp = regexp.MustCompile(`^\d*$`)
type okxTime struct {
time.Time
}
// UnmarshalJSON deserializes byte data to okxTime instance.
func (t *okxTime) UnmarshalJSON(data []byte) error {
var num string
err := json.Unmarshal(data, &num)
if err != nil {
return err
}
if num == "" {
return nil
}
value, err := strconv.ParseInt(num, 10, 64)
if err != nil {
return err
}
t.Time = time.UnixMilli(value)
return nil
}
type okxAssetType struct {
asset.Item
}
// UnmarshalJSON deserializes JSON, and timestamp information.
func (a *okxAssetType) UnmarshalJSON(data []byte) error {
var t string
err := json.Unmarshal(data, &t)
if err != nil {
return err
}
a.Item, err = GetAssetTypeFromInstrumentType(strings.ToUpper(t))
return err
}
// UnmarshalJSON deserializes JSON, and timestamp information.
func (a *Instrument) UnmarshalJSON(data []byte) error {
type Alias Instrument
chil := &struct {
*Alias
ListTime string `json:"listTime"`
ExpTime string `json:"expTime"`
InstrumentType string `json:"instType"`
ListTime okxTime `json:"listTime"`
ExpTime okxTime `json:"expTime"`
InstrumentType okxAssetType `json:"instType"`
MaxLeverage okxNumericalValue `json:"lever"`
TickSize okxNumericalValue `json:"tickSz"`
LotSize okxNumericalValue `json:"lotSz"`
MinimumOrderSize okxNumericalValue `json:"minSz"`
MaxQuantityOfSpotLimitOrder okxNumericalValue `json:"maxLmtSz"`
MaxQuantityOfMarketLimitOrder okxNumericalValue `json:"maxMktSz"`
MaxQuantityOfSpotTwapLimitOrder okxNumericalValue `json:"maxTwapSz"`
MaxSpotIcebergSize okxNumericalValue `json:"maxIcebergSz"`
MaxTriggerSize okxNumericalValue `json:"maxTriggerSz"`
MaxStopSize okxNumericalValue `json:"maxStopSz"`
}{
Alias: (*Alias)(a),
}
err := json.Unmarshal(data, chil)
if err != nil {
return err
}
if numbersOnlyRegexp.MatchString(chil.ListTime) {
var val int
if val, err = strconv.Atoi(chil.ListTime); err == nil {
a.ListTime = time.UnixMilli(int64(val))
}
}
if numbersOnlyRegexp.MatchString(chil.ExpTime) {
var val int
if val, err = strconv.Atoi(chil.ExpTime); err == nil {
a.ExpTime = time.UnixMilli(int64(val))
}
}
chil.InstrumentType = strings.ToUpper(chil.InstrumentType)
if a.InstrumentType, err = GetAssetTypeFromInstrumentType(chil.InstrumentType); err != nil {
if err := json.Unmarshal(data, chil); err != nil {
return err
}
a.ListTime = chil.ListTime.Time
a.ExpTime = chil.ExpTime.Time
a.InstrumentType = chil.InstrumentType.Item
a.MaxLeverage = chil.MaxLeverage.Float64()
a.TickSize = chil.TickSize.Float64()
a.LotSize = chil.LotSize.Float64()
a.MinimumOrderSize = chil.MinimumOrderSize.Float64()
a.MaxQuantityOfSpotLimitOrder = chil.MaxQuantityOfSpotLimitOrder.Float64()
a.MaxQuantityOfMarketLimitOrder = chil.MaxQuantityOfMarketLimitOrder.Float64()
a.MaxQuantityOfSpotTwapLimitOrder = chil.MaxQuantityOfSpotTwapLimitOrder.Float64()
a.MaxSpotIcebergSize = chil.MaxSpotIcebergSize.Float64()
a.MaxTriggerSize = chil.MaxTriggerSize.Float64()
a.MaxStopSize = chil.MaxStopSize.Float64()
return nil
}

View File

@@ -274,33 +274,34 @@ type InstrumentsFetchParams struct {
// Instrument representing an instrument with open contract.
type Instrument struct {
InstrumentType asset.Item `json:"instType"`
InstrumentID string `json:"instId"`
Underlying string `json:"uly"`
Category string `json:"category"`
BaseCurrency string `json:"baseCcy"`
QuoteCurrency string `json:"quoteCcy"`
SettlementCurrency string `json:"settleCcy"`
ContractValue string `json:"ctVal"`
ContractMultiplier string `json:"ctMult"`
ContractValueCurrency string `json:"ctValCcy"`
OptionType string `json:"optType"`
StrikePrice string `json:"stk"`
ListTime time.Time `json:"listTime"`
ExpTime time.Time `json:"expTime"`
MaxLeverage okxNumericalValue `json:"lever"`
TickSize okxNumericalValue `json:"tickSz"`
LotSize okxNumericalValue `json:"lotSz"`
MinimumOrderSize okxNumericalValue `json:"minSz"`
ContractType string `json:"ctType"`
Alias string `json:"alias"`
State string `json:"state"`
MaxQuantityOfSpotLimitOrder float64 `json:"maxLmtSz,string"`
MaxQuantityOfMarketLimitOrder float64 `json:"maxMktSz,string"`
MaxQuantityOfSpotTwapLimitOrder float64 `json:"maxTwapSz,string"`
MaxSpotIcebergSize float64 `json:"maxIcebergSz,string"`
MaxTriggerSize float64 `json:"maxTriggerSz,string"`
MaxStopSize float64 `json:"maxStopSz,string"`
InstrumentType asset.Item `json:"instType"`
InstrumentID string `json:"instId"`
InstrumentFamily string `json:"instFamily"`
Underlying string `json:"uly"`
Category string `json:"category"`
BaseCurrency string `json:"baseCcy"`
QuoteCurrency string `json:"quoteCcy"`
SettlementCurrency string `json:"settleCcy"`
ContractValue string `json:"ctVal"`
ContractMultiplier string `json:"ctMult"`
ContractValueCurrency string `json:"ctValCcy"`
OptionType string `json:"optType"`
StrikePrice string `json:"stk"`
ListTime time.Time `json:"listTime"`
ExpTime time.Time `json:"expTime"`
MaxLeverage float64 `json:"lever"`
TickSize float64 `json:"tickSz"`
LotSize float64 `json:"lotSz"`
MinimumOrderSize float64 `json:"minSz"`
ContractType string `json:"ctType"`
Alias string `json:"alias"`
State string `json:"state"`
MaxQuantityOfSpotLimitOrder float64 `json:"maxLmtSz"`
MaxQuantityOfMarketLimitOrder float64 `json:"maxMktSz"`
MaxQuantityOfSpotTwapLimitOrder float64 `json:"maxTwapSz"`
MaxSpotIcebergSize float64 `json:"maxIcebergSz"`
MaxTriggerSize float64 `json:"maxTriggerSz"`
MaxStopSize float64 `json:"maxStopSz"`
}
// DeliveryHistoryDetail holds instrument id and delivery price information detail

View File

@@ -341,8 +341,8 @@ func (ok *Okx) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) err
limits[x] = order.MinMaxLevel{
Pair: pair,
Asset: a,
PriceStepIncrementSize: insts[x].TickSize.Float64(),
MinAmount: insts[x].MinimumOrderSize.Float64(),
PriceStepIncrementSize: insts[x].TickSize,
MinAmount: insts[x].MinimumOrderSize,
}
}