Skip to content

Commit

Permalink
Merge master and minor fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
cranktakular committed Oct 8, 2024
2 parents c88b057 + 83cefe7 commit 0eeb73c
Show file tree
Hide file tree
Showing 63 changed files with 4,548 additions and 5,520 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/proto-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- uses: bufbuild/buf-setup-action@v1.41.0
- uses: bufbuild/buf-setup-action@v1.44.0

- name: buf generate
working-directory: ./gctrpc
Expand Down
59 changes: 2 additions & 57 deletions common/convert/convert.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package convert

import (
"encoding/json"
"fmt"
"math"
"strconv"
Expand Down Expand Up @@ -59,11 +58,10 @@ func TimeFromUnixTimestampFloat(raw interface{}) (time.Time, error) {
return time.UnixMilli(int64(ts)), nil
}

// TimeFromUnixTimestampDecimal converts a unix timestamp in decimal form to
// a time.Time
// TimeFromUnixTimestampDecimal converts a unix timestamp in decimal form to a time.Time in UTC
func TimeFromUnixTimestampDecimal(input float64) time.Time {
i, f := math.Modf(input)
return time.Unix(int64(i), int64(f*(1e9)))
return time.Unix(int64(i), int64(f*(1e9))).UTC()
}

// UnixTimestampToTime returns time.time
Expand Down Expand Up @@ -187,56 +185,3 @@ func InterfaceToStringOrZeroValue(r interface{}) string {
}
return ""
}

// ExchangeTime provides timestamp to time conversion method.
type ExchangeTime time.Time

// UnmarshalJSON is custom type json unmarshaller for ExchangeTime
func (k *ExchangeTime) UnmarshalJSON(data []byte) error {
var timestamp interface{}
err := json.Unmarshal(data, &timestamp)
if err != nil {
return err
}
var standard int64
switch value := timestamp.(type) {
case string:
if value == "" {
// Setting the time to zero value because some timestamp fields could return an empty string while there is no error
// So, in such cases, Time returns zero timestamp.
break
}
standard, err = strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
case int64:
standard = value
case float64:
// Warning: converting float64 to int64 instance may create loss of precision in the timestamp information.
// be aware or consider customizing this section if found necessary.
standard = int64(value)
case nil:
// for some exchange timestamp fields, if the timestamp information is not specified,
// the data is 'nil' instead of zero value string or integer value.
default:
return fmt.Errorf("unsupported timestamp type %T", timestamp)
}

switch {
case standard == 0:
*k = ExchangeTime(time.Time{})
case standard >= 1e13:
*k = ExchangeTime(time.Unix(standard/1e9, standard%1e9))
case standard > 9999999999:
*k = ExchangeTime(time.UnixMilli(standard))
default:
*k = ExchangeTime(time.Unix(standard, 0))
}
return nil
}

// Time returns a time.Time instance from ExchangeTime instance object.
func (k ExchangeTime) Time() time.Time {
return time.Time(k)
}
108 changes: 12 additions & 96 deletions common/convert/convert_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package convert

import (
"encoding/json"
"strings"
"testing"
"time"

"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
)

