From 70615279bd9acf0fd60c0e7ede3188ab53ab629e Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 24 Apr 2020 15:36:49 +1000 Subject: [PATCH] (GCTScript) Add technical analysis support via script bindings (#467) * added mfi and example * renamed to moving average * converted to array return type and added obv and mfi * started work on test coverage * test coverage added for rsi & mfi * test coverage added for all indicators removed go mod replace moved to append helper method * moved all indicators to new appendTo and increased test coverage * added additional test and bumped go-talib to latest commi * go.mod update * linter fixes * go mod clean up * small fixes * reverted changes from previous attempt to rework as data is still incorrect now passing full OHLCV data back to script binding * testing new structure of passing full ohlcv data * started linking ohlcv to gctscript * OHCLV link up completed reworking passing back to indicators started * OHCLV link up completed reworking passing back to indicators started * added test coverage for tofloat * linter fixes (gofmt) * removed unused value * improved test coverage * added correct detection for 1w added ParseInterval test coverage moved OHCLV string to const * removed unused value * first round of changes addressed * all indicators have been split with packages named after each indicator and a new calculate() method added * linters * fixed tests * added check to check ta is running in validator for uploading * Added test data for OHLCV testing new indicator interface for wrapper * typed const to float64 * reworked validator data to generate previous timestamps * rewored macd to return slice of array * adding bbands linking and example * why didn't this pick it up before :D * bumped up total number of modules for test * moved parseIndicator to exchange added comments * test coverage added for ParseMAType & ParseIndicatorSelector * gofmt * WIP changes * updated tests for bbands & obv bumped to latest go-talib * move multiple use strong to const * reverted rpc.pb.go to master * added 4w option * removed selector from obv as unneeded * improved test coverage and reworked all indicator methods on how they pass errors back * order incoming OHCLV data * revert go.mod * removed verbose toggles * added spot asset type * removed 4w as its unused/uncommon * renamed * reworked further tests * converted all examples to use coinbasepro for consistency * updated all date ranges to 2019 + 6 months * backported binance OHLCV wrapper from #479 * removed o * rounded numbers * chnage requests addressed and attempt to fix MACD... today has been really unproctive code wise :D * Migrated to gct-ta library * Corrected test import * wording changes on test * removed TA lib from go.mod * PR changes addressed Removed parallel running from tests due to slight possibility in very extreme cases TestExecution might not be set to the expected value and will cause lower test coverage * removed pkg folder * bumped gct-ta version * gct-ta version bump --- exchanges/binance/binance.go | 58 +- exchanges/binance/binance_test.go | 132 ++++ exchanges/binance/binance_types.go | 9 +- exchanges/binance/binance_wrapper.go | 35 +- exchanges/coinbasepro/coinbasepro_test.go | 4 +- gctscript/examples/exchange/ohlcv.gct | 11 + gctscript/examples/ta/atr.gct | 15 + gctscript/examples/ta/bbands.gct | 15 + gctscript/examples/ta/ema.gct | 19 + gctscript/examples/ta/macd.gct | 15 + gctscript/examples/ta/mfi.gct | 15 + gctscript/examples/ta/obv.gct | 16 + gctscript/examples/ta/rsi.gct | 15 + gctscript/modules/gct/exchange.go | 93 +++ gctscript/modules/gct/gct_test.go | 42 ++ gctscript/modules/gct/gct_types.go | 5 + gctscript/modules/loader/loader.go | 9 +- gctscript/modules/ta/indicators/atr.go | 80 +++ gctscript/modules/ta/indicators/bbands.go | 118 ++++ gctscript/modules/ta/indicators/ema.go | 63 ++ gctscript/modules/ta/indicators/indicators.go | 61 ++ .../modules/ta/indicators/indicators_test.go | 563 ++++++++++++++++++ gctscript/modules/ta/indicators/macd.go | 80 +++ gctscript/modules/ta/indicators/mfi.go | 78 +++ gctscript/modules/ta/indicators/obv.go | 75 +++ gctscript/modules/ta/indicators/rsi.go | 63 ++ gctscript/modules/ta/indicators/sma.go | 61 ++ gctscript/modules/ta/ta.go | 10 + gctscript/modules/ta/ta_test.go | 17 + gctscript/modules/ta/ta_types.go | 18 + gctscript/modules/wrapper_types.go | 10 + gctscript/wrappers/gct/exchange/exchange.go | 21 + gctscript/wrappers/validator/validator.go | 48 ++ .../wrappers/validator/validator_test.go | 14 + go.mod | 2 + go.sum | 4 + 36 files changed, 1882 insertions(+), 12 deletions(-) create mode 100644 gctscript/examples/exchange/ohlcv.gct create mode 100644 gctscript/examples/ta/atr.gct create mode 100644 gctscript/examples/ta/bbands.gct create mode 100644 gctscript/examples/ta/ema.gct create mode 100644 gctscript/examples/ta/macd.gct create mode 100644 gctscript/examples/ta/mfi.gct create mode 100644 gctscript/examples/ta/obv.gct create mode 100644 gctscript/examples/ta/rsi.gct create mode 100644 gctscript/modules/ta/indicators/atr.go create mode 100644 gctscript/modules/ta/indicators/bbands.go create mode 100644 gctscript/modules/ta/indicators/ema.go create mode 100644 gctscript/modules/ta/indicators/indicators.go create mode 100644 gctscript/modules/ta/indicators/indicators_test.go create mode 100644 gctscript/modules/ta/indicators/macd.go create mode 100644 gctscript/modules/ta/indicators/mfi.go create mode 100644 gctscript/modules/ta/indicators/obv.go create mode 100644 gctscript/modules/ta/indicators/rsi.go create mode 100644 gctscript/modules/ta/indicators/sma.go create mode 100644 gctscript/modules/ta/ta.go create mode 100644 gctscript/modules/ta/ta_test.go create mode 100644 gctscript/modules/ta/ta_types.go diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 72b469c4..39a5d813 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" "github.com/thrasher-corp/gocryptotrader/log" @@ -193,7 +194,7 @@ func (b *Binance) GetAggregatedTrades(symbol string, limit int) ([]AggregatedTra // endTime: endTime filter for the kline data func (b *Binance) GetSpotKline(arg KlinesRequestParams) ([]CandleStick, error) { var resp interface{} - var kline []CandleStick + var klineData []CandleStick params := url.Values{} params.Set("symbol", arg.Symbol) @@ -211,7 +212,7 @@ func (b *Binance) GetSpotKline(arg KlinesRequestParams) ([]CandleStick, error) { path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, candleStick, params.Encode()) if err := b.SendHTTPRequest(path, limitDefault, &resp); err != nil { - return kline, err + return klineData, err } for _, responseData := range resp.([]interface{}) { @@ -219,7 +220,12 @@ func (b *Binance) GetSpotKline(arg KlinesRequestParams) ([]CandleStick, error) { for i, individualData := range responseData.([]interface{}) { switch i { case 0: - candle.OpenTime = individualData.(float64) + tempTime := individualData.(float64) + var err error + candle.OpenTime, err = convert.TimeFromUnixTimestampFloat(tempTime) + if err != nil { + return klineData, err + } case 1: candle.Open, _ = strconv.ParseFloat(individualData.(string), 64) case 2: @@ -231,7 +237,12 @@ func (b *Binance) GetSpotKline(arg KlinesRequestParams) ([]CandleStick, error) { case 5: candle.Volume, _ = strconv.ParseFloat(individualData.(string), 64) case 6: - candle.CloseTime = individualData.(float64) + tempTime := individualData.(float64) + var err error + candle.CloseTime, err = convert.TimeFromUnixTimestampFloat(tempTime) + if err != nil { + return klineData, err + } case 7: candle.QuoteAssetVolume, _ = strconv.ParseFloat(individualData.(string), 64) case 8: @@ -242,9 +253,9 @@ func (b *Binance) GetSpotKline(arg KlinesRequestParams) ([]CandleStick, error) { candle.TakerBuyQuoteAssetVolume, _ = strconv.ParseFloat(individualData.(string), 64) } } - kline = append(kline, candle) + klineData = append(klineData, candle) } - return kline, nil + return klineData, nil } // GetAveragePrice returns current average price for a symbol. @@ -728,3 +739,38 @@ func (b *Binance) MaintainWsAuthStreamKey() error { HTTPRecording: b.HTTPRecording, }) } + +func parseInterval(in time.Duration) (TimeInterval, error) { + switch in { + case kline.OneMin: + return TimeIntervalMinute, nil + case kline.ThreeMin: + return TimeIntervalThreeMinutes, nil + case kline.FiveMin: + return TimeIntervalFiveMinutes, nil + case kline.FifteenMin: + return TimeIntervalFifteenMinutes, nil + case kline.ThirtyMin: + return TimeIntervalThirtyMinutes, nil + case kline.OneHour: + return TimeIntervalHour, nil + case kline.TwoHour: + return TimeIntervalTwoHours, nil + case kline.FourHour: + return TimeIntervalFourHours, nil + case kline.SixHour: + return TimeIntervalSixHours, nil + case kline.OneHour * 8: + return TimeIntervalEightHours, nil + case kline.TwelveHour: + return TimeIntervalTwelveHours, nil + case kline.OneDay: + return TimeIntervalDay, nil + case kline.ThreeDay: + return TimeIntervalThreeDays, nil + case kline.OneWeek: + return TimeIntervalWeek, nil + default: + return TimeIntervalMinute, errInvalidInterval + } +} diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index f15f8100..57181163 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -2,12 +2,14 @@ package binance import ( "testing" + "time" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) @@ -790,3 +792,133 @@ func TestExecutionTypeToOrderStatus(t *testing.T) { } } } + +func TestGetHistoricCandles(t *testing.T) { + if mockTests { + t.Skip("skipping test under mock as its covered by GetSpotKlines()") + } + currencyPair := currency.NewPairFromString("BTCUSDT") + start := time.Date(2017, 8, 18, 0, 0, 0, 0, time.UTC) + end := start.AddDate(0, 6, 0) + + _, err := b.GetHistoricCandles(currencyPair, asset.Spot, start, end, kline.OneDay) + if err != nil { + t.Fatal(err) + } +} + +func TestParseInterval(t *testing.T) { + testCases := []struct { + name string + interval time.Duration + expected TimeInterval + err error + }{ + { + "OneMin", + kline.OneMin, + TimeIntervalMinute, + nil, + }, + { + "ThreeMin", + kline.ThreeMin, + TimeIntervalThreeMinutes, + nil, + }, + { + "FiveMin", + kline.FiveMin, + TimeIntervalFiveMinutes, + nil, + }, + { + "FifteenMin", + kline.FifteenMin, + TimeIntervalFifteenMinutes, + nil, + }, + { + "ThirtyMin", + kline.ThirtyMin, + TimeIntervalThirtyMinutes, + nil, + }, + { + "OneHour", + kline.OneHour, + TimeIntervalHour, + nil, + }, + { + "TwoHour", + kline.TwoHour, + TimeIntervalTwoHours, + nil, + }, + { + "FourHour", + kline.FourHour, + TimeIntervalFourHours, + nil, + }, + { + "SixHour", + kline.SixHour, + TimeIntervalSixHours, + nil, + }, + { + "EightHour", + kline.OneHour * 8, + TimeIntervalEightHours, + nil, + }, + { + "TwelveHour", + kline.TwelveHour, + TimeIntervalTwelveHours, + nil, + }, + { + "OneDay", + kline.OneDay, + TimeIntervalDay, + nil, + }, + { + "ThreeDay", + kline.ThreeDay, + TimeIntervalThreeDays, + nil, + }, + { + "OneWeek", + kline.OneWeek, + TimeIntervalWeek, + nil, + }, + { + "default", + time.Hour * 1337, + TimeIntervalHour, + errInvalidInterval, + }, + } + + for x := range testCases { + test := testCases[x] + t.Run(test.name, func(t *testing.T) { + v, err := parseInterval(test.interval) + if err != nil { + if err != test.err { + t.Fatal(err) + } + } else { + if v != test.expected { + t.Fatalf("%v: received %v expected %v", test.name, v, test.expected) + } + } + }) + } +} diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index 6bde3f08..c9a6e825 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -1,9 +1,14 @@ package binance import ( + "errors" + "time" + "github.com/thrasher-corp/gocryptotrader/currency" ) +var errInvalidInterval = errors.New("invalid interval") + // Response holds basic binance api response data type Response struct { Code int `json:"code"` @@ -208,13 +213,13 @@ type AggregatedTrade struct { // CandleStick holds kline data type CandleStick struct { - OpenTime float64 + OpenTime time.Time Open float64 High float64 Low float64 Close float64 Volume float64 - CloseTime float64 + CloseTime time.Time QuoteAssetVolume float64 TradeCount float64 TakerBuyAssetVolume float64 diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 4bba2a0a..c3bf2b7c 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -674,5 +674,38 @@ func (b *Binance) ValidateCredentials() error { // GetHistoricCandles returns candles between a time period for a set time interval func (b *Binance) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) { - return kline.Item{}, common.ErrNotYetImplemented + intervalToString, err := parseInterval(interval) + if err != nil { + return kline.Item{}, err + } + klineParams := KlinesRequestParams{ + Interval: intervalToString, + Symbol: b.FormatExchangeCurrency(pair, a).String(), + StartTime: start.Unix() * 1000, + EndTime: end.Unix() * 1000, + } + + candles, err := b.GetSpotKline(klineParams) + if err != nil { + return kline.Item{}, err + } + + ret := kline.Item{ + Exchange: b.Name, + Pair: pair, + Asset: a, + Interval: interval, + } + + for x := range candles { + ret.Candles = append(ret.Candles, kline.Candle{ + Time: candles[x].OpenTime, + Open: candles[x].Open, + High: candles[x].Close, + Low: candles[x].Low, + Close: candles[x].Close, + Volume: candles[x].Volume, + }) + } + return ret, nil } diff --git a/exchanges/coinbasepro/coinbasepro_test.go b/exchanges/coinbasepro/coinbasepro_test.go index 511609dc..72d78416 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -79,10 +79,10 @@ func TestGetTrades(t *testing.T) { func TestGetHistoricRatesGranularityCheck(t *testing.T) { end := time.Now().UTC() - start := end.Add(-time.Second * 300) + start := end.Add(-time.Hour * 24) p := currency.NewPair(currency.BTC, currency.USD) - _, err := c.GetHistoricCandles(p, asset.Spot, start, end, time.Minute) + _, err := c.GetHistoricCandles(p, asset.Spot, start, end, time.Hour) if err != nil { t.Fatal(err) } diff --git a/gctscript/examples/exchange/ohlcv.gct b/gctscript/examples/exchange/ohlcv.gct new file mode 100644 index 00000000..ff0cabfd --- /dev/null +++ b/gctscript/examples/exchange/ohlcv.gct @@ -0,0 +1,11 @@ +fmt := import("fmt") +exch := import("exchange") +t := import("times") + +load := func() { + start := t.add(t.now(), -t.hour*24) + ohlcvData := exch.ohlcv("coinbasepro", "BTC-USD", "-", "SPOT", start, t.now(), "1h") + fmt.println(ohlcvData) +} + +load() diff --git a/gctscript/examples/ta/atr.gct b/gctscript/examples/ta/atr.gct new file mode 100644 index 00000000..c421a869 --- /dev/null +++ b/gctscript/examples/ta/atr.gct @@ -0,0 +1,15 @@ +fmt := import("fmt") +exch := import("exchange") +t := import("times") +atr := import("indicator/atr") + +load := func() { + start := t.date(2017, 8 , 17, 0 , 0 , 0, 0) + end := t.add_date(start, 0, 6 , 0) + ohlcvData := exch.ohlcv("binance", "BTC-USDT", "-", "SPOT", start, end, "1d") + + ret := atr.calculate(ohlcvData.candles, 14) + fmt.println(ret) +} + +load() diff --git a/gctscript/examples/ta/bbands.gct b/gctscript/examples/ta/bbands.gct new file mode 100644 index 00000000..230521c6 --- /dev/null +++ b/gctscript/examples/ta/bbands.gct @@ -0,0 +1,15 @@ +fmt := import("fmt") +exch := import("exchange") +t := import("times") +bbands := import("indicator/bbands") + +load := func() { + start := t.date(2017, 8 , 17 , 0 , 0 , 0, 0) + end := t.add_date(start, 0, 6 , 0) + ohlcvData := exch.ohlcv("binance", "BTC-USDT", "-", "SPOT", start, end, "1d") + + ret := bbands.calculate("close", ohlcvData.candles, 20, 2.0, 2.0, "sma") + fmt.println(ret) +} + +load() diff --git a/gctscript/examples/ta/ema.gct b/gctscript/examples/ta/ema.gct new file mode 100644 index 00000000..d719e2e7 --- /dev/null +++ b/gctscript/examples/ta/ema.gct @@ -0,0 +1,19 @@ +fmt := import("fmt") +exch := import("exchange") +t := import("times") +sma := import("indicator/sma") +ema := import("indicator/ema") + +load := func() { + start := t.date(2017, 8 , 17 , 0 , 0 , 0, 0) + end := t.add_date(start, 0, 6 , 0) + ohlcvData := exch.ohlcv("binance", "BTC-USDT", "-", "SPOT", start, end, "1d") + + ret := ema.calculate(ohlcvData.candles, 9) + fmt.println(ret) + + ret = sma.calculate(ohlcvData.candles, 9) + fmt.println(ret) +} + +load() diff --git a/gctscript/examples/ta/macd.gct b/gctscript/examples/ta/macd.gct new file mode 100644 index 00000000..41e456c2 --- /dev/null +++ b/gctscript/examples/ta/macd.gct @@ -0,0 +1,15 @@ +fmt := import("fmt") +exch := import("exchange") +t := import("times") +macd := import("indicator/macd") + +load := func() { + start := t.date(2017, 8 , 17 , 0 , 0 , 0, 0) + end := t.add_date(start, 0, 6 , 0) + ohlcvData := exch.ohlcv("binance", "BTC-USDT", "-", "SPOT", start, end, "1d") + + ret := macd.calculate(ohlcvData.candles, 12, 26, 9) + fmt.println(ret) +} + +load() diff --git a/gctscript/examples/ta/mfi.gct b/gctscript/examples/ta/mfi.gct new file mode 100644 index 00000000..80e364f6 --- /dev/null +++ b/gctscript/examples/ta/mfi.gct @@ -0,0 +1,15 @@ +fmt := import("fmt") +exch := import("exchange") +t := import("times") +mfi := import("indicator/mfi") + +load := func() { + start := t.date(2017, 8 , 17 , 0 , 0 , 0, 0) + end := t.add_date(start, 0, 6 , 0) + ohlcvData := exch.ohlcv("binance", "BTC-USDT", "-", "SPOT", start, end, "1d") + + ret := mfi.calculate(ohlcvData.candles, 14) + fmt.println(ret) +} + +load() diff --git a/gctscript/examples/ta/obv.gct b/gctscript/examples/ta/obv.gct new file mode 100644 index 00000000..d6bc5989 --- /dev/null +++ b/gctscript/examples/ta/obv.gct @@ -0,0 +1,16 @@ +fmt := import("fmt") +exch := import("exchange") +t := import("times") +obv := import("indicator/obv") + +load := func() { + start := t.date(2017, 8 , 17 , 0 , 0 , 0, 0) + end := t.add_date(start, 0, 6 , 0) + ohlcvData := exch.ohlcv("binance", "BTC-USDT", "-", "SPOT", start, end, "1d") + + ret := obv.calculate(ohlcvData.candles) + fmt.println(ret) +} + +load() + diff --git a/gctscript/examples/ta/rsi.gct b/gctscript/examples/ta/rsi.gct new file mode 100644 index 00000000..dad5affe --- /dev/null +++ b/gctscript/examples/ta/rsi.gct @@ -0,0 +1,15 @@ +fmt := import("fmt") +exch := import("exchange") +t := import("times") +rsi := import("indicator/rsi") + +load := func() { + start := t.date(2017, 8 , 17 , 0 , 0 , 0, 0) + end := t.add_date(start, 0, 6 , 0) + ohlcvData := exch.ohlcv("binance", "BTC-USDT", "-", "SPOT", start, end, "1d") + + ret := rsi.calculate(ohlcvData.candles, 14) + fmt.println(ret) +} + +load() diff --git a/gctscript/modules/gct/exchange.go b/gctscript/modules/gct/exchange.go index 80f96d4a..de1a341a 100644 --- a/gctscript/modules/gct/exchange.go +++ b/gctscript/modules/gct/exchange.go @@ -3,8 +3,10 @@ package gct import ( "fmt" "strings" + "time" objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -24,6 +26,7 @@ var exchangeModule = map[string]objects.Object{ "ordersubmit": &objects.UserFunction{Name: "ordersubmit", Value: ExchangeOrderSubmit}, "withdrawcrypto": &objects.UserFunction{Name: "withdrawcrypto", Value: ExchangeWithdrawCrypto}, "withdrawfiat": &objects.UserFunction{Name: "withdrawfiat", Value: ExchangeWithdrawFiat}, + "ohlcv": &objects.UserFunction{Name: "ohlcv", Value: exchangeOHLCV}, } // ExchangeOrderbook returns orderbook for requested exchange & currencypair @@ -494,3 +497,93 @@ func ExchangeWithdrawFiat(args ...objects.Object) (objects.Object, error) { return &objects.String{Value: rtn}, nil } + +func exchangeOHLCV(args ...objects.Object) (objects.Object, error) { + if len(args) != 7 { + return nil, objects.ErrWrongNumArguments + } + + exchangeName, ok := objects.ToString(args[0]) + if !ok { + return nil, fmt.Errorf(ErrParameterConvertFailed, exchangeName) + } + currencyPair, ok := objects.ToString(args[1]) + if !ok { + return nil, fmt.Errorf(ErrParameterConvertFailed, currencyPair) + } + delimiter, ok := objects.ToString(args[2]) + if !ok { + return nil, fmt.Errorf(ErrParameterConvertFailed, delimiter) + } + assetTypeParam, ok := objects.ToString(args[3]) + if !ok { + return nil, fmt.Errorf(ErrParameterConvertFailed, assetTypeParam) + } + + startTime, ok := objects.ToTime(args[4]) + if !ok { + return nil, fmt.Errorf(ErrParameterConvertFailed, startTime) + } + + endTime, ok := objects.ToTime(args[5]) + if !ok { + return nil, fmt.Errorf(ErrParameterConvertFailed, endTime) + } + + intervalStr, ok := objects.ToString(args[6]) + if !ok { + return nil, fmt.Errorf(ErrParameterConvertFailed, endTime) + } + interval, err := parseInterval(intervalStr) + if err != nil { + return nil, err + } + pairs := currency.NewPairDelimiter(currencyPair, delimiter) + assetType := asset.Item(assetTypeParam) + + ret, err := wrappers.GetWrapper().OHLCV(exchangeName, pairs, assetType, startTime, endTime, interval) + if err != nil { + return nil, err + } + + var candles objects.Array + for x := range ret.Candles { + candle := &objects.Array{} + candle.Value = append(candle.Value, &objects.Time{Value: ret.Candles[x].Time}, + &objects.Float{Value: ret.Candles[x].Open}, + &objects.Float{Value: ret.Candles[x].High}, + &objects.Float{Value: ret.Candles[x].Low}, + &objects.Float{Value: ret.Candles[x].Close}, + &objects.Float{Value: ret.Candles[x].Volume}, + ) + + candles.Value = append(candles.Value, candle) + } + + retValue := make(map[string]objects.Object, 5) + retValue["exchange"] = &objects.String{Value: ret.Exchange} + retValue["pair"] = &objects.String{Value: ret.Pair.String()} + retValue["asset"] = &objects.String{Value: ret.Asset.String()} + retValue["intervals"] = &objects.String{Value: ret.Interval.String()} + retValue["candles"] = &candles + + return &objects.Map{ + Value: retValue, + }, nil +} + +// parseInterval will parse the interval param of indictors that have them and convert to time.Duration +func parseInterval(in string) (time.Duration, error) { + if !common.StringDataContainsInsensitive(supportedDurations, in) { + return time.Nanosecond, errInvalidInterval + } + switch in { + case "1d": + in = "24h" + case "3d": + in = "72h" + case "1w": + in = "168h" + } + return time.ParseDuration(in) +} diff --git a/gctscript/modules/gct/gct_test.go b/gctscript/modules/gct/gct_test.go index d279d26d..595856e6 100644 --- a/gctscript/modules/gct/gct_test.go +++ b/gctscript/modules/gct/gct_test.go @@ -5,6 +5,7 @@ import ( "os" "reflect" "testing" + "time" objects "github.com/d5/tengo/v2" "github.com/thrasher-corp/gocryptotrader/gctscript/modules" @@ -268,3 +269,44 @@ func TestExchangeWithdrawFiat(t *testing.T) { t.Fatal(err) } } + +func TestParseInterval(t *testing.T) { + v, err := parseInterval("1h") + if err != nil { + t.Fatal(err) + } + if v != time.Hour { + t.Fatalf("unexpected value return expected %v received %v", time.Hour, v) + } + + v, err = parseInterval("1d") + if err != nil { + t.Fatal(err) + } + if v != time.Hour*24 { + t.Fatalf("unexpected value return expected %v received %v", time.Hour*24, v) + } + + v, err = parseInterval("3d") + if err != nil { + t.Fatal(err) + } + if v != time.Hour*72 { + t.Fatalf("unexpected value return expected %v received %v", time.Hour*72, v) + } + + v, err = parseInterval("1w") + if err != nil { + t.Fatal(err) + } + if v != time.Hour*168 { + t.Fatalf("unexpected value return expected %v received %v", time.Hour*168, v) + } + + _, err = parseInterval("6m") + if err != nil { + if !errors.Is(err, errInvalidInterval) { + t.Fatal(err) + } + } +} diff --git a/gctscript/modules/gct/gct_types.go b/gctscript/modules/gct/gct_types.go index 6f67de3f..0f78acb4 100644 --- a/gctscript/modules/gct/gct_types.go +++ b/gctscript/modules/gct/gct_types.go @@ -1,6 +1,8 @@ package gct import ( + "errors" + "github.com/d5/tengo/v2" ) @@ -9,6 +11,9 @@ const ( ErrParameterConvertFailed = "%v failed conversion" ) +var errInvalidInterval = errors.New("invalid interval") +var supportedDurations = []string{"1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "12h", "24h", "1d", "3d", "1w"} + // Modules map of all loadable modules var Modules = map[string]map[string]tengo.Object{ "exchange": exchangeModule, diff --git a/gctscript/modules/loader/loader.go b/gctscript/modules/loader/loader.go index e4764d70..06eb3201 100644 --- a/gctscript/modules/loader/loader.go +++ b/gctscript/modules/loader/loader.go @@ -3,8 +3,8 @@ package loader import ( "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/stdlib" - "github.com/thrasher-corp/gocryptotrader/gctscript/modules/gct" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules/ta" ) // GetModuleMap returns the module map that includes all modules @@ -19,6 +19,13 @@ func GetModuleMap() *tengo.ModuleMap { } } + taModuleList := ta.AllModuleNames() + for _, name := range taModuleList { + if mod := ta.Modules[name]; mod != nil { + modules.AddBuiltinModule(name, mod) + } + } + stdLib := stdlib.AllModuleNames() for _, name := range stdLib { if mod := stdlib.BuiltinModules[name]; mod != nil { diff --git a/gctscript/modules/ta/indicators/atr.go b/gctscript/modules/ta/indicators/atr.go new file mode 100644 index 00000000..53a01c6f --- /dev/null +++ b/gctscript/modules/ta/indicators/atr.go @@ -0,0 +1,80 @@ +package indicators + +import ( + "errors" + "fmt" + "math" + "strings" + + objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules" + "github.com/thrasher-corp/gocryptotrader/gctscript/wrappers/validator" +) + +// AtrModule range indicator commands +var AtrModule = map[string]objects.Object{ + "calculate": &objects.UserFunction{Name: "calculate", Value: atr}, +} + +func atr(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + r := &objects.Array{} + if validator.IsTestExecution.Load() == true { + return r, nil + } + + ohlcvInput := objects.ToInterface(args[0]) + ohlcvInputData, valid := ohlcvInput.([]interface{}) + if !valid { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, OHLCV) + } + + ohlcvData := make([][]float64, 6) + var allErrors []string + for x := range ohlcvInputData { + t := ohlcvInputData[x].([]interface{}) + value, err := toFloat64(t[2]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[2] = append(ohlcvData[2], value) + + value, err = toFloat64(t[3]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[3] = append(ohlcvData[3], value) + + value, err = toFloat64(t[4]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[4] = append(ohlcvData[4], value) + + value, err = toFloat64(t[5]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[5] = append(ohlcvData[5], value) + } + + inTimePeriod, ok := objects.ToInt(args[1]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inTimePeriod)) + } + + if len(allErrors) > 0 { + return nil, errors.New(strings.Join(allErrors, ", ")) + } + + ret := indicators.ATR(ohlcvData[2], ohlcvData[3], ohlcvData[4], inTimePeriod) + for x := range ret { + r.Value = append(r.Value, &objects.Float{Value: math.Round(ret[x]*100) / 100}) + } + + return r, nil +} diff --git a/gctscript/modules/ta/indicators/bbands.go b/gctscript/modules/ta/indicators/bbands.go new file mode 100644 index 00000000..f8d90ea5 --- /dev/null +++ b/gctscript/modules/ta/indicators/bbands.go @@ -0,0 +1,118 @@ +package indicators + +import ( + "errors" + "fmt" + "math" + "strings" + + objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules" + "github.com/thrasher-corp/gocryptotrader/gctscript/wrappers/validator" +) + +// BBandsModule bollinger bands indicator commands +var BBandsModule = map[string]objects.Object{ + "calculate": &objects.UserFunction{Name: "calculate", Value: bbands}, +} + +func bbands(args ...objects.Object) (objects.Object, error) { + if len(args) != 6 { + return nil, objects.ErrWrongNumArguments + } + + var ret objects.Array + if validator.IsTestExecution.Load() == true { + return &ret, nil + } + + ohlcIndicatorType, ok := objects.ToString(args[0]) + if !ok { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, ohlcIndicatorType) + } + + selector, errIndSelector := ParseIndicatorSelector(ohlcIndicatorType) + if errIndSelector != nil { + return nil, errIndSelector + } + + ohlcvInput := objects.ToInterface(args[1]) + ohlcvInputData, valid := ohlcvInput.([]interface{}) + if !valid { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, OHLCV) + } + + ohlcvData := make([][]float64, 6) + var allErrors []string + for x := range ohlcvInputData { + t := ohlcvInputData[x].([]interface{}) + value, err := toFloat64(t[2]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[2] = append(ohlcvData[2], value) + + value, err = toFloat64(t[3]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[3] = append(ohlcvData[3], value) + + value, err = toFloat64(t[4]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[4] = append(ohlcvData[4], value) + + value, err = toFloat64(t[5]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[5] = append(ohlcvData[5], value) + } + + inTimePeriod, ok := objects.ToInt(args[2]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inTimePeriod)) + } + + inNbDevUp, ok := objects.ToFloat64(args[3]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inNbDevUp)) + } + + inNbDevDn, ok := objects.ToFloat64(args[4]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inNbDevDn)) + } + + inMAType, ok := objects.ToString(args[5]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inMAType)) + } + + if len(allErrors) > 0 { + return nil, errors.New(strings.Join(allErrors, ", ")) + } + + MAType, err := ParseMAType(inMAType) + if err != nil { + return nil, err + } + + retUpper, retMiddle, retLower := indicators.BBANDS(ohlcvData[selector], inTimePeriod, inNbDevDn, inNbDevDn, MAType) + for x := range retMiddle { + temp := &objects.Array{} + temp.Value = append(temp.Value, &objects.Float{Value: math.Round(retMiddle[x]*100) / 100}) + if retUpper != nil { + temp.Value = append(temp.Value, &objects.Float{Value: math.Round(retUpper[x]*100) / 100}) + } + if retLower != nil { + temp.Value = append(temp.Value, &objects.Float{Value: math.Round(retLower[x]*100) / 100}) + } + ret.Value = append(ret.Value, temp) + } + + return &ret, nil +} diff --git a/gctscript/modules/ta/indicators/ema.go b/gctscript/modules/ta/indicators/ema.go new file mode 100644 index 00000000..f903b40c --- /dev/null +++ b/gctscript/modules/ta/indicators/ema.go @@ -0,0 +1,63 @@ +package indicators + +import ( + "errors" + "fmt" + "math" + "strings" + + objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules" + "github.com/thrasher-corp/gocryptotrader/gctscript/wrappers/validator" +) + +// EMAModule EMA indicator commands +var EMAModule = map[string]objects.Object{ + "calculate": &objects.UserFunction{Name: "calculate", Value: ema}, +} + +func ema(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + r := &objects.Array{} + if validator.IsTestExecution.Load() == true { + return r, nil + } + + ohlcvInput := objects.ToInterface(args[0]) + ohlcvInputData, valid := ohlcvInput.([]interface{}) + if !valid { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, OHLCV) + } + + var ohlcvClose []float64 + var allErrors []string + for x := range ohlcvInputData { + t := ohlcvInputData[x].([]interface{}) + + value, err := toFloat64(t[4]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvClose = append(ohlcvClose, value) + } + + inTimePeriod, ok := objects.ToInt(args[1]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inTimePeriod)) + } + + if len(allErrors) > 0 { + return nil, errors.New(strings.Join(allErrors, ", ")) + } + + ret := indicators.EMA(ohlcvClose, inTimePeriod) + for x := range ret { + r.Value = append(r.Value, &objects.Float{Value: math.Round(ret[x]*100) / 100}) + } + + return r, nil +} diff --git a/gctscript/modules/ta/indicators/indicators.go b/gctscript/modules/ta/indicators/indicators.go new file mode 100644 index 00000000..3b404c61 --- /dev/null +++ b/gctscript/modules/ta/indicators/indicators.go @@ -0,0 +1,61 @@ +package indicators + +import ( + "errors" + "fmt" + "strings" + + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules" +) + +// OHLCV locale string for OHLCV data conversion failure +const OHLCV = "OHLCV data" + +var errInvalidSelector = errors.New("invalid selector") + +func toFloat64(data interface{}) (float64, error) { + switch d := data.(type) { + case float64: + return d, nil + case int: + return float64(d), nil + case int32: + return float64(d), nil + case int64: + return float64(d), nil + default: + return 0, fmt.Errorf(modules.ErrParameterConvertFailed, d) + } +} + +// ParseIndicatorSelector returns indicator number from string for slice selection +func ParseIndicatorSelector(in string) (int, error) { + switch in { + case "open": + return 1, nil + case "high": + return 2, nil + case "low": + return 3, nil + case "close": + return 4, nil + case "vol": + return 5, nil + default: + return 0, errInvalidSelector + } +} + +// ParseMAType returns moving average from sring +func ParseMAType(in string) (indicators.MaType, error) { + in = strings.ToLower(in) + switch in { + case "sma": + return indicators.Sma, nil + case "ema": + return indicators.Ema, nil + default: + return 0, errInvalidSelector + } +} diff --git a/gctscript/modules/ta/indicators/indicators_test.go b/gctscript/modules/ta/indicators/indicators_test.go new file mode 100644 index 00000000..a6618043 --- /dev/null +++ b/gctscript/modules/ta/indicators/indicators_test.go @@ -0,0 +1,563 @@ +package indicators + +import ( + "errors" + "math/rand" + "os" + "reflect" + "testing" + "time" + + objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/wrappers/validator" +) + +const errFailedConversion = "0 failed conversion" + +var ( + ohlcvData = &objects.Array{} + ohlcvDataInvalid = &objects.Array{} + testString = "1D10TH0R53" +) + +func TestMain(m *testing.M) { + for x := 0; x < 100; x++ { + v := rand.Float64() + candle := &objects.Array{} + candle.Value = append(candle.Value, &objects.Time{Value: time.Now()}, + &objects.Float{Value: v}, + &objects.Float{Value: v + float64(x)}, + &objects.Float{Value: v - float64(x)}, + &objects.Float{Value: v}, + &objects.Float{Value: v}, + ) + ohlcvData.Value = append(ohlcvData.Value, candle) + } + + for x := 0; x < 5; x++ { + candle := &objects.Array{} + candle.Value = append(candle.Value, &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + ) + ohlcvDataInvalid.Value = append(ohlcvDataInvalid.Value, candle) + } + + os.Exit(m.Run()) +} + +func TestMfi(t *testing.T) { + _, err := mfi() + if err != nil { + if !errors.Is(err, objects.ErrWrongNumArguments) { + t.Error(err) + } + } + + v := &objects.String{Value: testString} + _, err = mfi(ohlcvData, v) + if err != nil { + if err.Error() != errFailedConversion { + t.Error(err) + } + } + + _, err = mfi(ohlcvDataInvalid, &objects.Int{Value: 14}) + if err == nil { + t.Error("expected conversion failed error") + } + + _, err = mfi(ohlcvData, &objects.Int{Value: 14}) + if err != nil { + t.Error(err) + } + + _, err = mfi(v, &objects.Int{Value: 14}) + if err != nil { + if err.Error() != "OHLCV data failed conversion" { + t.Error(err) + } + } + + validator.IsTestExecution.Store(true) + ret, err := mfi(ohlcvData, &objects.Int{Value: 10}) + if err != nil { + t.Fatal(err) + } + if (ret == &objects.Array{}) { + t.Error("expected empty Array on test execution received data") + } + validator.IsTestExecution.Store(false) +} + +func TestRsi(t *testing.T) { + _, err := rsi() + if err != nil { + if !errors.Is(err, objects.ErrWrongNumArguments) { + t.Error(err) + } + } + + v := &objects.String{Value: testString} + _, err = rsi(ohlcvData, v) + if err != nil { + if err.Error() != errFailedConversion { + t.Error(err) + } + } + + _, err = rsi(ohlcvData, &objects.Int{Value: 14}) + if err != nil { + t.Error(err) + } + + _, err = rsi(v, &objects.Int{Value: 14}) + if err == nil { + if err.Error() != "OHLCV data failed conversion" { + t.Error(err) + } + } + + _, err = rsi(ohlcvDataInvalid, &objects.Int{Value: 14}) + if err == nil { + if err.Error() != "OHLCV data failed conversion" { + t.Error(err) + } + } + + validator.IsTestExecution.Store(true) + ret, err := rsi(ohlcvData, &objects.Int{Value: 14}) + if err != nil { + t.Fatal(err) + } + if (ret == &objects.Array{}) { + t.Error("expected empty Array on test execution received data") + } + validator.IsTestExecution.Store(false) +} + +func TestEMA(t *testing.T) { + _, err := ema() + if err != nil { + if !errors.Is(err, objects.ErrWrongNumArguments) { + t.Error(err) + } + } + + v := &objects.String{Value: testString} + _, err = ema(ohlcvData, v) + if err != nil { + if err.Error() != errFailedConversion { + t.Error(err) + } + } + + _, err = ema(ohlcvData, &objects.Int{Value: 14}) + if err != nil { + t.Error(err) + } + + _, err = ema(ohlcvDataInvalid, &objects.String{Value: testString}) + if err == nil { + t.Error("expected conversion failed error") + } + + _, err = ema(&objects.String{Value: testString}, &objects.String{Value: testString}) + if err == nil { + t.Error("expected conversion failed error") + } + + validator.IsTestExecution.Store(true) + ret, err := ema(ohlcvData, &objects.Int{Value: 14}) + if err != nil { + t.Fatal(err) + } + if (ret == &objects.Array{}) { + t.Error("expected empty Array on test execution received data") + } + validator.IsTestExecution.Store(false) +} + +func TestSMA(t *testing.T) { + _, err := sma() + if err != nil { + if !errors.Is(err, objects.ErrWrongNumArguments) { + t.Error(err) + } + } + + v := &objects.String{Value: testString} + _, err = sma(ohlcvData, v) + if err != nil { + if err.Error() != errFailedConversion { + t.Error(err) + } + } + + _, err = sma(ohlcvData, &objects.Int{Value: 14}) + if err != nil { + t.Error(err) + } + + _, err = sma(ohlcvDataInvalid, &objects.String{Value: testString}) + if err == nil { + t.Error("expected conversion failed error") + } + + _, err = sma(&objects.String{Value: testString}, &objects.String{Value: testString}) + if err == nil { + t.Error("expected conversion failed error") + } + + validator.IsTestExecution.Store(true) + ret, err := sma(ohlcvData, &objects.Int{Value: 14}) + if err != nil { + t.Fatal(err) + } + if (ret == &objects.Array{}) { + t.Error("expected empty Array on test execution received data") + } + validator.IsTestExecution.Store(false) +} + +func TestMACD(t *testing.T) { + _, err := macd() + if err != nil { + if !errors.Is(err, objects.ErrWrongNumArguments) { + t.Error(err) + } + } + + v := &objects.String{Value: testString} + _, err = macd(ohlcvData, &objects.Int{Value: 12}, &objects.Int{Value: 26}, v) + if err != nil { + if err.Error() != errFailedConversion { + t.Error(err) + } + } + + _, err = macd(ohlcvData, &objects.Int{Value: 12}, &objects.Int{Value: 26}, &objects.Int{Value: 9}) + if err != nil { + t.Error(err) + } + + _, err = macd(ohlcvDataInvalid, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}) + if err == nil { + t.Error("expected conversion failed error") + } + + _, err = macd(&objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}) + if err == nil { + t.Error("expected conversion failed error") + } + + validator.IsTestExecution.Store(true) + ret, err := macd(ohlcvData, &objects.Int{Value: 12}, &objects.Int{Value: 26}, &objects.Int{Value: 9}) + if err != nil { + t.Fatal(err) + } + if (ret == &objects.Array{}) { + t.Error("expected empty Array on test execution received data") + } + validator.IsTestExecution.Store(false) +} + +func TestAtr(t *testing.T) { + _, err := atr() + if err != nil { + if !errors.Is(err, objects.ErrWrongNumArguments) { + t.Error(err) + } + } + + v := &objects.String{Value: testString} + _, err = atr(ohlcvData, v) + if err != nil { + if err.Error() != errFailedConversion { + t.Error(err) + } + } + + _, err = atr(ohlcvData, &objects.Int{Value: 14}) + if err != nil { + t.Error(err) + } + + _, err = atr(v, &objects.Int{Value: 14}) + if err == nil { + t.Error("expected conversion failed error") + } + + _, err = atr(ohlcvDataInvalid, &objects.Int{Value: 14}) + if err == nil { + t.Error("expected conversion failed error") + } + + validator.IsTestExecution.Store(true) + ret, err := atr(ohlcvData, &objects.Int{Value: 14}) + if err != nil { + t.Fatal(err) + } + if (ret == &objects.Array{}) { + t.Error("expected empty Array on test execution received data") + } + validator.IsTestExecution.Store(false) +} + +func TestBbands(t *testing.T) { + _, err := bbands() + if err != nil { + if !errors.Is(err, objects.ErrWrongNumArguments) { + t.Error(err) + } + } + + _, err = bbands(&objects.String{Value: testString}, ohlcvData, + &objects.Int{Value: 5}, + &objects.Float{Value: 2.0}, + &objects.Float{Value: 2.0}, + &objects.String{Value: "sma"}) + if err != nil { + if err != errInvalidSelector { + t.Error(err) + } + } + + _, err = bbands(&objects.String{Value: "close"}, ohlcvData, + &objects.Int{Value: 5}, + &objects.Float{Value: 2.0}, + &objects.Float{Value: 2.0}, + &objects.String{Value: "sma"}) + if err != nil { + t.Error(err) + } + + validator.IsTestExecution.Store(true) + ret, err := bbands(&objects.String{Value: "close"}, ohlcvData, + &objects.Int{Value: 5}, + &objects.Float{Value: 2.0}, + &objects.Float{Value: 2.0}, + &objects.String{Value: "sma"}) + if err != nil { + t.Error(err) + } + if (ret == &objects.Array{}) { + t.Error("expected empty Array on test execution received data") + } + validator.IsTestExecution.Store(false) + + _, err = bbands(&objects.String{Value: "close"}, ohlcvDataInvalid, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + objects.UndefinedValue) + if err == nil { + t.Error("expected conversion failed error") + } + + _, err = bbands(&objects.String{Value: "close"}, &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: testString}, + &objects.String{Value: "ema"}) + if err == nil { + t.Error("expected conversion failed error") + } + + _, err = bbands(&objects.String{Value: "close"}, ohlcvData, + &objects.Int{Value: 5}, + &objects.Float{Value: 2.0}, + &objects.Float{Value: 2.0}, + &objects.String{Value: testString}) + if err != nil { + if !errors.Is(err, errInvalidSelector) { + t.Error(err) + } + } + + _, err = bbands(objects.UndefinedValue, ohlcvData, + &objects.Int{Value: 5}, + &objects.Float{Value: 2.0}, + &objects.Float{Value: 2.0}, + &objects.String{Value: testString}) + if err == nil { + t.Error("expected conversion failed error") + } +} + +func TestOBV(t *testing.T) { + _, err := obv() + if err != nil { + if !errors.Is(err, objects.ErrWrongNumArguments) { + t.Error(err) + } + } + + _, err = obv(ohlcvData) + if err != nil { + t.Error(err) + } + + _, err = obv(ohlcvDataInvalid) + if err == nil { + t.Error("expected conversion failed error") + } + + _, err = obv(&objects.String{Value: testString}) + if err == nil { + t.Error("expected conversion failed error") + } + + validator.IsTestExecution.Store(true) + ret, err := obv(ohlcvData) + if err != nil { + t.Fatal(err) + } + if (ret == &objects.Array{}) { + t.Error("expected empty Array on test execution received data") + } + validator.IsTestExecution.Store(false) +} + +func TestToFloat64(t *testing.T) { + value := 54.0 + v, err := toFloat64(value) + if err != nil { + t.Fatal(err) + } + if reflect.TypeOf(v).Kind() != reflect.Float64 { + t.Fatalf("expected toFloat to return kind float64 received: %v", reflect.TypeOf(v).Kind()) + } + + v, err = toFloat64(int(value)) + if err != nil { + t.Fatal(err) + } + if reflect.TypeOf(v).Kind() != reflect.Float64 { + t.Fatalf("expected toFloat to return kind float64 received: %v", reflect.TypeOf(v).Kind()) + } + + v, err = toFloat64(int32(value)) + if err != nil { + t.Fatal(err) + } + if reflect.TypeOf(v).Kind() != reflect.Float64 { + t.Fatalf("expected toFloat to return kind float64 received: %v", reflect.TypeOf(v).Kind()) + } + + v, err = toFloat64(int64(value)) + if err != nil { + t.Fatal(err) + } + if reflect.TypeOf(v).Kind() != reflect.Float64 { + t.Fatalf("expected toFloat to return kind float64 received: %v", reflect.TypeOf(v).Kind()) + } + + _, err = toFloat64("54") + if err == nil { + t.Fatalf("attempting to convert a string should fail but test passed") + } +} + +func TestParseIndicatorSelector(t *testing.T) { + testCases := []struct { + name string + expected int + err error + }{ + { + "open", + 1, + nil, + }, + { + "high", + 2, + nil, + }, + { + "low", + 3, + nil, + }, + { + "close", + 4, + nil, + }, + { + "vol", + 5, + nil, + }, + { + "invalid", + 0, + errInvalidSelector, + }, + } + + for _, tests := range testCases { + test := tests + t.Run(test.name, func(t *testing.T) { + v, err := ParseIndicatorSelector(test.name) + if err != nil { + if err != test.err { + t.Fatal(err) + } + } + if v != test.expected { + t.Fatalf("expected %v received %v", test.expected, v) + } + }) + } +} + +func TestParseMAType(t *testing.T) { + testCases := []struct { + name string + expected indicators.MaType + err error + }{ + { + "sma", + indicators.Sma, + nil, + }, + { + "ema", + indicators.Ema, + nil, + }, + { + "no", + indicators.Sma, + errInvalidSelector, + }, + } + + for _, tests := range testCases { + test := tests + t.Run(test.name, func(t *testing.T) { + v, err := ParseMAType(test.name) + if err != nil { + if err != test.err { + t.Fatal(err) + } + } + if v != test.expected { + t.Fatalf("expected %v received %v", test.expected, v) + } + }) + } +} diff --git a/gctscript/modules/ta/indicators/macd.go b/gctscript/modules/ta/indicators/macd.go new file mode 100644 index 00000000..6eafda85 --- /dev/null +++ b/gctscript/modules/ta/indicators/macd.go @@ -0,0 +1,80 @@ +package indicators + +import ( + "errors" + "fmt" + "math" + "strings" + + objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules" + "github.com/thrasher-corp/gocryptotrader/gctscript/wrappers/validator" +) + +// MACDModule MACD indicator commands +var MACDModule = map[string]objects.Object{ + "calculate": &objects.UserFunction{Name: "calculate", Value: macd}, +} + +func macd(args ...objects.Object) (objects.Object, error) { + if len(args) != 4 { + return nil, objects.ErrWrongNumArguments + } + + r := &objects.Array{} + if validator.IsTestExecution.Load() == true { + return r, nil + } + + ohlcvInput := objects.ToInterface(args[0]) + ohlcvInputData, valid := ohlcvInput.([]interface{}) + if !valid { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, OHLCV) + } + + var ohlcvClose []float64 + var allErrors []string + for x := range ohlcvInputData { + t := ohlcvInputData[x].([]interface{}) + value, err := toFloat64(t[4]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvClose = append(ohlcvClose, value) + } + + inFastPeriod, ok := objects.ToInt(args[1]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inFastPeriod)) + } + + inSlowPeriod, ok := objects.ToInt(args[2]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inSlowPeriod)) + } + + inTimePeriod, ok := objects.ToInt(args[3]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inTimePeriod)) + } + + if len(allErrors) > 0 { + return nil, errors.New(strings.Join(allErrors, ", ")) + } + + macd, macdSignal, macdHist := indicators.MACD(ohlcvClose, inFastPeriod, inSlowPeriod, inTimePeriod) + for x := range macdHist { + tempMACD := &objects.Array{} + tempMACD.Value = append(tempMACD.Value, &objects.Float{Value: math.Round(macdHist[x]*100) / 100}) + if macd != nil { + tempMACD.Value = append(tempMACD.Value, &objects.Float{Value: math.Round(macd[x]*100) / 100}) + } + if macdSignal != nil { + tempMACD.Value = append(tempMACD.Value, &objects.Float{Value: math.Round(macdSignal[x]*100) / 100}) + } + r.Value = append(r.Value, tempMACD) + } + + return r, nil +} diff --git a/gctscript/modules/ta/indicators/mfi.go b/gctscript/modules/ta/indicators/mfi.go new file mode 100644 index 00000000..1203d5d8 --- /dev/null +++ b/gctscript/modules/ta/indicators/mfi.go @@ -0,0 +1,78 @@ +package indicators + +import ( + "errors" + "fmt" + "math" + "strings" + + objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules" + "github.com/thrasher-corp/gocryptotrader/gctscript/wrappers/validator" +) + +// MfiModule index indicator commands +var MfiModule = map[string]objects.Object{ + "calculate": &objects.UserFunction{Name: "calculate", Value: mfi}, +} + +func mfi(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + r := &objects.Array{} + if validator.IsTestExecution.Load() == true { + return r, nil + } + + ohlcvInput := objects.ToInterface(args[0]) + ohlcvInputData, valid := ohlcvInput.([]interface{}) + if !valid { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, OHLCV) + } + ohlcvData := make([][]float64, 6) + var allErrors []string + + for x := range ohlcvInputData { + t := ohlcvInputData[x].([]interface{}) + value, err := toFloat64(t[2]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[2] = append(ohlcvData[2], value) + + value, err = toFloat64(t[3]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[3] = append(ohlcvData[3], value) + + value, err = toFloat64(t[4]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[4] = append(ohlcvData[4], value) + + value, err = toFloat64(t[5]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[5] = append(ohlcvData[5], value) + } + if len(allErrors) > 0 { + return nil, errors.New(strings.Join(allErrors, ", ")) + } + inTimePeriod, ok := objects.ToInt(args[1]) + if !ok { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, inTimePeriod) + } + + ret := indicators.MFI(ohlcvData[2], ohlcvData[3], ohlcvData[4], ohlcvData[5], inTimePeriod) + for x := range ret { + r.Value = append(r.Value, &objects.Float{Value: math.Round(ret[x]*100) / 100}) + } + + return r, nil +} diff --git a/gctscript/modules/ta/indicators/obv.go b/gctscript/modules/ta/indicators/obv.go new file mode 100644 index 00000000..bd54babe --- /dev/null +++ b/gctscript/modules/ta/indicators/obv.go @@ -0,0 +1,75 @@ +package indicators + +import ( + "errors" + "fmt" + "math" + "strings" + + objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules" + "github.com/thrasher-corp/gocryptotrader/gctscript/wrappers/validator" +) + +// ObvModule volume indicator commands +var ObvModule = map[string]objects.Object{ + "calculate": &objects.UserFunction{Name: "calculate", Value: obv}, +} + +func obv(args ...objects.Object) (objects.Object, error) { + if len(args) != 1 { + return nil, objects.ErrWrongNumArguments + } + + r := &objects.Array{} + if validator.IsTestExecution.Load() == true { + return r, nil + } + + ohlcvInput := objects.ToInterface(args[0]) + ohlcvInputData, valid := ohlcvInput.([]interface{}) + if !valid { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, OHLCV) + } + + ohlcvData := make([][]float64, 6) + var allErrors []string + for x := range ohlcvInputData { + t := ohlcvInputData[x].([]interface{}) + value, err := toFloat64(t[2]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[2] = append(ohlcvData[2], value) + + value, err = toFloat64(t[3]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[3] = append(ohlcvData[3], value) + + value, err = toFloat64(t[4]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[4] = append(ohlcvData[4], value) + + value, err = toFloat64(t[5]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvData[5] = append(ohlcvData[5], value) + } + + if len(allErrors) > 0 { + return nil, errors.New(strings.Join(allErrors, ", ")) + } + + ret := indicators.OBV(ohlcvData[4], ohlcvData[5]) + for x := range ret { + temp := &objects.Float{Value: math.Round(ret[x]*100) / 100} + r.Value = append(r.Value, temp) + } + return r, nil +} diff --git a/gctscript/modules/ta/indicators/rsi.go b/gctscript/modules/ta/indicators/rsi.go new file mode 100644 index 00000000..06554d41 --- /dev/null +++ b/gctscript/modules/ta/indicators/rsi.go @@ -0,0 +1,63 @@ +package indicators + +import ( + "errors" + "fmt" + "math" + "strings" + + objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules" + "github.com/thrasher-corp/gocryptotrader/gctscript/wrappers/validator" +) + +// RsiModule relative strength index indicator commands +var RsiModule = map[string]objects.Object{ + "calculate": &objects.UserFunction{Name: "calculate", Value: rsi}, +} + +func rsi(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + r := &objects.Array{} + if validator.IsTestExecution.Load() == true { + return r, nil + } + + ohlcvInput := objects.ToInterface(args[0]) + ohlcvInputData, valid := ohlcvInput.([]interface{}) + if !valid { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, OHLCV) + } + + var ohlcvClose []float64 + var allErrors []string + for x := range ohlcvInputData { + t := ohlcvInputData[x].([]interface{}) + + value, err := toFloat64(t[4]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvClose = append(ohlcvClose, value) + } + + inTimePeriod, ok := objects.ToInt(args[1]) + if !ok { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, inTimePeriod) + } + + if len(allErrors) > 0 { + return nil, errors.New(strings.Join(allErrors, ", ")) + } + + ret := indicators.RSI(ohlcvClose, inTimePeriod) + for x := range ret { + r.Value = append(r.Value, &objects.Float{Value: math.Round(ret[x]*100) / 100}) + } + + return r, nil +} diff --git a/gctscript/modules/ta/indicators/sma.go b/gctscript/modules/ta/indicators/sma.go new file mode 100644 index 00000000..471fc6d0 --- /dev/null +++ b/gctscript/modules/ta/indicators/sma.go @@ -0,0 +1,61 @@ +package indicators + +import ( + "errors" + "fmt" + "math" + "strings" + + objects "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gct-ta/indicators" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules" + "github.com/thrasher-corp/gocryptotrader/gctscript/wrappers/validator" +) + +// SMAModule simple moving average indicator commands +var SMAModule = map[string]objects.Object{ + "calculate": &objects.UserFunction{Name: "calculate", Value: sma}, +} + +func sma(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + r := &objects.Array{} + if validator.IsTestExecution.Load() == true { + return r, nil + } + + ohlcvInput := objects.ToInterface(args[0]) + ohlcvInputData, valid := ohlcvInput.([]interface{}) + if !valid { + return nil, fmt.Errorf(modules.ErrParameterConvertFailed, OHLCV) + } + + var ohlcvClose []float64 + var allErrors []string + for x := range ohlcvInputData { + t := ohlcvInputData[x].([]interface{}) + value, err := toFloat64(t[4]) + if err != nil { + allErrors = append(allErrors, err.Error()) + } + ohlcvClose = append(ohlcvClose, value) + } + + inTimePeriod, ok := objects.ToInt(args[1]) + if !ok { + allErrors = append(allErrors, fmt.Sprintf(modules.ErrParameterConvertFailed, inTimePeriod)) + } + + if len(allErrors) > 0 { + return nil, errors.New(strings.Join(allErrors, ", ")) + } + ret := indicators.SMA(ohlcvClose, inTimePeriod) + for x := range ret { + r.Value = append(r.Value, &objects.Float{Value: math.Round(ret[x]*100) / 100}) + } + + return r, nil +} diff --git a/gctscript/modules/ta/ta.go b/gctscript/modules/ta/ta.go new file mode 100644 index 00000000..1a1b6f85 --- /dev/null +++ b/gctscript/modules/ta/ta.go @@ -0,0 +1,10 @@ +package ta + +// AllModuleNames returns a list of all default module names. +func AllModuleNames() []string { + var names []string + for name := range Modules { + names = append(names, name) + } + return names +} diff --git a/gctscript/modules/ta/ta_test.go b/gctscript/modules/ta/ta_test.go new file mode 100644 index 00000000..7ec1e4bc --- /dev/null +++ b/gctscript/modules/ta/ta_test.go @@ -0,0 +1,17 @@ +package ta + +import ( + "reflect" + "testing" +) + +func TestGetModuleMap(t *testing.T) { + x := AllModuleNames() + xType := reflect.TypeOf(x).Kind() + if xType != reflect.Slice { + t.Fatalf("AllModuleNames() should return slice instead received: %v", x) + } + if len(x) != 8 { + t.Fatalf("unexpected results received expected 7 received: %v", len(x)) + } +} diff --git a/gctscript/modules/ta/ta_types.go b/gctscript/modules/ta/ta_types.go new file mode 100644 index 00000000..9c7139ec --- /dev/null +++ b/gctscript/modules/ta/ta_types.go @@ -0,0 +1,18 @@ +package ta + +import ( + "github.com/d5/tengo/v2" + "github.com/thrasher-corp/gocryptotrader/gctscript/modules/ta/indicators" +) + +// Modules map of all loadable modules +var Modules = map[string]map[string]tengo.Object{ + "indicator/bbands": indicators.BBandsModule, + "indicator/macd": indicators.MACDModule, + "indicator/ema": indicators.EMAModule, + "indicator/sma": indicators.SMAModule, + "indicator/rsi": indicators.RsiModule, + "indicator/obv": indicators.ObvModule, + "indicator/mfi": indicators.MfiModule, + "indicator/atr": indicators.AtrModule, +} diff --git a/gctscript/modules/wrapper_types.go b/gctscript/modules/wrapper_types.go index 917d9559..760548dc 100644 --- a/gctscript/modules/wrapper_types.go +++ b/gctscript/modules/wrapper_types.go @@ -1,15 +1,24 @@ package modules import ( + "time" + "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) +const ( + // ErrParameterConvertFailed error to return when type conversion fails + ErrParameterConvertFailed = "%v failed conversion" + ErrParameterWithPositionConvertFailed = "%v at position %v failed conversion" +) + // Wrapper instance of GCT to use for modules var Wrapper GCT @@ -32,6 +41,7 @@ type Exchange interface { DepositAddress(exch string, currencyCode currency.Code) (string, error) WithdrawalFiatFunds(exch, bankAccountID string, request *withdraw.Request) (out string, err error) WithdrawalCryptoFunds(exch string, request *withdraw.Request) (out string, err error) + OHLCV(exch string, pair currency.Pair, item asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) } // SetModuleWrapper link the wrapper and interface to use for modules diff --git a/gctscript/wrappers/gct/exchange/exchange.go b/gctscript/wrappers/gct/exchange/exchange.go index 3827f053..a3616348 100644 --- a/gctscript/wrappers/gct/exchange/exchange.go +++ b/gctscript/wrappers/gct/exchange/exchange.go @@ -3,13 +3,16 @@ package exchange import ( "errors" "fmt" + "sort" "strconv" + "time" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" @@ -207,3 +210,21 @@ func (e Exchange) WithdrawalCryptoFunds(exch string, request *withdraw.Request) } return resp.Exchange.ID, nil } + +// OHLCV returns open high low close volume candles for requested exchange/pair/asset/start & end time +func (e Exchange) OHLCV(exch string, pair currency.Pair, item asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) { + ex, err := e.GetExchange(exch) + if err != nil { + return kline.Item{}, err + } + ret, err := ex.GetHistoricCandles(pair, item, start, end, interval) + if err != nil { + return kline.Item{}, err + } + + sort.Slice(ret.Candles, func(i, j int) bool { + return ret.Candles[i].Time.Before(ret.Candles[j].Time) + }) + + return ret, nil +} diff --git a/gctscript/wrappers/validator/validator.go b/gctscript/wrappers/validator/validator.go index 93645372..fc941049 100644 --- a/gctscript/wrappers/validator/validator.go +++ b/gctscript/wrappers/validator/validator.go @@ -1,17 +1,27 @@ package validator import ( + "math/rand" "time" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) +const ( + validatorOpen float64 = 5000 + validatorHigh float64 = 6000 + validatorLow float64 = 5500 + validatorClose float64 = 5700 + validatorVol float64 = 10 +) + // Exchanges validator for test execution/scripts func (w Wrapper) Exchanges(enabledOnly bool) []string { if enabledOnly { @@ -212,3 +222,41 @@ func (w Wrapper) WithdrawalFiatFunds(exch, _ string, _ *withdraw.Request) (out s return "123", nil } + +// OHLCV returns open high low close volume candles for requested exchange/pair/asset/start & end time +func (w Wrapper) OHLCV(exch string, p currency.Pair, a asset.Item, start, end time.Time, i time.Duration) (kline.Item, error) { + if exch == exchError.String() { + return kline.Item{}, errTestFailed + } + var candles []kline.Candle + + candles = append(candles, kline.Candle{ + Time: start, + Open: validatorOpen, + High: validatorHigh, + Low: validatorLow, + Close: validatorClose, + Volume: validatorVol, + }) + + for x := 1; x < 200; x++ { + r := validatorLow + rand.Float64()*(validatorHigh-validatorLow) + candle := kline.Candle{ + Time: candles[x-1].Time.Add(-i), + Open: r, + High: r, + Low: r, + Close: r, + Volume: r, + } + candles = append(candles, candle) + } + + return kline.Item{ + Exchange: exch, + Pair: p, + Asset: a, + Interval: i, + Candles: candles, + }, nil +} diff --git a/gctscript/wrappers/validator/validator_test.go b/gctscript/wrappers/validator/validator_test.go index 18e31899..679f22dc 100644 --- a/gctscript/wrappers/validator/validator_test.go +++ b/gctscript/wrappers/validator/validator_test.go @@ -2,9 +2,11 @@ package validator import ( "testing" + "time" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) @@ -204,3 +206,15 @@ func TestWrapper_WithdrawalFiatFunds(t *testing.T) { t.Fatal("expected WithdrawalCryptoFunds to return error with invalid name") } } + +func TestWrapper_OHLCV(t *testing.T) { + c := currency.NewPairDelimiter(pairs, delimiter) + _, err := testWrapper.OHLCV("test", c, asset.Spot, time.Now().Add(-24*time.Hour), time.Now(), kline.OneDay) + if err != nil { + t.Fatal(err) + } + _, err = testWrapper.OHLCV(exchError.String(), c, asset.Spot, time.Now().Add(-24*time.Hour), time.Now(), kline.OneDay) + if err == nil { + t.Fatal("expected OHLCV to return error with invalid name") + } +} diff --git a/go.mod b/go.mod index 9576deba..faeaa303 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,8 @@ require ( github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.2.0 github.com/spf13/viper v1.6.3 + github.com/stretchr/testify v1.5.1 // indirect + github.com/thrasher-corp/gct-ta v0.0.0-20200423101437-dc6b098dc762 github.com/thrasher-corp/goose v2.7.0-rc4.0.20191002032028-0f2c2a27abdb+incompatible github.com/thrasher-corp/sqlboiler v1.0.1-0.20191001234224-71e17f37a85e github.com/toorop/go-pusher v0.0.0-20180521062818-4521e2eb39fb diff --git a/go.sum b/go.sum index 6326e6fb..d769b9c4 100644 --- a/go.sum +++ b/go.sum @@ -213,8 +213,12 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/thrasher-corp/gct-ta v0.0.0-20200423101437-dc6b098dc762 h1:y+GonaRLfYBos8e//YiPi9aJQ/InsERnf3dmEZftKQE= +github.com/thrasher-corp/gct-ta v0.0.0-20200423101437-dc6b098dc762/go.mod h1:z51vdK6i7okTmwu9tPh9+W8nqPWv80B/nMZUCX17fwY= github.com/thrasher-corp/goose v2.7.0-rc4.0.20191002032028-0f2c2a27abdb+incompatible h1:SPqQlzFu3g4P9wK2iwJaWVLJWcQ5rYc43rvXBJ8RSCY= github.com/thrasher-corp/goose v2.7.0-rc4.0.20191002032028-0f2c2a27abdb+incompatible/go.mod h1:2Bb/y0SpnUWOlPU5kDz+ctvb3w/mzuAVqxy7JPfBzgw= github.com/thrasher-corp/sqlboiler v1.0.1-0.20191001234224-71e17f37a85e h1:4kYBo2YhqqFY7aZPPEhrtPTMoAq4iCsoDITd3jseRbY=