Files
gocryptotrader/exchanges/kline/kline_test.go
Ryan O'Hara-Reid 42475bf2b8 exchanges: add setTimeWindow boolean to GetKlineRequest param (#1160)
* exchanges: add setTimeWindow boolean to GetKlineRequest params to differentiate between a set time period return from endpoint.

* glorious: nits

* exchange: conjugation

* Update exchanges/exchange.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits and an assortment of differences

* exchanges: remove some comments

* glorious: nits

* cleanup

* tests: fix

* Update exchanges/hitbtc/hitbtc_wrapper.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/kline/kline.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/kline/kline_test.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* kline: fix test

* rm unused variables

* almost: nits

* glorious: nits

* linter: fix

* rm unused variable

* Refactored comment in the okex tests to ensure that it accurately reflects the variable name and the issue related to the time window, as requested by GloriousCode. The previous comment did not align with the identifier assigned to the property, which could cause confusion and misunderstanding among other programmers or stakeholders. The updated comment will improve the clarity and readability of the codebase and make it easier to understand the intended purpose of the associated variables. The change was made with the aim of improving the overall quality and maintainability of the code.

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
2023-04-27 10:10:19 +10:00

1380 lines
30 KiB
Go

package kline
import (
"errors"
"fmt"
"math/rand"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/database/drivers"
"github.com/thrasher-corp/gocryptotrader/database/repository/candle"
"github.com/thrasher-corp/gocryptotrader/database/repository/exchange"
"github.com/thrasher-corp/gocryptotrader/database/testhelpers"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
var (
verbose = false
testExchanges = []exchange.Details{{Name: "one"}}
)
func TestValidateData(t *testing.T) {
t.Parallel()
err := validateData(nil)
if err == nil {
t.Error("error cannot be nil")
}
var empty []order.TradeHistory
err = validateData(empty)
if err == nil {
t.Error("error cannot be nil")
}
tn := time.Now()
trade1 := []order.TradeHistory{
{Timestamp: tn.Add(2 * time.Minute), TID: "2"},
{Timestamp: tn.Add(time.Minute), TID: "1"},
{Timestamp: tn.Add(3 * time.Minute), TID: "3"},
}
err = validateData(trade1)
if err == nil {
t.Error("error cannot be nil")
}
trade2 := []order.TradeHistory{
{Timestamp: tn.Add(2 * time.Minute), TID: "2", Amount: 1, Price: 0},
}
err = validateData(trade2)
if err == nil {
t.Error("error cannot be nil")
}
trade3 := []order.TradeHistory{
{TID: "2", Amount: 1, Price: 0},
}
err = validateData(trade3)
if err == nil {
t.Error("error cannot be nil")
}
trade4 := []order.TradeHistory{
{Timestamp: tn.Add(2 * time.Minute), TID: "2", Amount: 1, Price: 1000},
{Timestamp: tn.Add(time.Minute), TID: "1", Amount: 1, Price: 1001},
{Timestamp: tn.Add(3 * time.Minute), TID: "3", Amount: 1, Price: 1001.5},
}
err = validateData(trade4)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if trade4[0].TID != "1" || trade4[1].TID != "2" || trade4[2].TID != "3" {
t.Error("trade history sorted incorrectly")
}
}
func TestCreateKline(t *testing.T) {
t.Parallel()
pair := currency.NewPair(currency.BTC, currency.USD)
_, err := CreateKline(nil, OneMin, pair, asset.Spot, "Binance")
if !errors.Is(err, errInsufficientTradeData) {
t.Fatalf("received: '%v' but expected '%v'", err, errInsufficientTradeData)
}
tradeTotal := 24000
var trades []order.TradeHistory
execution := time.Now()
for i := 0; i < tradeTotal; i++ {
price, rndTime := 1000+float64(rand.Intn(1000)), rand.Intn(10) //nolint:gosec // no need to import crypo/rand for testing
execution = execution.Add(time.Duration(rndTime) * time.Second)
trades = append(trades, order.TradeHistory{
Timestamp: execution,
Amount: 1, // Keep as one for counting
Price: price,
})
}
_, err = CreateKline(trades, 0, pair, asset.Spot, "Binance")
if !errors.Is(err, ErrInvalidInterval) {
t.Fatalf("received: '%v' but expected '%v'", err, ErrInvalidInterval)
}
c, err := CreateKline(trades, OneMin, pair, asset.Spot, "Binance")
if err != nil {
t.Fatal(err)
}
var amounts float64
for x := range c.Candles {
amounts += c.Candles[x].Volume
}
if amounts != float64(tradeTotal) {
t.Fatalf("received: '%v' but expected '%v'", amounts, float64(tradeTotal))
}
}
func TestKlineWord(t *testing.T) {
t.Parallel()
if OneDay.Word() != "oneday" {
t.Fatalf("unexpected result: %v", OneDay.Word())
}
}
func TestKlineDuration(t *testing.T) {
t.Parallel()
if OneDay.Duration() != time.Hour*24 {
t.Fatalf("unexpected result: %v", OneDay.Duration())
}
}
func TestKlineShort(t *testing.T) {
t.Parallel()
if OneDay.Short() != "24h" {
t.Fatalf("unexpected result: %v", OneDay.Short())
}
}
func TestDurationToWord(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
interval Interval
}{
{
"FifteenSecond",
FifteenSecond,
},
{
"OneMin",
OneMin,
},
{
"ThreeMin",
ThreeMin,
},
{
"FiveMin",
FiveMin,
},
{
"TenMin",
TenMin,
},
{
"FifteenMin",
FifteenMin,
},
{
"ThirtyMin",
ThirtyMin,
},
{
"OneHour",
OneHour,
},
{
"TwoHour",
TwoHour,
},
{
"FourHour",
FourHour,
},
{
"SixHour",
SixHour,
},
{
"EightHour",
OneHour * 8,
},
{
"TwelveHour",
TwelveHour,
},
{
"OneDay",
OneDay,
},
{
"ThreeDay",
ThreeDay,
},
{
"FiveDay",
FiveDay,
},
{
"FifteenDay",
FifteenDay,
},
{
"OneWeek",
OneWeek,
},
{
"TwoWeek",
TwoWeek,
},
{
"OneMonth",
OneMonth,
},
{
"OneYear",
OneYear,
},
{
"notfound",
Interval(time.Hour * 1337),
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
t.Parallel()
t.Helper()
v := durationToWord(test.interval)
if !strings.EqualFold(v, test.name) {
t.Fatalf("%v: received %v expected %v", test.name, v, test.name)
}
})
}
}
func TestTotalCandlesPerInterval(t *testing.T) {
t.Parallel()
start := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
testCases := []struct {
name string
interval Interval
expected int64
}{
{
"FifteenSecond",
FifteenSecond,
2102400,
},
{
"OneMin",
OneMin,
525600,
},
{
"ThreeMin",
ThreeMin,
175200,
},
{
"FiveMin",
FiveMin,
105120,
},
{
"TenMin",
TenMin,
52560,
},
{
"FifteenMin",
FifteenMin,
35040,
},
{
"ThirtyMin",
ThirtyMin,
17520,
},
{
"OneHour",
OneHour,
8760,
},
{
"TwoHour",
TwoHour,
4380,
},
{
"FourHour",
FourHour,
2190,
},
{
"SixHour",
SixHour,
1460,
},
{
"EightHour",
OneHour * 8,
1095,
},
{
"TwelveHour",
TwelveHour,
730,
},
{
"OneDay",
OneDay,
365,
},
{
"ThreeDay",
ThreeDay,
121,
},
{
"FiveDay",
FiveDay,
73,
},
{
"FifteenDay",
FifteenDay,
24,
},
{
"OneWeek",
OneWeek,
52,
},
{
"TwoWeek",
TwoWeek,
26,
},
{
"OneMonth",
OneMonth,
12,
},
{
"OneYear",
OneYear,
1,
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
t.Parallel()
v := TotalCandlesPerInterval(start, end, test.interval)
if v != test.expected {
t.Fatalf("%v: received %v expected %v", test.name, v, test.expected)
}
})
}
}
func TestCalculateCandleDateRanges(t *testing.T) {
t.Parallel()
pt := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
ft := time.Date(2222, 1, 1, 0, 0, 0, 0, time.UTC)
et := time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)
nt := time.Time{}
_, err := CalculateCandleDateRanges(nt, nt, OneMin, 300)
if !errors.Is(err, common.ErrDateUnset) {
t.Errorf("received %v expected %v", err, common.ErrDateUnset)
}
_, err = CalculateCandleDateRanges(et, pt, OneMin, 300)
if !errors.Is(err, common.ErrStartAfterEnd) {
t.Errorf("received %v expected %v", err, common.ErrStartAfterEnd)
}
_, err = CalculateCandleDateRanges(et, ft, 0, 300)
if !errors.Is(err, ErrInvalidInterval) {
t.Errorf("received %v expected %v", err, ErrInvalidInterval)
}
_, err = CalculateCandleDateRanges(et, et, OneMin, 300)
if !errors.Is(err, common.ErrStartEqualsEnd) {
t.Errorf("received %v expected %v", err, common.ErrStartEqualsEnd)
}
v, err := CalculateCandleDateRanges(pt, et, OneWeek, 300)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !v.Ranges[0].Start.Time.Equal(time.Unix(1546214400, 0)) {
t.Errorf("expected %v received %v", 1546214400, v.Ranges[0].Start.Ticks)
}
v, err = CalculateCandleDateRanges(pt, et, OneWeek, 100)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if len(v.Ranges) != 1 {
t.Fatalf("expected %v received %v", 1, len(v.Ranges))
}
if len(v.Ranges[0].Intervals) != 52 {
t.Errorf("expected %v received %v", 52, len(v.Ranges[0].Intervals))
}
v, err = CalculateCandleDateRanges(et, ft, OneWeek, 5)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if len(v.Ranges) != 2108 {
t.Errorf("expected %v received %v", 2108, len(v.Ranges))
}
if len(v.Ranges[0].Intervals) != 5 {
t.Errorf("expected %v received %v", 5, len(v.Ranges[0].Intervals))
}
if len(v.Ranges[1].Intervals) != 5 {
t.Errorf("expected %v received %v", 5, len(v.Ranges[1].Intervals))
}
lenRanges := len(v.Ranges) - 1
lenIntervals := len(v.Ranges[lenRanges].Intervals) - 1
if !v.Ranges[lenRanges].Intervals[lenIntervals].End.Equal(ft.Round(OneWeek.Duration())) {
t.Errorf("expected %v received %v", ft.Round(OneDay.Duration()), v.Ranges[lenRanges].Intervals[lenIntervals].End)
}
}
func TestItem_SortCandlesByTimestamp(t *testing.T) {
t.Parallel()
var tempKline = Item{
Exchange: "testExchange",
Pair: currency.NewPair(currency.BTC, currency.USDT),
Asset: asset.Spot,
Interval: OneDay,
}
for x := 0; x < 100; x++ {
y := rand.Float64() //nolint:gosec // used for generating test data, no need to import crypo/rand
tempKline.Candles = append(tempKline.Candles,
Candle{
Time: time.Now().AddDate(0, 0, -x),
Open: y,
High: y + float64(x),
Low: y - float64(x),
Close: y,
Volume: y,
})
}
tempKline.SortCandlesByTimestamp(false)
if tempKline.Candles[0].Time.After(tempKline.Candles[1].Time) {
t.Fatal("expected kline.Candles to be in descending order")
}
tempKline.SortCandlesByTimestamp(true)
if tempKline.Candles[0].Time.Before(tempKline.Candles[1].Time) {
t.Fatal("expected kline.Candles to be in ascending order")
}
}
func setupTest(t *testing.T) {
t.Helper()
if verbose {
err := testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
var err error
testhelpers.MigrationDir = filepath.Join("..", "..", "database", "migrations")
testhelpers.PostgresTestDatabase = testhelpers.GetConnectionDetails()
testhelpers.TempDir, err = os.MkdirTemp("", "gct-temp")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
}
func TestStoreInDatabase(t *testing.T) {
setupTest(t)
testCases := []struct {
name string
config *database.Config
seedDB func(bool) error
runner func(t *testing.T)
closer func(dbConn *database.Instance) error
}{
{
name: "postgresql",
config: testhelpers.PostgresTestDatabase,
seedDB: seedDB,
},
{
name: "SQLite",
config: &database.Config{
Driver: database.DBSQLite3,
ConnectionDetails: drivers.ConnectionDetails{Database: "./testdb"},
},
seedDB: seedDB,
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
if !testhelpers.CheckValidConfig(&test.config.ConnectionDetails) {
t.Skip("database not configured skipping test")
}
dbConn, err := testhelpers.ConnectToDatabase(test.config)
if err != nil {
t.Fatal(err)
}
if test.seedDB != nil {
err = test.seedDB(false)
if err != nil {
t.Error(err)
}
}
_, ohlcvData, err := genOHCLVData()
if err != nil {
t.Fatal(err)
}
r, err := StoreInDatabase(&ohlcvData, false)
if err != nil {
t.Fatal(err)
}
if r != 365 {
t.Fatalf("unexpected number inserted: %v", r)
}
r, err = StoreInDatabase(&ohlcvData, true)
if err != nil {
t.Fatal(err)
}
if r != 365 {
t.Fatalf("unexpected number inserted: %v", r)
}
err = testhelpers.CloseDatabase(dbConn)
if err != nil {
t.Error(err)
}
})
}
err := os.RemoveAll(testhelpers.TempDir)
if err != nil {
t.Fatalf("Failed to remove temp db file: %v", err)
}
}
func TestLoadFromDatabase(t *testing.T) {
setupTest(t)
testCases := []struct {
name string
config *database.Config
seedDB func(bool) error
runner func(t *testing.T)
closer func(dbConn *database.Instance) error
}{
{
name: "postgresql",
config: testhelpers.PostgresTestDatabase,
seedDB: seedDB,
},
{
name: "SQLite",
config: &database.Config{
Driver: database.DBSQLite3,
ConnectionDetails: drivers.ConnectionDetails{Database: "./testdb"},
},
seedDB: seedDB,
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
if !testhelpers.CheckValidConfig(&test.config.ConnectionDetails) {
t.Skip("database not configured skipping test")
}
dbConn, err := testhelpers.ConnectToDatabase(test.config)
if err != nil {
t.Fatal(err)
}
if test.seedDB != nil {
err = test.seedDB(true)
if err != nil {
t.Error(err)
}
}
p, err := currency.NewPairFromString("BTCUSDT")
if err != nil {
t.Fatal(err)
}
start := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
end := start.AddDate(1, 0, 0)
ret, err := LoadFromDatabase(testExchanges[0].Name, p, asset.Spot, OneDay, start, end)
if err != nil {
t.Fatal(err)
}
if ret.Exchange != testExchanges[0].Name {
t.Fatalf("incorrect data returned: %v", ret.Exchange)
}
err = testhelpers.CloseDatabase(dbConn)
if err != nil {
t.Error(err)
}
})
}
err := os.RemoveAll(testhelpers.TempDir)
if err != nil {
t.Fatalf("Failed to remove temp db file: %v", err)
}
}
// TODO: find a better way to handle this to remove duplication between candle test
func seedDB(includeOHLCVData bool) error {
err := exchange.InsertMany(testExchanges)
if err != nil {
return err
}
if includeOHLCVData {
data, _, err := genOHCLVData()
if err != nil {
return err
}
_, err = candle.Insert(&data)
return err
}
return nil
}
func genOHCLVData() (out candle.Item, outItem Item, err error) {
exchangeUUID, err := exchange.UUIDByName(testExchanges[0].Name)
if err != nil {
return
}
out.ExchangeID = exchangeUUID.String()
out.Base = currency.BTC.String()
out.Quote = currency.USDT.String()
out.Interval = 86400
out.Asset = "spot"
start := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
for x := 0; x < 365; x++ {
out.Candles = append(out.Candles, candle.Candle{
Timestamp: start.Add(time.Hour * 24 * time.Duration(x)),
Open: 1000,
High: 1000,
Low: 1000,
Close: 1000,
Volume: 1000,
})
}
outItem.Interval = OneDay
outItem.Asset = asset.Spot
outItem.Pair = currency.NewPair(currency.BTC, currency.USDT)
outItem.Exchange = testExchanges[0].Name
for x := 0; x < 365; x++ {
outItem.Candles = append(outItem.Candles, Candle{
Time: start.Add(time.Hour * 24 * time.Duration(x)),
Open: 1000,
High: 1000,
Low: 1000,
Close: 1000,
Volume: 1000,
})
}
return out, outItem, nil
}
func TestLoadCSV(t *testing.T) {
v, err := LoadFromGCTScriptCSV(filepath.Join("..", "..", "testdata", "binance_BTCUSDT_24h_2019_01_01_2020_01_01.csv"))
if err != nil {
t.Fatal(err)
}
if v[0].Time.UTC() != time.Unix(1546300800, 0).UTC() {
t.Fatalf("unexpected value received: %v", v[0].Time)
}
if v[269].Close != 8177.91 {
t.Fatalf("unexpected value received: %v", v[269].Close)
}
if v[364].Open != 7246 {
t.Fatalf("unexpected value received: %v", v[364].Open)
}
}
func TestVerifyResultsHaveData(t *testing.T) {
t.Parallel()
tt1 := time.Now().Round(OneDay.Duration())
tt2 := tt1.Add(OneDay.Duration())
tt3 := tt2.Add(OneDay.Duration()) // end date no longer inclusive
dateRanges, err := CalculateCandleDateRanges(tt1, tt3, OneDay, 0)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if dateRanges.HasDataAtDate(tt1) {
t.Error("unexpected true value")
}
err = dateRanges.SetHasDataFromCandles([]Candle{
{
Time: tt1,
Low: 1337,
},
{
Time: tt2,
},
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !dateRanges.HasDataAtDate(tt1) {
t.Error("expected true")
}
err = dateRanges.SetHasDataFromCandles([]Candle{
{
Time: tt1,
},
{
Time: tt2,
Low: 1337,
},
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if dateRanges.HasDataAtDate(tt1) {
t.Error("expected false")
}
}
func TestDataSummary(t *testing.T) {
t.Parallel()
tt1 := time.Now().Add(-time.Hour * 24).Round(OneDay.Duration())
tt2 := time.Now().Round(OneDay.Duration())
tt3 := time.Now().Add(time.Hour * 24).Round(OneDay.Duration())
dateRanges, err := CalculateCandleDateRanges(tt1, tt2, OneDay, 0)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
result := dateRanges.DataSummary(false)
if len(result) != 1 {
t.Errorf("expected %v received %v", 1, len(result))
}
dateRanges, err = CalculateCandleDateRanges(tt1, tt3, OneDay, 0)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
dateRanges.Ranges[0].Intervals[0].HasData = true
result = dateRanges.DataSummary(true)
if len(result) != 2 {
t.Errorf("expected %v received %v", 2, len(result))
}
result = dateRanges.DataSummary(false)
if len(result) != 1 {
t.Errorf("expected %v received %v", 1, len(result))
}
}
func TestHasDataAtDate(t *testing.T) {
t.Parallel()
tt1 := time.Now().Round(OneDay.Duration())
tt2 := tt1.Add(OneDay.Duration())
tt3 := tt2.Add(OneDay.Duration()) // end date no longer inclusive
dateRanges, err := CalculateCandleDateRanges(tt1, tt3, OneDay, 0)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if dateRanges.HasDataAtDate(tt2) {
t.Error("unexpected true value")
}
err = dateRanges.SetHasDataFromCandles([]Candle{
{
Time: tt1,
Close: 1337,
},
{
Time: tt2,
Close: 1337,
},
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !dateRanges.HasDataAtDate(tt2) {
t.Error("unexpected false value")
}
if dateRanges.HasDataAtDate(tt2.Add(time.Hour * 24)) {
t.Error("should not have data")
}
}
func TestIntervalsPerYear(t *testing.T) {
t.Parallel()
var i Interval
if i.IntervalsPerYear() != 0 {
t.Error("expected 0")
}
i = OneYear
if i.IntervalsPerYear() != 1.0 {
t.Error("expected 1")
}
i = OneDay
if i.IntervalsPerYear() != 365 {
t.Error("expected 365")
}
i = OneHour
if i.IntervalsPerYear() != 8760 {
t.Error("expected 8670")
}
i = TwoHour
if i.IntervalsPerYear() != 4380 {
t.Error("expected 4380")
}
i = TwoHour + FifteenSecond
if i.IntervalsPerYear() != 4370.893970893971 {
t.Error("expected 4370...")
}
}
// The purpose of this benchmark is to highlight that requesting
// '.Unix()` frequently is a slow process
func BenchmarkJustifyIntervalTimeStoringUnixValues1(b *testing.B) {
tt1 := time.Now()
tt2 := time.Now().Add(-time.Hour)
tt3 := time.Now().Add(time.Hour)
for i := 0; i < b.N; i++ {
if tt1.Unix() == tt2.Unix() || //nolint:staticcheck // it is a benchmark to demonstrate inefficiency in calling
(tt1.Unix() > tt2.Unix() && tt1.Unix() < tt3.Unix()) {
}
}
}
// The purpose of this benchmark is to highlight that storing the unix value
// at time of creation is dramatically faster than frequently requesting `.Unix()`
// at runtime at scale. When dealing with the backtester and comparing
// tens of thousands of candle times
func BenchmarkJustifyIntervalTimeStoringUnixValues2(b *testing.B) {
tt1 := time.Now().Unix()
tt2 := time.Now().Add(-time.Hour).Unix()
tt3 := time.Now().Add(time.Hour).Unix()
for i := 0; i < b.N; i++ {
if tt1 >= tt2 && tt1 <= tt3 { //nolint:staticcheck // it is a benchmark to demonstrate inefficiency in calling
}
}
}
func TestConvertToNewInterval(t *testing.T) {
_, err := (*Item)(nil).ConvertToNewInterval(OneMin)
if !errors.Is(err, errNilKline) {
t.Errorf("received '%v' expected '%v'", err, errNilKline)
}
_, err = (&Item{}).ConvertToNewInterval(OneMin)
if !errors.Is(err, ErrInvalidInterval) {
t.Errorf("received '%v' expected '%v'", err, ErrInvalidInterval)
}
old := &Item{
Exchange: "lol",
Pair: currency.NewPair(currency.BTC, currency.USDT),
Asset: asset.Spot,
Interval: OneDay,
Candles: []Candle{
{
Time: time.Now(),
Open: 1337,
High: 1339,
Low: 1336,
Close: 1338,
Volume: 1337,
},
{
Time: time.Now().AddDate(0, 0, 1),
Open: 1338,
High: 2000,
Low: 1332,
Close: 1696,
Volume: 6420,
},
{
Time: time.Now().AddDate(0, 0, 2),
Open: 1696,
High: 1998,
Low: 1337,
Close: 6969,
Volume: 2520,
},
},
}
_, err = old.ConvertToNewInterval(0)
if !errors.Is(err, ErrInvalidInterval) {
t.Errorf("received '%v' expected '%v'", err, ErrInvalidInterval)
}
_, err = old.ConvertToNewInterval(OneMin)
if !errors.Is(err, ErrCanOnlyUpscaleCandles) {
t.Errorf("received '%v' expected '%v'", err, ErrCanOnlyUpscaleCandles)
}
old.Interval = ThreeDay
_, err = old.ConvertToNewInterval(OneWeek)
if !errors.Is(err, ErrWholeNumberScaling) {
t.Errorf("received '%v' expected '%v'", err, ErrWholeNumberScaling)
}
old.Interval = OneDay
newInterval := ThreeDay
newCandle, err := old.ConvertToNewInterval(newInterval)
if !errors.Is(err, nil) {
t.Fatalf("received '%v' expected '%v'", err, nil)
}
if len(newCandle.Candles) != 1 {
t.Error("expected one candle")
}
if newCandle.Candles[0].Open != 1337 &&
newCandle.Candles[0].High != 2000 &&
newCandle.Candles[0].Low != 1332 &&
newCandle.Candles[0].Close != 6969 &&
newCandle.Candles[0].Volume != (2520+6420+1337) {
t.Error("unexpected updoot")
}
old.Candles = append(old.Candles, Candle{
Time: time.Now().AddDate(0, 0, 3),
Open: 6969,
High: 1998,
Low: 2342,
Close: 7777,
Volume: 111,
})
newCandle, err = old.ConvertToNewInterval(newInterval)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if len(newCandle.Candles) != 1 {
t.Error("expected one candle")
}
_, err = old.ConvertToNewInterval(OneMonth)
if !errors.Is(err, ErrInsufficientCandleData) {
t.Errorf("received '%v' expected '%v'", err, ErrInsufficientCandleData)
}
tn := time.Now().Truncate(time.Duration(OneDay))
// Test incorrectly padded candles
old.Candles = []Candle{
{
Time: tn,
Open: 1337,
High: 1339,
Low: 1336,
Close: 1338,
Volume: 1337,
},
{
Time: tn.AddDate(0, 0, 1),
Open: 1338,
High: 2000,
Low: 1332,
Close: 1696,
Volume: 6420,
},
{
Time: tn.AddDate(0, 0, 2),
Open: 1696,
High: 1998,
Low: 1337,
Close: 6969,
Volume: 2520,
},
// empty candle should be here <---
// aaaand empty candle should be here <---
{
Time: tn.AddDate(0, 0, 5),
Open: 6969,
High: 8888,
Low: 1111,
Close: 5555,
Volume: 2520,
},
{
Time: tn.AddDate(0, 0, 6),
// Empty end padding
},
{
Time: tn.AddDate(0, 0, 7),
// Empty end padding
},
{
Time: tn.AddDate(0, 0, 8),
// Empty end padding
},
}
_, err = old.ConvertToNewInterval(newInterval)
if !errors.Is(err, errCandleDataNotPadded) {
t.Errorf("received '%v' expected '%v'", err, errCandleDataNotPadded)
}
err = old.addPadding(tn, tn.AddDate(0, 0, 9), false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
newCandle, err = old.ConvertToNewInterval(newInterval)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if len(newCandle.Candles) != 3 {
t.Errorf("received '%v' expected '%v'", len(newCandle.Candles), 3)
}
}
func TestAddPadding(t *testing.T) {
t.Parallel()
tn := time.Now().Truncate(time.Duration(OneDay))
var k *Item
err := k.addPadding(tn, tn.AddDate(0, 0, 5), false)
if !errors.Is(err, errNilKline) {
t.Fatalf("received '%v' expected '%v'", err, errNilKline)
}
k = &Item{}
k.Candles = []Candle{
{
Time: tn,
Open: 1337,
High: 1339,
Low: 1336,
Close: 1338,
Volume: 1337,
},
}
err = k.addPadding(tn, tn.AddDate(0, 0, 5), false)
if !errors.Is(err, ErrInvalidInterval) {
t.Fatalf("received '%v' expected '%v'", err, ErrInvalidInterval)
}
k.Interval = OneDay
k.Candles = []Candle{
{
Time: tn.AddDate(0, 0, 1),
Open: 1338,
High: 2000,
Low: 1332,
Close: 1696,
Volume: 6420,
},
{
Time: tn,
Open: 1337,
High: 1339,
Low: 1336,
Close: 1338,
Volume: 1337,
},
}
err = k.addPadding(tn.AddDate(0, 0, 5), tn, false)
if !errors.Is(err, errCannotEstablishTimeWindow) {
t.Fatalf("received '%v' expected '%v'", err, errCannotEstablishTimeWindow)
}
k.Candles = []Candle{
{
Time: tn.Add(time.Hour * 8),
Open: 1337,
High: 1339,
Low: 1336,
Close: 1338,
Volume: 1337,
},
{
Time: tn.AddDate(0, 0, 1).Add(time.Hour * 8),
Open: 1338,
High: 2000,
Low: 1332,
Close: 1696,
Volume: 6420,
},
{
Time: tn.AddDate(0, 0, 2).Add(time.Hour * 8),
Open: 1696,
High: 1998,
Low: 1337,
Close: 6969,
Volume: 2520,
}}
err = k.addPadding(tn, tn.AddDate(0, 0, 3), false)
if !errors.Is(err, errCandleOpenTimeIsNotUTCAligned) {
t.Fatalf("received '%v' expected '%v'", err, errCandleOpenTimeIsNotUTCAligned)
}
k.Candles = []Candle{
{
Time: tn,
Open: 1337,
High: 1339,
Low: 1336,
Close: 1338,
Volume: 1337,
},
{
Time: tn.AddDate(0, 0, 1),
Open: 1338,
High: 2000,
Low: 1332,
Close: 1696,
Volume: 6420,
},
{
Time: tn.AddDate(0, 0, 2),
Open: 1696,
High: 1998,
Low: 1337,
Close: 6969,
Volume: 2520,
}}
err = k.addPadding(tn, tn.AddDate(0, 0, 3), false)
if !errors.Is(err, nil) {
t.Fatalf("received '%v' expected '%v'", err, nil)
}
if len(k.Candles) != 3 {
t.Fatalf("received '%v' expected '%v'", len(k.Candles), 3)
}
k.Candles = append(k.Candles, Candle{
Time: tn.AddDate(0, 0, 5),
Open: 6969,
High: 8888,
Low: 1111,
Close: 5555,
Volume: 2520,
})
err = k.addPadding(tn, tn.AddDate(0, 0, 6), false)
if !errors.Is(err, nil) {
t.Fatalf("received '%v' expected '%v'", err, nil)
}
if len(k.Candles) != 6 {
t.Fatalf("received '%v' expected '%v'", len(k.Candles), 6)
}
// No candles test when there is zero activity for that period
k.Candles = nil
err = k.addPadding(tn, tn.AddDate(0, 0, 6), false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if len(k.Candles) != 6 {
t.Errorf("received '%v' expected '%v'", len(k.Candles), 6)
}
}
func TestGetClosePriceAtTime(t *testing.T) {
t.Parallel()
tt := time.Now()
k := Item{
Candles: []Candle{
{
Time: tt,
Close: 1337,
},
{
Time: tt.Add(time.Hour),
Close: 1338,
},
},
}
price, err := k.GetClosePriceAtTime(tt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if price != 1337 {
t.Errorf("received '%v' expected '%v'", price, 1337)
}
_, err = k.GetClosePriceAtTime(tt.Add(time.Minute))
if !errors.Is(err, ErrNotFoundAtTime) {
t.Errorf("received '%v' expected '%v'", err, ErrNotFoundAtTime)
}
}
func TestDeployExchangeIntervals(t *testing.T) {
t.Parallel()
exchangeIntervals := DeployExchangeIntervals()
if exchangeIntervals.ExchangeSupported(OneWeek) {
t.Errorf("received '%v' expected '%v'", exchangeIntervals.ExchangeSupported(OneWeek), false)
}
exchangeIntervals = DeployExchangeIntervals(IntervalCapacity{Interval: OneWeek})
if !exchangeIntervals.ExchangeSupported(OneWeek) {
t.Errorf("received '%v' expected '%v'", exchangeIntervals.ExchangeSupported(OneWeek), true)
}
_, err := exchangeIntervals.Construct(0)
if !errors.Is(err, ErrInvalidInterval) {
t.Errorf("received '%v' expected '%v'", err, ErrInvalidInterval)
}
_, err = exchangeIntervals.Construct(OneMin)
if !errors.Is(err, ErrCannotConstructInterval) {
t.Errorf("received '%v' expected '%v'", err, ErrCannotConstructInterval)
}
request, err := exchangeIntervals.Construct(OneWeek)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if request != OneWeek {
t.Errorf("received '%v' expected '%v'", request, OneWeek)
}
exchangeIntervals = DeployExchangeIntervals(IntervalCapacity{Interval: OneWeek}, IntervalCapacity{Interval: OneDay})
request, err = exchangeIntervals.Construct(OneMonth)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if request != OneDay {
t.Errorf("received '%v' expected '%v'", request, OneDay)
}
}
func TestSetHasDataFromCandles(t *testing.T) {
t.Parallel()
ohc := getOneHour()
localEnd := ohc[len(ohc)-1].Time.Add(OneHour.Duration())
i, err := CalculateCandleDateRanges(ohc[0].Time, localEnd, OneHour, 100000)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = i.SetHasDataFromCandles(ohc)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !i.Start.Equal(ohc[0].Time) {
t.Errorf("received '%v' expected '%v'", i.Start.Time, ohc[0].Time)
}
if !i.End.Equal(localEnd) {
t.Errorf("received '%v' expected '%v'", i.End.Time, ohc[len(ohc)-1].Time)
}
k := Item{
Interval: OneHour,
Candles: ohc[2:],
}
err = k.addPadding(i.Start.Time, i.End.Time, false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = i.SetHasDataFromCandles(k.Candles)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !i.Start.Equal(k.Candles[0].Time) {
t.Errorf("received '%v' expected '%v'", i.Start.Time, k.Candles[0].Time)
}
if i.HasDataAtDate(k.Candles[0].Time) {
t.Errorf("received '%v' expected '%v'", false, true)
}
if !i.HasDataAtDate(k.Candles[len(k.Candles)-1].Time) {
t.Errorf("received '%v' expected '%v'", true, false)
}
}
func TestGetIntervalResultLimit(t *testing.T) {
t.Parallel()
var e *ExchangeCapabilitiesEnabled
_, err := e.GetIntervalResultLimit(OneMin)
if !errors.Is(err, errExchangeCapabilitiesEnabledIsNil) {
t.Errorf("received '%v' expected '%v'", err, errExchangeCapabilitiesEnabledIsNil)
}
e = &ExchangeCapabilitiesEnabled{}
e.Intervals = ExchangeIntervals{}
_, err = e.GetIntervalResultLimit(OneDay)
if !errors.Is(err, errIntervalNotSupported) {
t.Errorf("received '%v' expected '%v'", err, errIntervalNotSupported)
}
e.Intervals = ExchangeIntervals{
supported: map[Interval]int64{
OneDay: 100000,
OneMin: 0,
},
}
_, err = e.GetIntervalResultLimit(OneMin)
if !errors.Is(err, errCannotFetchIntervalLimit) {
t.Errorf("received '%v' expected '%v'", err, errCannotFetchIntervalLimit)
}
limit, err := e.GetIntervalResultLimit(OneDay)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if limit != 100000 {
t.Errorf("received '%v' expected '%v'", limit, 100000)
}
e.GlobalResultLimit = 1337
limit, err = e.GetIntervalResultLimit(OneMin)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if limit != 1337 {
t.Errorf("received '%v' expected '%v'", limit, 1337)
}
}