func TestFloatFromString(t *testing.T) {
Expand Down Expand Up @@ -98,18 +98,17 @@ func TestTimeFromUnixTimestampFloat(t *testing.T) {
}

func TestTimeFromUnixTimestampDecimal(t *testing.T) {
r := TimeFromUnixTimestampDecimal(1590633982.5714)
if r.Year() != 2020 ||
r.Month().String() != "May" ||
r.Day() != 28 {
t.Error("unexpected result")
}

r = TimeFromUnixTimestampDecimal(1560516023.070651)
if r.Year() != 2019 ||
r.Month().String() != "June" ||
r.Day() != 14 {
t.Error("unexpected result")
for in, exp := range map[float64]time.Time{
1590633982.5714: time.Date(2020, 5, 28, 2, 46, 22, 571400000, time.UTC),
1560516023.070651: time.Date(2019, 6, 14, 12, 40, 23, 70651000, time.UTC),
// Examples from Kraken
1373750306.9819: time.Date(2013, 7, 13, 21, 18, 26, 981900000, time.UTC),
1534614098.345543: time.Date(2018, 8, 18, 17, 41, 38, 345543000, time.UTC),
} {
got := TimeFromUnixTimestampDecimal(in)
z, _ := got.Zone()
assert.Equal(t, "UTC", z, "TimeFromUnixTimestampDecimal should return a UTC time")
assert.WithinRangef(t, got, exp.Add(-time.Microsecond), exp.Add(time.Microsecond), "TimeFromUnixTimestampDecimal(%f) should parse a unix timestamp correctly", in)
}
}

Expand Down Expand Up @@ -316,86 +315,3 @@ func TestInterfaceToStringOrZeroValue(t *testing.T) {
t.Errorf("expected meow, got: %v", x)
}
}

func TestExchangeTimeUnmarshalJSON(t *testing.T) {
t.Parallel()
unmarshaledResult := &struct {
Timestamp ExchangeTime `json:"ts"`
}{}
data1 := `{"ts":""}`
result := time.Time{}
err := json.Unmarshal([]byte(data1), &unmarshaledResult)
if err != nil {
t.Fatal(err)
} else if !unmarshaledResult.Timestamp.Time().Equal(result) {
t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result)
}
data2 := `{"ts":"1685564775371"}`
result = time.UnixMilli(1685564775371)
err = json.Unmarshal([]byte(data2), &unmarshaledResult)
if err != nil {
t.Fatal(err)
} else if !unmarshaledResult.Timestamp.Time().Equal(result) {
t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result)
}
data3 := `{"ts":1685564775371}`
err = json.Unmarshal([]byte(data3), &unmarshaledResult)
if err != nil {
t.Fatal(err)
} else if !unmarshaledResult.Timestamp.Time().Equal(result) {
t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result)
}
data4 := `{"ts":"1685564775"}`
result = time.Unix(1685564775, 0)
err = json.Unmarshal([]byte(data4), &unmarshaledResult)
if err != nil {
t.Fatal(err)
} else if !unmarshaledResult.Timestamp.Time().Equal(result) {
t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result)
}
data5 := `{"ts":1685564775}`
err = json.Unmarshal([]byte(data5), &unmarshaledResult)
if err != nil {
t.Fatal(err)
} else if !unmarshaledResult.Timestamp.Time().Equal(result) {
t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result)
}
data6 := `{"ts":"1685564775371320000"}`
result = time.Unix(int64(1685564775371320000)/1e9, int64(1685564775371320000)%1e9)
err = json.Unmarshal([]byte(data6), &unmarshaledResult)
if err != nil {
t.Fatal(err)
} else if !unmarshaledResult.Timestamp.Time().Equal(result) {
t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result)
}
data7 := `{"ts":"abcdefg"}`
err = json.Unmarshal([]byte(data7), &unmarshaledResult)
if err == nil {
t.Fatal("expecting error but found nil")
}
data8 := `{"ts":0}`
result = time.Time{}
err = json.Unmarshal([]byte(data8), &unmarshaledResult)
if err != nil {
t.Fatal(err)
} else if !unmarshaledResult.Timestamp.Time().Equal(result) {
t.Errorf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result)
}
}

// 2239239 516.1 ns/op 424 B/op 9 allocs/op
func BenchmarkExchangeTimeUnmarshaling(b *testing.B) {
unmarshaledResult := &struct {
Timestamp ExchangeTime `json:"ts"`
}{}
data5 := `{"ts":1685564775}`
result := time.Unix(1685564775, 0)
var err error
for i := 0; i < b.N; i++ {
if err = json.Unmarshal([]byte(data5), &unmarshaledResult); err != nil {
b.Fatal(err)
} else if !unmarshaledResult.Timestamp.Time().Equal(result) {
b.Fatalf("found %v, but expected %v", unmarshaledResult.Timestamp.Time(), result)
}
}
}
3 changes: 1 addition & 2 deletions currency/pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ func NewBTCUSD() Pair {
return NewPair(BTC, USD)
}

