From ce134a0a1d7778047ccb92382f909e76688079f5 Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Wed, 11 Jun 2025 08:43:36 +1000 Subject: [PATCH] types/time, exchanges/kraken: Refactor spot/futures time types (#1936) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * types/time, exchanges/kraken: Refactor spot/futures time types - Updated WSFuturesTickerData, WsFuturesTradeData, and other related structs to replace int64 timestamps with the new types.Time. - Adjusted related test cases to accommodate changes in timestamp handling. - Modified functions in kraken_wrapper and kraken_websocket to utilise the new Time type for better time management. - Enhanced JSON unmarshalling in the Time type to handle various timestamp formats, including "0" and "0.0". * refactor: Update JSON field name/types and improve perf * types/time: revert to more precise check, just check for 'n' * refactor: Ryan the F1 driver is so back 🏎️ * refactor: Enhance UnmarshalJSON error handling and simplify test cases * ocd: Fix trigger --- exchanges/kraken/futures_types.go | 406 ++++++++++++++------------- exchanges/kraken/kraken_test.go | 8 +- exchanges/kraken/kraken_types.go | 178 ++++++------ exchanges/kraken/kraken_websocket.go | 6 +- exchanges/kraken/kraken_wrapper.go | 40 +-- types/time.go | 12 +- types/time_test.go | 91 +++--- 7 files changed, 360 insertions(+), 381 deletions(-) diff --git a/exchanges/kraken/futures_types.go b/exchanges/kraken/futures_types.go index cd3a48b0..d3b3f043 100644 --- a/exchanges/kraken/futures_types.go +++ b/exchanges/kraken/futures_types.go @@ -2,8 +2,10 @@ package kraken import ( "sync" + "time" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/types" ) var ( @@ -27,31 +29,31 @@ var ( // WSFuturesTickerData stores ws ticker data for futures websocket type WSFuturesTickerData struct { - Time int64 `json:"time"` - Feed string `json:"feed"` - ProductID string `json:"product_id"` - Bid float64 `json:"bid"` - Ask float64 `json:"ask"` - BidSize float64 `json:"bid_size"` - AskSize float64 `json:"ask_size"` - Volume float64 `json:"volume"` - DTM float64 `json:"dtm"` - Leverage string `json:"leverage"` - Index float64 `json:"index"` - Premium float64 `json:"premium"` - Last float64 `json:"last"` - Change float64 `json:"change"` - Suspended bool `json:"suspended"` - Tag string `json:"tag"` - Pair string `json:"pair"` - OpenInterest float64 `json:"openinterest"` - MarkPrice float64 `json:"markPrice"` - MaturityTime int64 `json:"maturityTime"` - FundingRate float64 `json:"funding_rate"` - FundingRatePrediction float64 `json:"funding_rate_prediction"` - RelativeFundingRate float64 `json:"relative_funding_rate"` - RelativeFundingRatePrediction float64 `json:"relative_funding_rate_prediction"` - NextFundingRateTime int64 `json:"next_funding_rate_time"` + Time types.Time `json:"time"` + Feed string `json:"feed"` + ProductID string `json:"product_id"` + Bid float64 `json:"bid"` + Ask float64 `json:"ask"` + BidSize float64 `json:"bid_size"` + AskSize float64 `json:"ask_size"` + Volume float64 `json:"volume"` + DTM float64 `json:"dtm"` + Leverage string `json:"leverage"` + Index float64 `json:"index"` + Premium float64 `json:"premium"` + Last float64 `json:"last"` + Change float64 `json:"change"` + Suspended bool `json:"suspended"` + Tag string `json:"tag"` + Pair string `json:"pair"` + OpenInterest float64 `json:"openinterest"` + MarkPrice float64 `json:"markPrice"` + MaturityTime types.Time `json:"maturityTime"` + FundingRate float64 `json:"funding_rate"` + FundingRatePrediction float64 `json:"funding_rate_prediction"` + RelativeFundingRate float64 `json:"relative_funding_rate"` + RelativeFundingRatePrediction float64 `json:"relative_funding_rate_prediction"` + NextFundingRateTime types.Time `json:"next_funding_rate_time"` } // WsFuturesTradeData stores public trade data for futures websocket @@ -59,14 +61,14 @@ type WsFuturesTradeData struct { Feed string `json:"feed"` ProductID string `json:"product_id"` Trades []struct { - Feed string `json:"feed"` - ProductID string `json:"product_id"` - Side string `json:"side"` - ProductType string `json:"type"` - Seq int64 `json:"seq"` - Time int64 `json:"time"` - Quantity float64 `json:"qty"` - Price float64 `json:"price"` + Feed string `json:"feed"` + ProductID string `json:"product_id"` + Side string `json:"side"` + ProductType string `json:"type"` + Seq int64 `json:"seq"` + Time types.Time `json:"time"` + Quantity float64 `json:"qty"` + Price float64 `json:"price"` } `json:"trades"` } @@ -103,17 +105,17 @@ type WsVerboseOpenOrders struct { Feed string `json:"feed"` Account string `json:"account"` Orders []struct { - Instrument string `json:"instrument"` - Time int64 `json:"time"` - LastUpdateTime int64 `json:"last_update_time"` - Qty float64 `json:"qty"` - Filled float64 `json:"filled"` - LimitPrice float64 `json:"limit_price"` - StopPrice float64 `json:"stop_price"` - OrderType string `json:"type"` - OrderID string `json:"order_id"` - Direction int64 `json:"direction"` - ReduceOnly bool `json:"reduce_only"` + Instrument string `json:"instrument"` + Time types.Time `json:"time"` + LastUpdateTime types.Time `json:"last_update_time"` + Qty float64 `json:"qty"` + Filled float64 `json:"filled"` + LimitPrice float64 `json:"limit_price"` + StopPrice float64 `json:"stop_price"` + OrderType string `json:"type"` + OrderID string `json:"order_id"` + Direction int64 `json:"direction"` + ReduceOnly bool `json:"reduce_only"` } `json:"orders"` } @@ -161,16 +163,16 @@ type WsFuturesFillsData struct { Feed string `json:"feed"` Account string `json:"account"` Fills []struct { - Instrument string `json:"instrument"` - Time int64 `json:"time"` - Price float64 `json:"price"` - Seq int64 `json:"seq"` - Buy bool `json:"buy"` - Quantity float64 `json:"qty"` - OrderID string `json:"order_id"` - ClientOrderID string `json:"cli_order_id"` - FillID string `json:"fill_id"` - FillType string `json:"fill_type"` + Instrument string `json:"instrument"` + Time types.Time `json:"time"` + Price float64 `json:"price"` + Seq int64 `json:"seq"` + Buy bool `json:"buy"` + Quantity float64 `json:"qty"` + OrderID string `json:"order_id"` + ClientOrderID string `json:"cli_order_id"` + FillID string `json:"fill_id"` + FillType string `json:"fill_type"` } `json:"fills"` } @@ -179,17 +181,17 @@ type WsFuturesOpenOrders struct { Feed string `json:"feed"` Account string `json:"account"` Orders []struct { - Instrument string `json:"instrument"` - Time int64 `json:"time"` - LastUpdateTime int64 `json:"last_update_time"` - Qty float64 `json:"qty"` - Filled float64 `json:"filled"` - LimitPrice float64 `json:"limit_price"` - StopPrice float64 `json:"stop_price"` - OrderType string `json:"order_type"` - OrderID string `json:"order_id"` - Direction string `json:"direction"` - ReduceOnly bool `json:"reduce_only"` + Instrument string `json:"instrument"` + Time types.Time `json:"time"` + LastUpdateTime types.Time `json:"last_update_time"` + Qty float64 `json:"qty"` + Filled float64 `json:"filled"` + LimitPrice float64 `json:"limit_price"` + StopPrice float64 `json:"stop_price"` + OrderType string `json:"order_type"` + OrderID string `json:"order_id"` + Direction string `json:"direction"` + ReduceOnly bool `json:"reduce_only"` } `json:"orders"` } @@ -214,11 +216,11 @@ type WsAccountBalancesAndMargin struct { type WsFuturesNotifications struct { Feed string `json:"feed"` Notifications []struct { - ID int64 `json:"id"` - NotificationType string `json:"notificationType"` - Priority string `json:"priority"` - Note string `json:"note"` - EffectiveTime int64 `json:"effective_time"` + ID int64 `json:"id"` + NotificationType string `json:"notificationType"` + Priority string `json:"priority"` + Note string `json:"note"` + EffectiveTime types.Time `json:"effective_time"` } } @@ -229,7 +231,7 @@ type assetTranslatorStore struct { // FuturesOrderbookData stores orderbook data for futures type FuturesOrderbookData struct { - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` Orderbook struct { Bids [][2]float64 `json:"bids"` Asks [][2]float64 `json:"asks"` @@ -238,21 +240,21 @@ type FuturesOrderbookData struct { // TimeResponse type type TimeResponse struct { - Unixtime int64 `json:"unixtime"` - Rfc1123 string `json:"rfc1123"` + Unixtime types.Time `json:"unixtime"` + Rfc1123 string `json:"rfc1123"` } // FuturesInstrumentData stores info for futures market type FuturesInstrumentData struct { Instruments []struct { - Symbol string `json:"symbol"` - FutureType string `json:"type"` - Underlying string `json:"underlying"` - OpeningDate string `json:"openingDate"` - LastTradingTime string `json:"lastTradingTime"` - TickSize float64 `json:"tickSize"` - ContractSize float64 `json:"contractSize"` - Tradable bool `json:"tradeable"` + Symbol string `json:"symbol"` + FutureType string `json:"type"` + Underlying string `json:"underlying"` + OpeningDate time.Time `json:"openingDate"` + LastTradingTime time.Time `json:"lastTradingTime"` + TickSize float64 `json:"tickSize"` + ContractSize float64 `json:"contractSize"` + Tradable bool `json:"tradeable"` MarginLevels []struct { Contracts float64 `json:"contracts"` InitialMargin float64 `json:"initialMargin"` @@ -264,34 +266,34 @@ type FuturesInstrumentData struct { // FuturesTradeHistoryData stores trade history data for futures type FuturesTradeHistoryData struct { History []struct { - Time string `json:"time"` - TradeID int64 `json:"trade_id"` - Price float64 `json:"price"` - Size float64 `json:"size"` - Side string `json:"side"` - TradeType string `json:"type"` + Time time.Time `json:"time"` + TradeID int64 `json:"trade_id"` + Price float64 `json:"price"` + Size float64 `json:"size"` + Side string `json:"side"` + TradeType string `json:"type"` } `json:"history"` } // FuturesTickersData stores info for futures tickers type FuturesTickersData struct { Tickers []FuturesTicker `json:"tickers"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // FuturesTickerData stores info for futures ticker type FuturesTickerData struct { Ticker FuturesTicker `json:"ticker"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // FuturesEditedOrderData stores an edited order's data type FuturesEditedOrderData struct { - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` EditStatus struct { - Status string `json:"status"` - OrderID string `json:"orderId"` - ReceivedTime string `json:"receivedTime"` + Status string `json:"status"` + OrderID string `json:"orderId"` + ReceivedTime time.Time `json:"receivedTime"` OrderEvents []struct { Old FuturesOrderData `json:"old"` New FuturesOrderData `json:"new"` @@ -303,34 +305,34 @@ type FuturesEditedOrderData struct { // FuturesTicker holds futures ticker data type FuturesTicker struct { - Tag string `json:"tag"` - Pair string `json:"pair"` - Symbol string `json:"symbol"` - MarkPrice float64 `json:"markPrice"` - Bid float64 `json:"bid"` - BidSize float64 `json:"bidSize"` - Ask float64 `json:"ask"` - AskSize float64 `json:"askSize"` - Vol24h float64 `json:"vol24h"` - OpenInterest float64 `json:"openInterest"` - Open24H float64 `json:"open24h"` - Last float64 `json:"last"` - LastTime string `json:"lastTime"` - LastSize float64 `json:"lastSize"` - Suspended bool `json:"suspended"` - FundingRate float64 `json:"fundingRate"` - FundingRatePrediction float64 `json:"fundingRatePrediction"` - IndexPrice float64 `json:"indexPrice"` - PostOnly bool `json:"postOnly"` - Change24H float64 `json:"change24h"` + Tag string `json:"tag"` + Pair string `json:"pair"` + Symbol string `json:"symbol"` + MarkPrice float64 `json:"markPrice"` + Bid float64 `json:"bid"` + BidSize float64 `json:"bidSize"` + Ask float64 `json:"ask"` + AskSize float64 `json:"askSize"` + Vol24h float64 `json:"vol24h"` + OpenInterest float64 `json:"openInterest"` + Open24H float64 `json:"open24h"` + Last float64 `json:"last"` + LastTime time.Time `json:"lastTime"` + LastSize float64 `json:"lastSize"` + Suspended bool `json:"suspended"` + FundingRate float64 `json:"fundingRate"` + FundingRatePrediction float64 `json:"fundingRatePrediction"` + IndexPrice float64 `json:"indexPrice"` + PostOnly bool `json:"postOnly"` + Change24H float64 `json:"change24h"` } // FuturesSendOrderData stores send order data type FuturesSendOrderData struct { SendStatus struct { - OrderID string `json:"order_id"` - Status string `json:"status"` - ReceivedTime string `json:"receivedTime"` + OrderID string `json:"order_id"` + Status string `json:"status"` + ReceivedTime time.Time `json:"receivedTime"` OrderEvents []struct { UID string `json:"uid"` Order FuturesOrderData `json:"order"` @@ -338,71 +340,71 @@ type FuturesSendOrderData struct { DataType string `json:"type"` } `json:"orderEvents"` } `json:"sendStatus"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // FuturesOrderData stores order data type FuturesOrderData struct { - OrderID string `json:"orderId"` - ClientOrderID string `json:"cliOrderId"` - OrderType string `json:"type"` - Symbol string `json:"symbol"` - Side string `json:"side"` - Quantity float64 `json:"quantity"` - Filled float64 `json:"filled"` - LimitPrice float64 `json:"limitPrice"` - ReduceOnly bool `json:"reduceOnly"` - Timestamp string `json:"timestamp"` - LastUpdateTimestamp string `json:"lastUpdateTimestamp"` + OrderID string `json:"orderId"` + ClientOrderID string `json:"cliOrderId"` + OrderType string `json:"type"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Quantity float64 `json:"quantity"` + Filled float64 `json:"filled"` + LimitPrice float64 `json:"limitPrice"` + ReduceOnly bool `json:"reduceOnly"` + Timestamp time.Time `json:"timestamp"` + LastUpdateTimestamp time.Time `json:"lastUpdateTimestamp"` } // FuturesCancelOrderData stores cancel order data for futures type FuturesCancelOrderData struct { CancelStatus struct { - Status string `json:"status"` - OrderID string `json:"order_id"` - ReceivedTime string `json:"receivedTime"` + Status string `json:"status"` + OrderID string `json:"order_id"` + ReceivedTime time.Time `json:"receivedTime"` OrderEvents []struct { UID string `json:"uid"` Order FuturesOrderData `json:"order"` DataType string `json:"type"` } `json:"orderEvents"` } `json:"cancelStatus"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // FuturesFillsData stores fills data type FuturesFillsData struct { Fills []struct { - FillID string `json:"fill_id"` - Symbol string `json:"symbol"` - Side string `json:"buy"` - OrderID string `json:"order_id"` - Size float64 `json:"size"` - Price float64 `json:"price"` - FillTime string `json:"fillTime"` - FillType string `json:"fillType"` + FillID string `json:"fill_id"` + Symbol string `json:"symbol"` + Side string `json:"side"` + OrderID string `json:"order_id"` + Size float64 `json:"size"` + Price float64 `json:"price"` + FillTime time.Time `json:"fillTime"` + FillType string `json:"fillType"` } `json:"fills"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // FuturesTransferData stores transfer data type FuturesTransferData struct { - Result string `json:"result"` - ServerTime string `json:"serverTime"` + Result string `json:"result"` + ServerTime time.Time `json:"serverTime"` } // FuturesOpenPositions stores open positions data for futures type FuturesOpenPositions struct { OpenPositions []struct { - Side string `json:"side"` - Symbol string `json:"symbol"` - Price float64 `json:"price"` - FillTime string `json:"fillTime"` - Size float64 `json:"size"` - UnrealizedFunding float64 `json:"unrealizedFunding"` + Side string `json:"side"` + Symbol string `json:"symbol"` + Price float64 `json:"price"` + FillTime time.Time `json:"fillTime"` + Size float64 `json:"size"` + UnrealizedFunding float64 `json:"unrealizedFunding"` } `json:"openPositions"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // FuturesNotificationData stores notification data @@ -413,7 +415,7 @@ type FuturesNotificationData struct { Note string `json:"note"` EffectiveTime string `json:"effectiveTime"` } `json:"notifications"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // FuturesAccountsData stores account data @@ -449,9 +451,9 @@ type AccountsData struct { // CancelAllOrdersData stores order data for all cancelled orders type CancelAllOrdersData struct { CancelStatus struct { - ReceivedTime string `json:"receivedTime"` - CancelOnly string `json:"cancelOnly"` - Status string `json:"status"` + ReceivedTime time.Time `json:"receivedTime"` + CancelOnly string `json:"cancelOnly"` + Status string `json:"status"` CancelledOrders []struct { OrderID string `json:"order_id"` } `json:"cancelledOrders"` @@ -461,17 +463,17 @@ type CancelAllOrdersData struct { Order FuturesOrderData `json:"order"` DataType string `json:"type"` } `json:"cancelStatus"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // CancelOrdersAfterData stores data of all orders after a certain time that are cancelled type CancelOrdersAfterData struct { Result string `json:"result"` Status struct { - CurrentTime string `json:"currentTime"` - TriggerTime string `json:"triggerTime"` + CurrentTime time.Time `json:"currentTime"` + TriggerTime time.Time `json:"triggerTime"` } `json:"status"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // RecentOrderData stores order data of a recent order @@ -491,20 +493,20 @@ type RecentOrderData struct { // FOpenOrdersData stores open orders data for futures type FOpenOrdersData struct { - OrderID string `json:"order_id"` - ClientOrderID string `json:"cliOrdId"` - Symbol string `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"orderType"` - LimitPrice float64 `json:"limitPrice"` - StopPrice float64 `json:"stopPrice"` - UnfilledSize float64 `json:"unfilledSize"` - ReceivedTime string `json:"receivedTime"` - Status string `json:"status"` - FilledSize float64 `json:"filledSize"` - ReduceOnly bool `json:"reduceOnly"` - TriggerSignal string `json:"triggerSignal"` - LastUpdateTime string `json:"lastUpdateTime"` + OrderID string `json:"order_id"` + ClientOrderID string `json:"cliOrdId"` + Symbol string `json:"symbol"` + Side string `json:"side"` + OrderType string `json:"orderType"` + LimitPrice float64 `json:"limitPrice"` + StopPrice float64 `json:"stopPrice"` + UnfilledSize float64 `json:"unfilledSize"` + ReceivedTime time.Time `json:"receivedTime"` + Status string `json:"status"` + FilledSize float64 `json:"filledSize"` + ReduceOnly bool `json:"reduceOnly"` + TriggerSignal string `json:"triggerSignal"` + LastUpdateTime time.Time `json:"lastUpdateTime"` } // FuturesRecentOrdersData stores recent orders data @@ -540,17 +542,17 @@ type FuturesRecentOrdersData struct { // BatchOrderData stores batch order data type BatchOrderData struct { - Result string `json:"result"` - ServerTime string `json:"serverTime"` + Result string `json:"result"` + ServerTime time.Time `json:"serverTime"` BatchStatus []struct { - Status string `json:"status"` - OrderTag string `json:"order_tag"` - OrderID string `json:"order_id"` - DateTimeReceived string `json:"dateTimeReceived"` + Status string `json:"status"` + OrderTag string `json:"order_tag"` + OrderID string `json:"order_id"` + DateTimeReceived time.Time `json:"dateTimeReceived"` OrderEvents []struct { OrderPlaced FuturesOrderData `json:"orderPlaced"` ReduceOnly bool `json:"reduceOnly"` - Timestamp string `json:"timestamp"` + Timestamp time.Time `json:"timestamp"` OldEditedOrder FuturesOrderData `json:"old"` NewEditedOrder FuturesOrderData `json:"new"` UID string `json:"uid"` @@ -584,7 +586,7 @@ type ExecutionData struct { type FuturesOpenOrdersData struct { Result string `json:"result"` OpenOrders []FOpenOrdersData `json:"openOrders"` - ServerTime string `json:"serverTime"` + ServerTime time.Time `json:"serverTime"` } // FuturesPublicTrades returns public trade data @@ -604,34 +606,34 @@ type FuturesPublicTrades struct { Quantity float64 `json:"quantity,string"` TakerOrder FutureTradeOrder `json:"takerOrder"` TakerOrderData FuturesTradeOrderData `json:"takerOrderData"` - Timestamp int64 `json:"timestamp"` + Timestamp types.Time `json:"timestamp"` UID string `json:"uid"` UsdValue float64 `json:"usdValue,string"` } `json:"execution"` TakerReducedQuantity string `json:"takerReducedQuantity"` } `json:"execution"` } `json:"event"` - Timestamp int64 `json:"timestamp"` - UID string `json:"uid"` + Timestamp types.Time `json:"timestamp"` + UID string `json:"uid"` } `json:"elements"` Len int64 `json:"len"` } // FutureTradeOrder holds details about the order for a futures trade type FutureTradeOrder struct { - AccountUID string `json:"accountUid"` - ClientID string `json:"clientId"` - Direction string `json:"direction"` - Filled string `json:"filled"` - LastUpdateTimestamp int64 `json:"lastUpdateTimestamp"` - LimitPrice float64 `json:"limitPrice,string"` - OrderType string `json:"orderType"` - Quantity float64 `json:"quantity,string"` - ReduceOnly bool `json:"reduceOnly"` - SpotData string `json:"spotData"` - Timestamp int64 `json:"timestamp"` - Tradeable string `json:"tradeable"` - UID string `json:"uid"` + AccountUID string `json:"accountUid"` + ClientID string `json:"clientId"` + Direction string `json:"direction"` + Filled string `json:"filled"` + LastUpdateTimestamp types.Time `json:"lastUpdateTimestamp"` + LimitPrice float64 `json:"limitPrice,string"` + OrderType string `json:"orderType"` + Quantity float64 `json:"quantity,string"` + ReduceOnly bool `json:"reduceOnly"` + SpotData string `json:"spotData"` + Timestamp types.Time `json:"timestamp"` + Tradeable string `json:"tradeable"` + UID string `json:"uid"` } // FuturesTradeOrderData holds additional order details for a futures trade order @@ -648,10 +650,10 @@ type FuturesCandles struct { // FuturesCandle is an individual candle type FuturesCandle struct { - Close float64 `json:"close,string"` - High float64 `json:"high,string"` - Low float64 `json:"low,string"` - Open float64 `json:"open,string"` - Time int64 `json:"time"` - Volume float64 `json:"volume,string"` + Close float64 `json:"close,string"` + High float64 `json:"high,string"` + Low float64 `json:"low,string"` + Open float64 `json:"open,string"` + Time types.Time `json:"time"` + Volume float64 `json:"volume,string"` } diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index 23af0682..50b74b70 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -1283,7 +1283,7 @@ func TestWsOpenOrders(t *testing.T) { assert.Equal(t, order.Pending, v.Status, "order status") assert.Equal(t, 0.0, v.Price, "price") assert.Equal(t, 0.0001, v.Amount, "amount") - assert.Equal(t, time.UnixMicro(1692851641361371).UTC(), v.Date, "Date") + assert.Equal(t, time.UnixMicro(1692851641361371).UTC(), v.Date.UTC(), "Date") case 4: assert.Equal(t, "OKB55A-UEMMN-YUXM2A", v.OrderID, "OrderID") assert.Equal(t, order.Open, v.Status, "order status") @@ -1300,7 +1300,7 @@ func TestWsOpenOrders(t *testing.T) { assert.Equal(t, 0.0001, v.ExecutedAmount, "ExecutedAmount") assert.Equal(t, 26425.2, v.AverageExecutedPrice, "AverageExecutedPrice") assert.Equal(t, 0.00687, v.Fee, "Fee") - assert.Equal(t, time.UnixMicro(1692851641361447).UTC(), v.LastUpdated, "LastUpdated") + assert.Equal(t, time.UnixMicro(1692851641361447).UTC(), v.LastUpdated.UTC(), "LastUpdated") case 1: assert.Equal(t, "OGTT3Y-C6I3P-XRI6HR", v.OrderID, "OrderID") assert.Equal(t, order.UnknownStatus, v.Status, "order status") @@ -1310,7 +1310,7 @@ func TestWsOpenOrders(t *testing.T) { case 0: assert.Equal(t, "OGTT3Y-C6I3P-XRI6HR", v.OrderID, "OrderID") assert.Equal(t, order.Closed, v.Status, "order status") - assert.Equal(t, time.UnixMicro(1692675961789052).UTC(), v.LastUpdated, "LastUpdated") + assert.Equal(t, time.UnixMicro(1692675961789052).UTC(), v.LastUpdated.UTC(), "LastUpdated") assert.Equal(t, 10.00345345, v.ExecutedAmount, "ExecutedAmount") assert.Equal(t, 0.001, v.Fee, "Fee") assert.Equal(t, 34.5, v.AverageExecutedPrice, "AverageExecutedPrice") @@ -1424,7 +1424,7 @@ func TestGetCharts(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, resp.Candles) - end := time.UnixMilli(resp.Candles[0].Time) + end := resp.Candles[0].Time.Time() _, err = k.GetFuturesCharts(t.Context(), "1d", "spot", futuresTestPair, end.Add(-time.Hour*24*7), end) require.NoError(t, err) } diff --git a/exchanges/kraken/kraken_types.go b/exchanges/kraken/kraken_types.go index 3cb0ca7a..e0e1960b 100644 --- a/exchanges/kraken/kraken_types.go +++ b/exchanges/kraken/kraken_types.go @@ -229,13 +229,13 @@ type TradeBalanceInfo struct { // OrderInfo type type OrderInfo struct { - RefID string `json:"refid"` - UserRef int32 `json:"userref"` - Status string `json:"status"` - OpenTime float64 `json:"opentm"` - CloseTime float64 `json:"closetm"` - StartTime float64 `json:"starttm"` - ExpireTime float64 `json:"expiretm"` + RefID string `json:"refid"` + UserRef int32 `json:"userref"` + Status string `json:"status"` + OpenTime types.Time `json:"opentm"` + CloseTime types.Time `json:"closetm"` + StartTime types.Time `json:"starttm"` + ExpireTime types.Time `json:"expiretm"` Description struct { Pair string `json:"pair"` Type string `json:"type"` @@ -303,44 +303,44 @@ type TradesHistory struct { // TradeInfo type type TradeInfo struct { - OrderTxID string `json:"ordertxid"` - Pair string `json:"pair"` - Time float64 `json:"time"` - Type string `json:"type"` - OrderType string `json:"ordertype"` - Price float64 `json:"price,string"` - Cost float64 `json:"cost,string"` - Fee float64 `json:"fee,string"` - Volume float64 `json:"vol,string"` - Margin float64 `json:"margin,string"` - Misc string `json:"misc"` - PosTxID string `json:"postxid"` - ClosedPositionAveragePrice float64 `json:"cprice,string"` - ClosedPositionFee float64 `json:"cfee,string"` - ClosedPositionVolume float64 `json:"cvol,string"` - ClosedPositionMargin float64 `json:"cmargin,string"` - Trades []string `json:"trades"` - PosStatus string `json:"posstatus"` + OrderTxID string `json:"ordertxid"` + Pair string `json:"pair"` + Time types.Time `json:"time"` + Type string `json:"type"` + OrderType string `json:"ordertype"` + Price float64 `json:"price,string"` + Cost float64 `json:"cost,string"` + Fee float64 `json:"fee,string"` + Volume float64 `json:"vol,string"` + Margin float64 `json:"margin,string"` + Misc string `json:"misc"` + PosTxID string `json:"postxid"` + ClosedPositionAveragePrice float64 `json:"cprice,string"` + ClosedPositionFee float64 `json:"cfee,string"` + ClosedPositionVolume float64 `json:"cvol,string"` + ClosedPositionMargin float64 `json:"cmargin,string"` + Trades []string `json:"trades"` + PosStatus string `json:"posstatus"` } // Position holds the opened position type Position struct { - Ordertxid string `json:"ordertxid"` - Pair string `json:"pair"` - Time float64 `json:"time"` - Type string `json:"type"` - OrderType string `json:"ordertype"` - Cost float64 `json:"cost,string"` - Fee float64 `json:"fee,string"` - Volume float64 `json:"vol,string"` - VolumeClosed float64 `json:"vol_closed,string"` - Margin float64 `json:"margin,string"` - RolloverTime int64 `json:"rollovertm,string"` - Misc string `json:"misc"` - OrderFlags string `json:"oflags"` - PositionStatus string `json:"posstatus"` - Net string `json:"net"` - Terms string `json:"terms"` + Ordertxid string `json:"ordertxid"` + Pair string `json:"pair"` + Time types.Time `json:"time"` + Type string `json:"type"` + OrderType string `json:"ordertype"` + Cost float64 `json:"cost,string"` + Fee float64 `json:"fee,string"` + Volume float64 `json:"vol,string"` + VolumeClosed float64 `json:"vol_closed,string"` + Margin float64 `json:"margin,string"` + RolloverTime int64 `json:"rollovertm,string"` + Misc string `json:"misc"` + OrderFlags string `json:"oflags"` + PositionStatus string `json:"posstatus"` + Net string `json:"net"` + Terms string `json:"terms"` } // GetLedgersOptions type @@ -361,14 +361,14 @@ type Ledgers struct { // LedgerInfo type type LedgerInfo struct { - Refid string `json:"refid"` - Time float64 `json:"time"` - Type string `json:"type"` - Aclass string `json:"aclass"` - Asset string `json:"asset"` - Amount float64 `json:"amount,string"` - Fee float64 `json:"fee,string"` - Balance float64 `json:"balance,string"` + Refid string `json:"refid"` + Time types.Time `json:"time"` + Type string `json:"type"` + Aclass string `json:"aclass"` + Asset string `json:"asset"` + Amount float64 `json:"amount,string"` + Fee float64 `json:"fee,string"` + Balance float64 `json:"balance,string"` } // TradeVolumeResponse type @@ -482,16 +482,16 @@ type DepositAddress struct { // WithdrawStatusResponse defines a withdrawal status response type WithdrawStatusResponse struct { - Method string `json:"method"` - Aclass string `json:"aclass"` - Asset string `json:"asset"` - Refid string `json:"refid"` - TxID string `json:"txid"` - Info string `json:"info"` - Amount float64 `json:"amount,string"` - Fee float64 `json:"fee,string"` - Time float64 `json:"time"` - Status string `json:"status"` + Method string `json:"method"` + Aclass string `json:"aclass"` + Asset string `json:"asset"` + Refid string `json:"refid"` + TxID string `json:"txid"` + Info string `json:"info"` + Amount float64 `json:"amount,string"` + Fee float64 `json:"fee,string"` + Time types.Time `json:"time"` + Status string `json:"status"` } // WebsocketSubRequest contains request data for Subscribe/Unsubscribe to channels @@ -552,22 +552,22 @@ type wsSystemStatus struct { // WsOpenOrder contains all open order data from ws feed type WsOpenOrder struct { - UserReferenceID int64 `json:"userref"` - ExpireTime float64 `json:"expiretm,string"` - LastUpdated float64 `json:"lastupdated,string"` - OpenTime float64 `json:"opentm,string"` - StartTime float64 `json:"starttm,string"` - Fee float64 `json:"fee,string"` - LimitPrice float64 `json:"limitprice,string"` - StopPrice float64 `json:"stopprice,string"` - Volume float64 `json:"vol,string"` - ExecutedVolume float64 `json:"vol_exec,string"` - Cost float64 `json:"cost,string"` - AveragePrice float64 `json:"avg_price,string"` - Misc string `json:"misc"` - OFlags string `json:"oflags"` - RefID string `json:"refid"` - Status string `json:"status"` + UserReferenceID int64 `json:"userref"` + ExpireTime types.Time `json:"expiretm"` + LastUpdated types.Time `json:"lastupdated"` + OpenTime types.Time `json:"opentm"` + StartTime types.Time `json:"starttm"` + Fee float64 `json:"fee,string"` + LimitPrice float64 `json:"limitprice,string"` + StopPrice float64 `json:"stopprice,string"` + Volume float64 `json:"vol,string"` + ExecutedVolume float64 `json:"vol_exec,string"` + Cost float64 `json:"cost,string"` + AveragePrice float64 `json:"avg_price,string"` + Misc string `json:"misc"` + OFlags string `json:"oflags"` + RefID string `json:"refid"` + Status string `json:"status"` Description struct { Close string `json:"close"` Price float64 `json:"price,string"` @@ -582,32 +582,32 @@ type WsOpenOrder struct { // WsOwnTrade ws auth owntrade data type WsOwnTrade struct { - Cost float64 `json:"cost,string"` - Fee float64 `json:"fee,string"` - Margin float64 `json:"margin,string"` - OrderTransactionID string `json:"ordertxid"` - OrderType string `json:"ordertype"` - Pair string `json:"pair"` - PostTransactionID string `json:"postxid"` - Price float64 `json:"price,string"` - Time float64 `json:"time,string"` - Type string `json:"type"` - Vol float64 `json:"vol,string"` + Cost float64 `json:"cost,string"` + Fee float64 `json:"fee,string"` + Margin float64 `json:"margin,string"` + OrderTransactionID string `json:"ordertxid"` + OrderType string `json:"ordertype"` + Pair string `json:"pair"` + PostTransactionID string `json:"postxid"` + Price float64 `json:"price,string"` + Time types.Time `json:"time"` + Type string `json:"type"` + Vol float64 `json:"vol,string"` } // WsOpenOrders ws auth open order data type WsOpenOrders struct { Cost float64 `json:"cost,string"` Description WsOpenOrderDescription `json:"descr"` - ExpireTime time.Time `json:"expiretm"` + ExpireTime types.Time `json:"expiretm"` Fee float64 `json:"fee,string"` LimitPrice float64 `json:"limitprice,string"` Misc string `json:"misc"` OFlags string `json:"oflags"` - OpenTime time.Time `json:"opentm"` + OpenTime types.Time `json:"opentm"` Price float64 `json:"price,string"` RefID string `json:"refid"` - StartTime time.Time `json:"starttm"` + StartTime types.Time `json:"starttm"` Status string `json:"status"` StopPrice float64 `json:"stopprice,string"` UserReference float64 `json:"userref"` diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index d1e7a7d3..973cb788 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -322,7 +322,7 @@ func (k *Kraken) wsProcessOwnTrades(ownOrders any) error { TID: key, Type: oType, Side: oSide, - Timestamp: convert.TimeFromUnixTimestampDecimal(val.Time), + Timestamp: val.Time.Time(), } k.Websocket.DataHandler <- &order.Detail{ Exchange: k.Name, @@ -357,8 +357,8 @@ func (k *Kraken) wsProcessOpenOrders(ownOrders any) error { LimitPriceUpper: val.LimitPrice, ExecutedAmount: val.ExecutedVolume, Fee: val.Fee, - Date: convert.TimeFromUnixTimestampDecimal(val.OpenTime).Truncate(time.Microsecond), - LastUpdated: convert.TimeFromUnixTimestampDecimal(val.LastUpdated).Truncate(time.Microsecond), + Date: val.OpenTime.Time(), + LastUpdated: val.LastUpdated.Time(), } if val.Status != "" { diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 2ef9ffed..f96decfe 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -603,7 +603,7 @@ func (k *Kraken) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ a resp[i] = exchange.WithdrawalHistory{ Status: withdrawals[i].Status, TransferID: withdrawals[i].Refid, - Timestamp: time.Unix(int64(withdrawals[i].Time), 0), + Timestamp: withdrawals[i].Time.Time(), Amount: withdrawals[i].Amount, Fee: withdrawals[i].Fee, CryptoToAddress: withdrawals[i].Info, @@ -665,7 +665,7 @@ func (k *Kraken) GetRecentTrades(ctx context.Context, p currency.Pair, assetType Side: side, Price: tradeData.Elements[i].ExecutionEvent.OuterExecutionHolder.Execution.MakerOrder.LimitPrice, Amount: tradeData.Elements[i].ExecutionEvent.OuterExecutionHolder.Execution.MakerOrder.Quantity, - Timestamp: time.UnixMilli(tradeData.Elements[i].ExecutionEvent.OuterExecutionHolder.Execution.MakerOrder.Timestamp), + Timestamp: tradeData.Elements[i].ExecutionEvent.OuterExecutionHolder.Execution.MakerOrder.Timestamp.Time(), }) } default: @@ -941,8 +941,8 @@ func (k *Kraken) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pa Pair: p, Side: side, Type: oType, - Date: convert.TimeFromUnixTimestampDecimal(orderInfo.OpenTime), - CloseTime: convert.TimeFromUnixTimestampDecimal(orderInfo.CloseTime), + Date: orderInfo.OpenTime.Time(), + CloseTime: orderInfo.CloseTime.Time(), Status: status, Price: price, Amount: orderInfo.Volume, @@ -974,17 +974,13 @@ func (k *Kraken) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pa if err != nil { return nil, err } - timeVar, err := time.Parse(krakenFormat, orderInfo.Fills[y].FillTime) - if err != nil { - return nil, err - } orderDetail = order.Detail{ OrderID: orderID, Price: orderInfo.Fills[y].Price, Amount: orderInfo.Fills[y].Size, Side: oSide, Type: fillOrderType, - Date: timeVar, + Date: orderInfo.Fills[y].FillTime, Pair: pair, Exchange: k.Name, AssetType: asset.Futures, @@ -1143,7 +1139,7 @@ func (k *Kraken) GetActiveOrders(ctx context.Context, req *order.MultiOrderReque RemainingAmount: resp.Open[i].Volume - resp.Open[i].VolumeExecuted, ExecutedAmount: resp.Open[i].VolumeExecuted, Exchange: k.Name, - Date: convert.TimeFromUnixTimestampDecimal(resp.Open[i].OpenTime), + Date: resp.Open[i].OpenTime.Time(), Price: resp.Open[i].Description.Price, Side: side, Type: orderType, @@ -1184,17 +1180,13 @@ func (k *Kraken) GetActiveOrders(ctx context.Context, req *order.MultiOrderReque if err != nil { return orders, err } - timeVar, err := time.Parse(krakenFormat, activeOrders.OpenOrders[a].ReceivedTime) - if err != nil { - return orders, err - } orders = append(orders, order.Detail{ OrderID: activeOrders.OpenOrders[a].OrderID, Price: activeOrders.OpenOrders[a].LimitPrice, Amount: activeOrders.OpenOrders[a].FilledSize, Side: oSide, Type: oType, - Date: timeVar, + Date: activeOrders.OpenOrders[a].ReceivedTime, Pair: fPair, Exchange: k.Name, AssetType: asset.Futures, @@ -1276,8 +1268,8 @@ func (k *Kraken) GetOrderHistory(ctx context.Context, getOrdersRequest *order.Mu Cost: resp.Closed[i].Cost, CostAsset: p.Quote, Exchange: k.Name, - Date: convert.TimeFromUnixTimestampDecimal(resp.Closed[i].OpenTime), - CloseTime: convert.TimeFromUnixTimestampDecimal(resp.Closed[i].CloseTime), + Date: resp.Closed[i].OpenTime.Time(), + CloseTime: resp.Closed[i].CloseTime.Time(), Price: resp.Closed[i].Description.Price, Side: side, Status: status, @@ -1615,20 +1607,14 @@ func (k *Kraken) GetFuturesContractDetails(ctx context.Context, item asset.Item) return nil, err } var s, e time.Time - if result.Instruments[i].OpeningDate != "" { - s, err = time.Parse(time.RFC3339, result.Instruments[i].OpeningDate) - if err != nil { - return nil, err - } + if !result.Instruments[i].OpeningDate.IsZero() { + s = result.Instruments[i].OpeningDate } var ct futures.ContractType - if result.Instruments[i].LastTradingTime == "" || item == asset.PerpetualSwap { + if result.Instruments[i].LastTradingTime.IsZero() || item == asset.PerpetualSwap { ct = futures.Perpetual } else { - e, err = time.Parse(time.RFC3339, result.Instruments[i].LastTradingTime) - if err != nil { - return nil, err - } + e = result.Instruments[i].LastTradingTime switch { // three day is used for generosity for contract date ranges case e.Sub(s) <= kline.OneMonth.Duration()+kline.ThreeDay.Duration(): diff --git a/types/time.go b/types/time.go index 29a503fa..ab34e8a3 100644 --- a/types/time.go +++ b/types/time.go @@ -1,6 +1,7 @@ package types import ( + "errors" "fmt" "strconv" "strings" @@ -13,6 +14,8 @@ import ( // format requirements. type Time time.Time +var errInvalidTimestampFormat = errors.New("invalid timestamp format") + // UnmarshalJSON deserializes json, and timestamp information. func (t *Time) UnmarshalJSON(data []byte) error { s := string(data) @@ -21,13 +24,16 @@ func (t *Time) UnmarshalJSON(data []byte) error { s = s[1 : len(s)-1] } - switch s { - case "null", "0", "": + if s == "" || s[0] == 'n' || s == "0" { return nil } if target := strings.Index(s, "."); target != -1 { s = s[:target] + s[target+1:] + + if strings.Trim(s, "0") == "" { + return nil + } } // Expects a string of length 10 (seconds), 13 (milliseconds), 16 (microseconds), or 19 (nanoseconds) representing a Unix timestamp @@ -53,7 +59,7 @@ func (t *Time) UnmarshalJSON(data []byte) error { case 19: *t = Time(time.Unix(0, unixTS)) default: - return fmt.Errorf("cannot unmarshal %s into Time", data) + return fmt.Errorf("%w: %q", errInvalidTimestampFormat, data) } return nil } diff --git a/types/time_test.go b/types/time_test.go index b60e6f6c..c2d4c020 100644 --- a/types/time_test.go +++ b/types/time_test.go @@ -12,62 +12,47 @@ import ( func TestUnmarshalJSON(t *testing.T) { t.Parallel() - var testTime Time - require.NoError(t, json.Unmarshal([]byte(`null`), &testTime)) - assert.Equal(t, time.Time{}, testTime.Time()) - - require.NoError(t, json.Unmarshal([]byte(`0`), &testTime)) - assert.Equal(t, time.Time{}, testTime.Time()) - - require.NoError(t, json.Unmarshal([]byte(`""`), &testTime)) - assert.Equal(t, time.Time{}, testTime.Time()) - - require.NoError(t, json.Unmarshal([]byte(`"0"`), &testTime)) - assert.Equal(t, time.Time{}, testTime.Time()) - - // seconds - require.NoError(t, json.Unmarshal([]byte(`"1628736847"`), &testTime)) - assert.Equal(t, time.Unix(1628736847, 0), testTime.Time()) - - // milliseconds - require.NoError(t, json.Unmarshal([]byte(`"1726104395.5"`), &testTime)) - assert.Equal(t, time.UnixMilli(1726104395500), testTime.Time()) - - require.NoError(t, json.Unmarshal([]byte(`"1726104395.56"`), &testTime)) - assert.Equal(t, time.UnixMilli(1726104395560), testTime.Time()) - - require.NoError(t, json.Unmarshal([]byte(`"1628736847325"`), &testTime)) - assert.Equal(t, time.UnixMilli(1628736847325), testTime.Time()) - - // microseconds - require.NoError(t, json.Unmarshal([]byte(`"1628736847325123"`), &testTime)) - assert.Equal(t, time.UnixMicro(1628736847325123), testTime.Time()) - - require.NoError(t, json.Unmarshal([]byte(`"1726106210903.0"`), &testTime)) - assert.Equal(t, time.UnixMicro(1726106210903000), testTime.Time()) - - require.NoError(t, json.Unmarshal([]byte(`"1747278712.09328"`), &testTime)) - assert.Equal(t, time.UnixMicro(1747278712093280), testTime.Time()) - - // nanoseconds - require.NoError(t, json.Unmarshal([]byte(`"1606292218213.4578"`), &testTime)) - assert.Equal(t, time.Unix(0, 1606292218213457800), testTime.Time()) - - require.NoError(t, json.Unmarshal([]byte(`"1606292218213457800"`), &testTime)) - assert.Equal(t, time.Unix(0, 1606292218213457800), testTime.Time()) - - require.ErrorIs(t, json.Unmarshal([]byte(`"blurp"`), &testTime), strconv.ErrSyntax) - require.Error(t, json.Unmarshal([]byte(`"123456"`), &testTime)) - - // Captures bad syntax when type should be time.Time (RFC3339) - require.ErrorIs(t, json.Unmarshal([]byte(`"2025-03-28T08:00:00Z"`), &testTime), strconv.ErrSyntax) - // parse int failure - require.ErrorIs(t, json.Unmarshal([]byte(`"1606292218213.45.8"`), &testTime), strconv.ErrSyntax) + for _, tc := range []struct { + input string + want time.Time + expError error + }{ + {"null", time.Time{}, nil}, + {"0", time.Time{}, nil}, + {`""`, time.Time{}, nil}, + {`"0"`, time.Time{}, nil}, + {`"0.0"`, time.Time{}, nil}, + {`"0.00000"`, time.Time{}, nil}, + {`"0.0.0.0"`, time.Time{}, strconv.ErrSyntax}, + {`"0.1"`, time.Time{}, errInvalidTimestampFormat}, + {`"1628736847"`, time.Unix(1628736847, 0), nil}, + {`"1726104395.5"`, time.UnixMilli(1726104395500), nil}, + {`"1726104395.56"`, time.UnixMilli(1726104395560), nil}, + {`"1628736847325"`, time.UnixMilli(1628736847325), nil}, + {`"1628736847325123"`, time.UnixMicro(1628736847325123), nil}, + {`"1726106210903.0"`, time.UnixMicro(1726106210903000), nil}, + {`"1747278712.09328"`, time.UnixMicro(1747278712093280), nil}, + {`"1606292218213.4578"`, time.Unix(0, 1606292218213457800), nil}, + {`"1560516023.070651"`, time.Unix(0, 1560516023070651000), nil}, + {`"1606292218213457800"`, time.Unix(0, 1606292218213457800), nil}, + {`"blurp"`, time.Time{}, strconv.ErrSyntax}, + {`"123456"`, time.Time{}, errInvalidTimestampFormat}, + {`"2025-03-28T08:00:00Z"`, time.Time{}, strconv.ErrSyntax}, // RFC3339 format + {`"1606292218213.45.8"`, time.Time{}, strconv.ErrSyntax}, // parse int failure + } { + t.Run(tc.input, func(t *testing.T) { + t.Parallel() + var testTime Time + err := json.Unmarshal([]byte(tc.input), &testTime) + require.ErrorIsf(t, err, tc.expError, "Unmarshal must not error for input %q", tc.input) + assert.Equal(t, tc.want, testTime.Time()) + }) + } } -// 6152384 195.5 ns/op 168 B/op 2 allocs/op (current) -// 5030734 240.1 ns/op 168 B/op 2 allocs/op (previous) +// 3948978 303.5 ns/op 168 B/op 2 allocs/op (current) after more stringent checks +// 6152384 195.5 ns/op 168 B/op 2 allocs/op (previous) func BenchmarkUnmarshalJSON(b *testing.B) { var testTime Time for b.Loop() {