// NewPairDelimiter splits the desired currency string at delimiter, then returns
// a Pair struct
// NewPairDelimiter splits the desired currency string at delimiter, then returns a Pair struct
func NewPairDelimiter(currencyPair, delimiter string) (Pair, error) {
if !strings.Contains(currencyPair, delimiter) {
return EMPTYPAIR,
Expand Down
45 changes: 24 additions & 21 deletions exchanges/binance/binance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"fmt"
"os"
"reflect"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -44,8 +43,6 @@ const (

var (
b = &Binance{}
// this lock guards against orderbook tests race
binanceOrderBookLock = &sync.Mutex{}
// this pair is used to ensure that endpoints match it correctly
testPairMapping = currency.NewPair(currency.DOGE, currency.USDT)
)
Expand Down Expand Up @@ -1985,10 +1982,11 @@ func TestSubscribe(t *testing.T) {
require.NoError(t, err, "generateSubscriptions must not error")
if mockTests {
exp := []string{"btcusdt@depth@100ms", "btcusdt@kline_1m", "btcusdt@ticker", "btcusdt@trade", "dogeusdt@depth@100ms", "dogeusdt@kline_1m", "dogeusdt@ticker", "dogeusdt@trade"}
mock := func(msg []byte, w *websocket.Conn) error {
mock := func(tb testing.TB, msg []byte, w *websocket.Conn) error {
tb.Helper()
var req WsPayload
require.NoError(t, json.Unmarshal(msg, &req), "Unmarshal should not error")
require.ElementsMatch(t, req.Params, exp, "Params should have correct channels")
require.NoError(tb, json.Unmarshal(msg, &req), "Unmarshal should not error")
require.ElementsMatch(tb, req.Params, exp, "Params should have correct channels")
return w.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf(`{"result":null,"id":%d}`, req.ID)))
}
b = testexch.MockWsInstance[Binance](t, testexch.CurryWsMockUpgrader(t, mock))
Expand All @@ -2006,10 +2004,11 @@ func TestSubscribeBadResp(t *testing.T) {
channels := subscription.List{
{Channel: "moons@ticker"},
}
mock := func(msg []byte, w *websocket.Conn) error {
mock := func(tb testing.TB, msg []byte, w *websocket.Conn) error {
tb.Helper()
var req WsPayload
err := json.Unmarshal(msg, &req)
require.NoError(t, err, "Unmarshal should not error")
require.NoError(tb, err, "Unmarshal should not error")
return w.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf(`{"result":{"error":"carrots"},"id":%d}`, req.ID)))
}
b := testexch.MockWsInstance[Binance](t, testexch.CurryWsMockUpgrader(t, mock)) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
Expand All @@ -2031,11 +2030,11 @@ func TestWsKlineUpdate(t *testing.T) {
t.Parallel()
pressXToJSON := []byte(`{"stream":"btcusdt@kline_1m","data":{
"e": "kline",
"E": 123456789,
"E": 1234567891,
"s": "BTCUSDT",
"k": {
"t": 123400000,
"T": 123460000,
"t": 1234000001,
"T": 1234600001,
"s": "BTCUSDT",
"i": "1m",
"f": 100,
Expand Down Expand Up @@ -2064,14 +2063,14 @@ func TestWsTradeUpdate(t *testing.T) {
b.SetSaveTradeDataStatus(true)
pressXToJSON := []byte(`{"stream":"btcusdt@trade","data":{
"e": "trade",
"E": 123456789,
"E": 1234567891,
"s": "BTCUSDT",
"t": 12345,
"p": "0.001",
"q": "100",
"b": 88,
"a": 50,
"T": 123456785,
"T": 1234567851,
"m": true,
"M": true
}}`)
Expand All @@ -2082,8 +2081,9 @@ func TestWsTradeUpdate(t *testing.T) {
}

func TestWsDepthUpdate(t *testing.T) {
binanceOrderBookLock.Lock()
defer binanceOrderBookLock.Unlock()
t.Parallel()
b := new(Binance) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
require.NoError(t, testexch.Setup(b), "Test instance Setup must not error")
b.setupOrderbookManager()
seedLastUpdateID := int64(161)
book := OrderBook{
Expand Down Expand Up @@ -2116,7 +2116,7 @@ func TestWsDepthUpdate(t *testing.T) {

update1 := []byte(`{"stream":"btcusdt@depth","data":{
"e": "depthUpdate",
"E": 123456788,
"E": 1234567881,
"s": "BTCUSDT",
"U": 157,
"u": 160,
Expand All @@ -2134,7 +2134,7 @@ func TestWsDepthUpdate(t *testing.T) {
}

if err := b.wsHandleData(update1); err != nil {
t.Error(err)
t.Fatal(err)
}

b.obm.state[currency.BTC][currency.USDT][asset.Spot].fetchingBook = false
Expand All @@ -2156,7 +2156,7 @@ func TestWsDepthUpdate(t *testing.T) {

update2 := []byte(`{"stream":"btcusdt@depth","data":{
"e": "depthUpdate",
"E": 123456789,
"E": 1234567892,
"s": "BTCUSDT",
"U": 161,
"u": 165,
Expand Down Expand Up @@ -2470,8 +2470,9 @@ var websocketDepthUpdate = []byte(`{"E":1608001030784,"U":7145637266,"a":[["1945

func TestProcessUpdate(t *testing.T) {
t.Parallel()
binanceOrderBookLock.Lock()
defer binanceOrderBookLock.Unlock()
b := new(Binance) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
require.NoError(t, testexch.Setup(b), "Test instance Setup must not error")
b.setupOrderbookManager()
p := currency.NewPair(currency.BTC, currency.USDT)
var depth WebsocketDepthStream
err := json.Unmarshal(websocketDepthUpdate, &depth)
Expand Down Expand Up @@ -2562,7 +2563,9 @@ func TestSetExchangeOrderExecutionLimits(t *testing.T) {
}

func TestWsOrderExecutionReport(t *testing.T) {
// cannot run in parallel due to inspecting the DataHandler result
t.Parallel()
b := new(Binance) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
require.NoError(t, testexch.Setup(b), "Test instance Setup must not error")
payload := []byte(`{"stream":"jTfvpakT2yT0hVIo5gYWVihZhdM2PrBgJUZ5PyfZ4EVpCkx4Uoxk5timcrQc","data":{"e":"executionReport","E":1616627567900,"s":"BTCUSDT","c":"c4wyKsIhoAaittTYlIVLqk","S":"BUY","o":"LIMIT","f":"GTC","q":"0.00028400","p":"52789.10000000","P":"0.00000000","F":"0.00000000","g":-1,"C":"","x":"NEW","X":"NEW","r":"NONE","i":5340845958,"l":"0.00000000","z":"0.00000000","L":"0.00000000","n":"0","N":"BTC","T":1616627567900,"t":-1,"I":11388173160,"w":true,"m":false,"M":false,"O":1616627567900,"Z":"0.00000000","Y":"0.00000000","Q":"0.00000000","W":1616627567900}}`)
// this is a buy BTC order, normally commission is charged in BTC, vice versa.
expectedResult := order.Detail{
Expand Down
Loading

0 comments on commit 0eeb73c

Please sign in to comment.