technical_analysis: TWAP & VWAP + TA methods to candles and link to existing RPC server for GCTCLI prototyping (#970)

* kline: add weighted price helpers for candles

* twap/vwap: basic implementation and hook to rpc for protype

* ta: cont implementation. (WIP)

* kline: Add tests

* kline: add helpers

* ta: full impl.

* kline: remove support for macd and add in correlation-coefficient handling

* rpc: change naming convention

* linter: fix

* protolinter: fix

* linter: ++

* kline: reinstate macd handling after adding in check

* glorious: nits

* gctcl: linter

* Update exchanges/kline/weighted_price.go

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

* glorious: nits

* glorious: nits v2.0

* kline: fix test

* huobi-tests: shift from next quarter to this weeks contracts as they were erroring out in tests.

* btcmarkets: update supported kline intervals

* zb: fix test

* rpcserver: fix bug and tests

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
This commit is contained in:
Ryan O'Hara-Reid
2022-07-08 15:21:56 +10:00
committed by GitHub
parent 68db4155bf
commit 7da745120f
21 changed files with 3509 additions and 325 deletions

View File

@@ -114,9 +114,15 @@ func (b *BTCMarkets) SetDefaults() {
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.OneHour.Word(): true,
kline.OneDay.Word(): true,
kline.OneMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.SixHour.Word(): true,
kline.OneDay.Word(): true,
kline.OneWeek.Word(): true,
kline.OneMonth.Word(): true,
},
ResultLimit: 1000,
},
@@ -978,8 +984,25 @@ func (b *BTCMarkets) ValidateCredentials(ctx context.Context, assetType asset.It
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (b *BTCMarkets) FormatExchangeKlineInterval(in kline.Interval) string {
if in == kline.OneDay {
switch in {
case kline.OneMin:
return "1m"
case kline.FiveMin:
return "5m"
case kline.FifteenMin:
return "15m"
case kline.ThirtyMin:
return "30m"
case kline.OneHour:
return "1h"
case kline.SixHour:
return "6h"
case kline.OneDay:
return "1d"
case kline.OneWeek:
return "1w"
case kline.OneMonth:
return "1mo"
}
return in.Short()
}

View File

@@ -988,26 +988,22 @@ func (b *Base) FormatExchangeKlineInterval(in kline.Interval) string {
return strconv.FormatFloat(in.Duration().Seconds(), 'f', 0, 64)
}
// ValidateKline confirms that the requested pair, asset & interval are supported and/or enabled by the requested exchange
// ValidateKline confirms that the requested pair, asset & interval are
// supported and/or enabled by the requested exchange.
func (b *Base) ValidateKline(pair currency.Pair, a asset.Item, interval kline.Interval) error {
var errorList []string
var err kline.Error
if b.CurrencyPairs.IsAssetEnabled(a) != nil {
err.Asset = a
errorList = append(errorList, "asset not enabled")
errorList = append(errorList, fmt.Sprintf("[%s] asset not enabled", a))
} else if !b.CurrencyPairs.Pairs[a].Enabled.Contains(pair, true) {
err.Pair = pair
errorList = append(errorList, "pair not enabled")
errorList = append(errorList, fmt.Sprintf("[%s] pair not enabled", pair))
}
if !b.klineIntervalEnabled(interval) {
err.Interval = interval
errorList = append(errorList, "interval not supported")
errorList = append(errorList, fmt.Sprintf("[%s] interval not supported", interval))
}
if len(errorList) > 0 {
err.Err = errors.New(strings.Join(errorList, ","))
return &err
return fmt.Errorf("%w: %v", kline.ErrValidatingParams, strings.Join(errorList, ", "))
}
return nil

View File

@@ -37,7 +37,7 @@ const (
var (
h HUOBI
wsSetupRan bool
futuresTestPair = currency.NewPair(currency.BTC, currency.NewCode("NQ"))
futuresTestPair = currency.NewPair(currency.BTC, currency.NewCode("CW")) // represents this week - NQ (next quarter) is erroring out.
)
func TestMain(m *testing.M) {
@@ -138,7 +138,7 @@ func TestFIndexPriceInfo(t *testing.T) {
func TestFContractPriceLimitations(t *testing.T) {
t.Parallel()
_, err := h.FContractPriceLimitations(context.Background(),
"BTC", "next_quarter", currency.EMPTYPAIR)
"BTC", "this_week", currency.EMPTYPAIR)
if err != nil {
t.Error(err)
}
@@ -147,7 +147,7 @@ func TestFContractPriceLimitations(t *testing.T) {
func TestFContractOpenInterest(t *testing.T) {
t.Parallel()
_, err := h.FContractOpenInterest(context.Background(),
"BTC", "next_quarter", currency.EMPTYPAIR)
"BTC", "this_week", currency.EMPTYPAIR)
if err != nil {
t.Error(err)
}
@@ -231,7 +231,7 @@ func TestFQueryTieredAdjustmentFactor(t *testing.T) {
func TestFQueryHisOpenInterest(t *testing.T) {
t.Parallel()
_, err := h.FQueryHisOpenInterest(context.Background(),
"BTC", "next_quarter", "60min", "cont", 3)
"BTC", "this_week", "60min", "cont", 3)
if err != nil {
t.Error(err)
}
@@ -2732,8 +2732,8 @@ func TestFormatFuturesPair(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if r != "BTC_NQ" {
t.Errorf("expected BTC_NQ, got %s", r)
if r != "BTC_CW" {
t.Errorf("expected BTC_CW, got %s", r)
}
availInstruments, err := h.FetchTradablePairs(context.Background(), asset.Futures)
if err != nil {

View File

@@ -222,7 +222,7 @@ func (k *Item) SortCandlesByTimestamp(desc bool) {
})
}
// FormatDates converts all date to UTC time
// FormatDates converts all dates to UTC time
func (k *Item) FormatDates() {
for x := range k.Candles {
k.Candles[x].Time = k.Candles[x].Time.UTC()
@@ -324,13 +324,15 @@ func TotalCandlesPerInterval(start, end time.Time, interval Interval) (out float
return -1
}
var oneYearDurationInNano = float64(OneYear.Duration().Nanoseconds())
// IntervalsPerYear helps determine the number of intervals in a year
// used in CAGR calculation to know the amount of time of an interval in a year
func (i *Interval) IntervalsPerYear() float64 {
if i.Duration() == 0 {
return 0
}
return float64(OneYear.Duration().Nanoseconds()) / float64(i.Duration().Nanoseconds())
return oneYearDurationInNano / float64(i.Duration().Nanoseconds())
}
// ConvertToNewInterval allows the scaling of candles to larger candles
@@ -557,10 +559,7 @@ func (h *IntervalRangeHolder) createDateSummaryRange(start, end time.Time, hasDa
// CreateIntervalTime is a simple helper function to set the time twice
func CreateIntervalTime(tt time.Time) IntervalTime {
return IntervalTime{
Time: tt,
Ticks: tt.Unix(),
}
return IntervalTime{Time: tt, Ticks: tt.Unix()}
}
// Equal allows for easier unix comparison

View File

@@ -262,31 +262,6 @@ func TestDurationToWord(t *testing.T) {
}
}
func TestKlineErrors(t *testing.T) {
t.Parallel()
v := Error{
Interval: OneYear,
Pair: currency.NewPair(currency.BTC, currency.AUD),
Err: errors.New("hello world"),
}
if v.Interval != OneYear {
t.Fatalf("expected OneYear received %v:", v.Interval)
}
if v.Pair != currency.NewPair(currency.BTC, currency.AUD) {
t.Fatalf("expected OneYear received %v:", v.Pair)
}
if v.Error() != "hello world" {
t.Fatal("expected error return received empty value")
}
if v.Unwrap().Error() != "hello world" {
t.Fatal("expected error return received empty value")
}
}
func TestTotalCandlesPerInterval(t *testing.T) {
t.Parallel()
start := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)

View File

@@ -52,6 +52,10 @@ var (
// ErrNotFoundAtTime returned when looking up a candle at a specific time
ErrNotFoundAtTime = errors.New("candle not found at time")
// ErrValidatingParams defines an error when the kline params are either not
// enabled or are invalid.
ErrValidatingParams = errors.New("kline param(s) are invalid")
// SupportedIntervals is a list of all supported intervals
SupportedIntervals = []Interval{
FifteenSecond,
@@ -131,24 +135,6 @@ type ExchangeCapabilitiesEnabled struct {
// Interval type for kline Interval usage
type Interval time.Duration
// Error struct to hold kline interval errors
type Error struct {
Asset asset.Item
Pair currency.Pair
Interval Interval
Err error
}
// Error returns short interval unsupported message
func (e *Error) Error() string {
return e.Err.Error()
}
// Unwrap returns interval unsupported message
func (e *Error) Unwrap() error {
return e.Err
}
// IntervalRangeHolder holds the entire range of intervals
// and the start end dates of everything
type IntervalRangeHolder struct {

View File

@@ -0,0 +1,354 @@
package kline
import (
"errors"
"fmt"
"github.com/thrasher-corp/gct-ta/indicators"
)
var (
errInvalidPeriod = errors.New("invalid period")
errNoData = errors.New("no data")
errInvalidDeviationMultiplier = errors.New("invalid deviation multiplier")
errNilOHLC = errors.New("nil OHLC data")
errInvalidDataSetLengths = errors.New("invalid data set lengths")
errNotEnoughData = errors.New("not enough data to derive signal")
)
// OHLC is a connector for technical analysis usage
type OHLC struct {
Open []float64
High []float64
Low []float64
Close []float64
Volume []float64
}
// GetOHLC returns the entire subset of candles as a friendly type for gct
// technical analysis usage.
func (k *Item) GetOHLC() *OHLC {
ohlc := &OHLC{
Open: make([]float64, len(k.Candles)),
High: make([]float64, len(k.Candles)),
Low: make([]float64, len(k.Candles)),
Close: make([]float64, len(k.Candles)),
Volume: make([]float64, len(k.Candles)),
}
for x := range k.Candles {
ohlc.Open[x] = k.Candles[x].Open
ohlc.High[x] = k.Candles[x].High
ohlc.Low[x] = k.Candles[x].Low
ohlc.Close[x] = k.Candles[x].Close
ohlc.Volume[x] = k.Candles[x].Volume
}
return ohlc
}
// GetAverageTrueRange returns the Average True Range for the given period.
func (k *Item) GetAverageTrueRange(period int64) ([]float64, error) {
return k.GetOHLC().GetAverageTrueRange(period)
}
// GetAverageTrueRange returns the Average True Range for the given period.
func (o *OHLC) GetAverageTrueRange(period int64) ([]float64, error) {
if o == nil {
return nil, fmt.Errorf("get average true range %w", errNilOHLC)
}
if period <= 0 {
return nil, fmt.Errorf("get average true range %w", errInvalidPeriod)
}
if len(o.High) == 0 {
return nil, fmt.Errorf("get average true range high %w", errNoData)
}
if len(o.Low) == 0 {
return nil, fmt.Errorf("get average true range low %w", errNoData)
}
if len(o.Close) == 0 {
return nil, fmt.Errorf("get average true range close %w", errNoData)
}
if int(period) > len(o.Close) {
return nil, fmt.Errorf("get average true range close %w exceeds data length, please reduce",
errInvalidPeriod)
}
return indicators.ATR(o.High, o.Low, o.Close, int(period)), nil
}
// GetBollingerBands returns Bollinger Bands for the given period.
func (k *Item) GetBollingerBands(period int64, nbDevUp, nbDevDown float64, m indicators.MaType) (*Bollinger, error) {
return k.GetOHLC().GetBollingerBands(period, nbDevUp, nbDevDown, m)
}
// Bollinger defines a return type for the bollinger bands
type Bollinger struct {
Upper []float64
Middle []float64
Lower []float64
}
// GetBollingerBands returns Bollinger Bands for the given period.
func (o *OHLC) GetBollingerBands(period int64, nbDevUp, nbDevDown float64, m indicators.MaType) (*Bollinger, error) {
if o == nil {
return nil, fmt.Errorf("get bollinger bands %w", errNilOHLC)
}
if period <= 0 {
return nil, fmt.Errorf("get bollinger bands %w", errInvalidPeriod)
}
if nbDevUp <= 0 {
return nil, fmt.Errorf("get bollinger bands %w upper limit", errInvalidDeviationMultiplier)
}
if nbDevDown <= 0 {
return nil, fmt.Errorf("get bollinger bands %w lower limit", errInvalidDeviationMultiplier)
}
if len(o.Close) == 0 {
return nil, fmt.Errorf("get bollinger bands close %w", errNoData)
}
if int(period) > len(o.Close) { // TODO: Investigate the panic when this protection is removed.
return nil, fmt.Errorf("get bollinger bands %w '%v' should not exceed close data length '%v'",
errInvalidPeriod, period, len(o.Close))
}
var bands Bollinger
bands.Upper, bands.Middle, bands.Lower = indicators.BBANDS(o.Close,
int(period),
nbDevUp,
nbDevDown,
m)
return &bands, nil
}
// GetCorrelationCoefficient returns GetCorrelation Coefficient against another
// candle data set for the given period.
func (k *Item) GetCorrelationCoefficient(other *Item, period int64) ([]float64, error) {
return k.GetOHLC().GetCorrelationCoefficient(other.GetOHLC(), period)
}
// GetCorrelationCoefficient returns GetCorrelation Coefficient against another
// candle data set for the given period.
func (o *OHLC) GetCorrelationCoefficient(other *OHLC, period int64) ([]float64, error) {
if o == nil {
return nil, fmt.Errorf("get correlation coefficient %w", errNilOHLC)
}
if period <= 0 {
return nil, fmt.Errorf("get correlation coefficient %w", errInvalidPeriod)
}
if period == 1 {
// TODO: Check correlation calculation.
return nil, fmt.Errorf("get correlation coefficient %w using period 1 results in NaN return",
errInvalidPeriod)
}
if other == nil {
return nil, fmt.Errorf("get correlation coefficient %w", errNilOHLC)
}
if len(o.Close) == 0 {
return nil, fmt.Errorf("get correlation coefficient close %w", errNoData)
}
if len(other.Close) == 0 {
return nil, fmt.Errorf("get correlation coefficient comparison close %w", errNoData)
}
if int(period) > len(o.Close) || int(period) > len(other.Close) {
return nil, fmt.Errorf("get correlation coefficient %w exceeds data length, please reduce",
errInvalidPeriod)
}
if len(o.Close) != len(other.Close) {
return nil,
fmt.Errorf("get correlation coefficient comparison close %w between data sets",
errInvalidDataSetLengths)
}
return indicators.CorrelationCoefficient(o.Close, other.Close, int(period)), nil
}
// GetSimpleMovingAverageOnClose returns MA the close prices set for the given
// period.
func (k *Item) GetSimpleMovingAverageOnClose(period int64) ([]float64, error) {
ohlc := k.GetOHLC()
return ohlc.GetSimpleMovingAverage(ohlc.Close, period)
}
// GetSimpleMovingAverage returns MA for the supplied price set for the given
// period.
func (o *OHLC) GetSimpleMovingAverage(option []float64, period int64) ([]float64, error) {
if o == nil {
return nil, fmt.Errorf("get simple moving average %w", errNilOHLC)
}
if period <= 0 {
return nil, fmt.Errorf("get simple moving average %w", errInvalidPeriod)
}
if len(option) == 0 {
return nil, fmt.Errorf("get simple moving average %w", errNoData)
}
if int(period) > len(option) {
return nil, fmt.Errorf("get simple moving average %w exceeds data length, please reduce",
errInvalidPeriod)
}
return indicators.SMA(option, int(period)), nil
}
// GetExponentialMovingAverageOnClose returns the EMA on the close price set for
// the given period.
func (k *Item) GetExponentialMovingAverageOnClose(period int64) ([]float64, error) {
ohlc := k.GetOHLC()
return ohlc.GetExponentialMovingAverage(ohlc.Close, period)
}
// GetExponentialMovingAverage returns the EMA on the supplied price set for the
// given period.
func (o *OHLC) GetExponentialMovingAverage(option []float64, period int64) ([]float64, error) {
if o == nil {
return nil, fmt.Errorf("get exponential moving average %w", errNilOHLC)
}
if period <= 0 {
return nil, fmt.Errorf("get exponential moving average %w", errInvalidPeriod)
}
if len(option) == 0 {
return nil, fmt.Errorf("get exponential moving average %w", errNoData)
}
if int(period) > len(option) {
return nil, fmt.Errorf("get exponential moving average %w exceeds data length, please reduce",
errInvalidPeriod)
}
return indicators.EMA(option, int(period)), nil
}
// MACD defines MACD values
type MACD struct {
Results []float64
SignalVals []float64
Histogram []float64
}
// GetMovingAverageConvergenceDivergenceOnClose returns the
// MACD (macd, signal period vals, histogram) for the given price
// set and the parameters fast, slow signal time periods.
func (k *Item) GetMovingAverageConvergenceDivergenceOnClose(fast, slow, signal int64) (*MACD, error) {
ohlc := k.GetOHLC()
return ohlc.GetMovingAverageConvergenceDivergence(ohlc.Close, fast, slow, signal)
}
// GetMovingAverageConvergenceDivergence returns the
// MACD (macd, signal period vals, histogram) for the given price
// set and the parameters fast, slow signal time periods.
func (o *OHLC) GetMovingAverageConvergenceDivergence(option []float64, fast, slow, signal int64) (*MACD, error) {
if o == nil {
return nil, fmt.Errorf("get macd %w", errNilOHLC)
}
if fast <= 0 {
return nil, fmt.Errorf("get macd %w fast", errInvalidPeriod)
}
if slow <= 0 {
return nil, fmt.Errorf("get macd %w slow", errInvalidPeriod)
}
if fast >= slow {
return nil, fmt.Errorf("get macd %w fast should not be equal or exceed slow", errInvalidPeriod)
}
if signal <= 0 {
return nil, fmt.Errorf("get macd %w signal", errInvalidPeriod)
}
if len(option) == 0 {
return nil, fmt.Errorf("get macd %w", errNoData)
}
if len(option) < int(slow+signal-2) {
return nil, fmt.Errorf("get macd %w %v data points are less than minimum %v length requirement derived from the slow %v and signal %v period subtract two, increase end date or scale down granularity",
errNotEnoughData,
len(option),
slow+signal-2,
slow,
signal)
}
var macd MACD
macd.Results, macd.SignalVals, macd.Histogram = indicators.MACD(option,
int(fast),
int(slow),
int(signal))
return &macd, nil
}
// GetMoneyFlowIndex returns Money Flow Index for the given period.
func (k *Item) GetMoneyFlowIndex(period int64) ([]float64, error) {
return k.GetOHLC().GetMoneyFlowIndex(period)
}
// GetMoneyFlowIndex returns Money Flow Index for the given period.
func (o *OHLC) GetMoneyFlowIndex(period int64) ([]float64, error) {
if o == nil {
return nil, fmt.Errorf("get money flow index %w", errNilOHLC)
}
if period <= 0 {
return nil, fmt.Errorf("get money flow index %w", errInvalidPeriod)
}
highLen := len(o.High)
if highLen == 0 {
return nil, fmt.Errorf("get money flow index %w for high", errNoData)
}
lowLen := len(o.Low)
if lowLen == 0 {
return nil, fmt.Errorf("get money flow index %w for low", errNoData)
}
closeLen := len(o.Close)
if closeLen == 0 {
return nil, fmt.Errorf("get money flow index %w for close", errNoData)
}
volLen := len(o.Volume)
if volLen == 0 {
return nil, fmt.Errorf("get money flow index %w for volume", errNoData)
}
if highLen != closeLen || lowLen != closeLen || volLen != closeLen {
// TODO: Investigate the panic when this protection is removed.
// This is very unstable with incorrect lengths.
return nil, fmt.Errorf("get money flow index %w", errInvalidDataSetLengths)
}
if int(period) >= len(o.Close) {
// TODO: Investigate the panic when this protection is removed.
return nil, fmt.Errorf("get money flow index %w '%v' should not exceed or equal close data length '%v'",
errInvalidPeriod, period, len(o.Close))
}
return indicators.MFI(o.High, o.Low, o.Close, o.Volume, int(period)), nil
}
// GetOnBalanceVolume returns On Balance Volume.
func (k *Item) GetOnBalanceVolume() ([]float64, error) {
return k.GetOHLC().GetOnBalanceVolume()
}
// GetOnBalanceVolume returns On Balance Volume.
func (o *OHLC) GetOnBalanceVolume() ([]float64, error) {
if o == nil {
return nil, fmt.Errorf("get on balance volume %w", errNilOHLC)
}
if len(o.Close) == 0 {
return nil, fmt.Errorf("get on balance volume %w for close", errNoData)
}
if len(o.Volume) == 0 {
return nil, fmt.Errorf("get on balance volume %w for volume", errNoData)
}
return indicators.OBV(o.Close, o.Volume), nil
}
// GetRelativeStrengthIndexOnClose returns the relative strength index from the
// given price set and period.
func (k *Item) GetRelativeStrengthIndexOnClose(period int64) ([]float64, error) {
ohlc := k.GetOHLC()
return ohlc.GetRelativeStrengthIndex(ohlc.Close, period)
}
// GetRelativeStrengthIndex returns the relative strength index from the the
// given price set and period.
func (o *OHLC) GetRelativeStrengthIndex(option []float64, period int64) ([]float64, error) {
if o == nil {
return nil, fmt.Errorf("get relative strength index %w", errNilOHLC)
}
if period <= 1 {
return nil, fmt.Errorf("get relative strength index %w cannot be equal or below 1", errInvalidPeriod)
}
if len(option) <= 2 {
// TODO: Check why 2 data points causes panic.
return nil, fmt.Errorf("get relative strength index %w, requires atleast 3 data points", errNotEnoughData)
}
if int(period) > len(option) {
return nil, fmt.Errorf("get exponential moving average %w exceeds data length, please reduce",
errInvalidPeriod)
}
return indicators.RSI(option, int(period)), nil
}

View File

@@ -0,0 +1,429 @@
package kline
import (
"errors"
"testing"
)
func TestGetOHLC(t *testing.T) {
t.Parallel()
if (&Item{Candles: []Candle{{Open: 1337}}}).GetOHLC() == nil {
t.Fatal("unexpected value")
}
}
func TestGetAverageTrueRange(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetAverageTrueRange(0)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetAverageTrueRange(0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetAverageTrueRange(9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.High = append(ohlc.High, 1337)
_, err = ohlc.GetAverageTrueRange(9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Low = append(ohlc.Low, 1337)
_, err = ohlc.GetAverageTrueRange(9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Close = append(ohlc.Close, 1337)
_, err = ohlc.GetAverageTrueRange(9)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetAverageTrueRange(1)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
wrap := Item{Candles: []Candle{{High: 1337, Low: 1337, Close: 1337}}}
_, err = wrap.GetAverageTrueRange(1)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetBollingerBands(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetBollingerBands(0, 0, 0, 5)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetBollingerBands(0, 0, 0, 5)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetBollingerBands(9, 0, 0, 5)
if !errors.Is(err, errInvalidDeviationMultiplier) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidDeviationMultiplier)
}
_, err = ohlc.GetBollingerBands(9, 1, 0, 5)
if !errors.Is(err, errInvalidDeviationMultiplier) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidDeviationMultiplier)
}
_, err = ohlc.GetBollingerBands(9, 1, 1, 5)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Close = append(ohlc.Close, 1337, 1337, 1337, 1337, 1337, 1337, 1337, 1337, 1337)
_, err = ohlc.GetBollingerBands(10, 1, 1, 5)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetBollingerBands(9, 1, 1, 5)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
wrap := Item{Candles: []Candle{{Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}}}
_, err = wrap.GetBollingerBands(9, 1, 1, 5)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetCorrelationCoefficient(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetCorrelationCoefficient(nil, 0)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetCorrelationCoefficient(nil, 0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetCorrelationCoefficient(nil, 1)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetCorrelationCoefficient(nil, 2)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
_, err = ohlc.GetCorrelationCoefficient(&OHLC{}, 9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Close = append(ohlc.Close, 1337, 1337)
_, err = ohlc.GetCorrelationCoefficient(&OHLC{}, 9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
_, err = ohlc.GetCorrelationCoefficient(&OHLC{Close: []float64{1337}}, 2)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
ohlc.Close = append(ohlc.Close, 1337)
_, err = ohlc.GetCorrelationCoefficient(&OHLC{Close: []float64{1337, 1337}}, 2)
if !errors.Is(err, errInvalidDataSetLengths) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidDataSetLengths)
}
_, err = ohlc.GetCorrelationCoefficient(&OHLC{Close: []float64{1337, 1337, 1337}}, 2)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
wrap := Item{Candles: []Candle{{Close: 1337}, {Close: 1337}, {Close: 1337}}}
_, err = wrap.GetCorrelationCoefficient(&Item{Candles: []Candle{{Close: 1337}, {Close: 1337}, {Close: 1337}}}, 2)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetSimpleMovingAverage(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetSimpleMovingAverage(nil, 0)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetSimpleMovingAverage(nil, 0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetSimpleMovingAverage(nil, 9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
_, err = ohlc.GetSimpleMovingAverage([]float64{1337}, 9)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetSimpleMovingAverage([]float64{1337, 1337}, 2)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
wrap := Item{Candles: []Candle{{Close: 1337}, {Close: 1337}}}
_, err = wrap.GetSimpleMovingAverageOnClose(2)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetExponentialMovingAverage(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetExponentialMovingAverage(nil, 0)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetExponentialMovingAverage(nil, 0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetExponentialMovingAverage(nil, 9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
_, err = ohlc.GetExponentialMovingAverage([]float64{1337}, 9)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetExponentialMovingAverage([]float64{1337, 1337, 1337}, 2)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
wrap := Item{Candles: []Candle{{Close: 1337}, {Close: 1337}, {Close: 1337}}}
_, err = wrap.GetExponentialMovingAverageOnClose(2)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetMovingAverageConvergenceDivergence(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetMovingAverageConvergenceDivergence(nil, 0, 0, 0)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetMovingAverageConvergenceDivergence(nil, 0, 0, 0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetMovingAverageConvergenceDivergence(nil, 1, 0, 0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetMovingAverageConvergenceDivergence(nil, 1, 1, 0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetMovingAverageConvergenceDivergence(nil, 1, 2, 0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetMovingAverageConvergenceDivergence(nil, 1, 2, 1)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
_, err = ohlc.GetMovingAverageConvergenceDivergence([]float64{1337}, 1, 2, 2)
if !errors.Is(err, errNotEnoughData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNotEnoughData)
}
_, err = ohlc.GetMovingAverageConvergenceDivergence([]float64{1337, 1337, 1337, 1337, 1337, 1337, 1337, 1337}, 1, 2, 1)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
wrap := Item{Candles: []Candle{{Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}, {Close: 1337}}}
_, err = wrap.GetMovingAverageConvergenceDivergenceOnClose(1, 2, 1)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetMoneyFlowIndex(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetMoneyFlowIndex(0)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetMoneyFlowIndex(0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetMoneyFlowIndex(9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.High = append(ohlc.High, 1337, 1337, 1337, 1337, 1337, 1337)
_, err = ohlc.GetMoneyFlowIndex(9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Low = append(ohlc.Low, 1337, 1337, 1337, 1337, 1337, 1337)
_, err = ohlc.GetMoneyFlowIndex(9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Close = append(ohlc.Close, 1337, 1337, 1337, 1337, 1337, 1337)
_, err = ohlc.GetMoneyFlowIndex(9)
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Volume = append(ohlc.Volume, 1337, 1337, 1337, 1337, 1337)
_, err = ohlc.GetMoneyFlowIndex(5)
if !errors.Is(err, errInvalidDataSetLengths) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidDataSetLengths)
}
ohlc.Volume = append(ohlc.Volume, 1337)
_, err = ohlc.GetMoneyFlowIndex(6)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetMoneyFlowIndex(3)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
wrap := Item{Candles: []Candle{
{Close: 1337, High: 1337, Low: 1337, Volume: 1337},
{Close: 1337, High: 1337, Low: 1337, Volume: 1337},
{Close: 1337, High: 1337, Low: 1337, Volume: 1337},
}}
_, err = wrap.GetMoneyFlowIndex(2)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetOnBalanceVolume(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetOnBalanceVolume()
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetOnBalanceVolume()
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Close = append(ohlc.Close, 1337, 1337, 1337, 1337, 1337, 1337)
_, err = ohlc.GetOnBalanceVolume()
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Volume = append(ohlc.Volume, 0.00000001)
_, err = ohlc.GetOnBalanceVolume()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
wrap := Item{Candles: []Candle{{Close: 1337, Volume: 1337}}}
_, err = wrap.GetOnBalanceVolume()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetRelativeStrengthIndex(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetRelativeStrengthIndex(nil, 0)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetRelativeStrengthIndex(nil, 0)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
_, err = ohlc.GetRelativeStrengthIndex(nil, 9)
if !errors.Is(err, errNotEnoughData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNotEnoughData)
}
_, err = ohlc.GetRelativeStrengthIndex([]float64{1337, 1337, 1337}, 9)
if !errors.Is(err, errInvalidPeriod) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidPeriod)
}
wrap := Item{Candles: []Candle{{Close: 1337}, {Close: 1337}, {Close: 1337}}}
_, err = wrap.GetRelativeStrengthIndexOnClose(2)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}

View File

@@ -0,0 +1,137 @@
package kline
import (
"errors"
"fmt"
)
var (
errInvalidElement = errors.New("invalid element")
errElementExceedsDataLength = errors.New("element exceeds data length")
errDataLengthMismatch = errors.New("data length mismatch")
)
// GetAveragePrice returns the average price from the open, high, low and close
func (c *Candle) GetAveragePrice() float64 {
return (c.Open + c.High + c.Low + c.Close) / 4
}
// GetAveragePrice returns the average price from the open, high, low and close
func (o *OHLC) GetAveragePrice(element int) (float64, error) {
if o == nil {
return 0, fmt.Errorf("get average price %w", errNilOHLC)
}
if element < 0 {
return 0, fmt.Errorf("get average price %w", errInvalidElement)
}
check := element + 1
if check > len(o.Open) || check > len(o.High) || check > len(o.Low) || check > len(o.Close) {
return 0, fmt.Errorf("get average price %w", errElementExceedsDataLength)
}
return (o.Open[element] + o.High[element] + o.Low[element] + o.Close[element]) / 4, nil
}
// GetTWAP returns the time weighted average price for the specified period.
// NOTE: This assumes the most recent price is at the tail end of the slice.
// Based off: https://blog.quantinsti.com/twap/
// Only returns one item as all other items are just the average price.
func (k *Item) GetTWAP() (float64, error) {
if len(k.Candles) == 0 {
return 0, fmt.Errorf("get twap %w", errNoData)
}
var cumAveragePrice float64
for x := range k.Candles {
cumAveragePrice += k.Candles[x].GetAveragePrice()
}
return cumAveragePrice / float64(len(k.Candles)), nil
}
// GetTWAP returns the time weighted average price for the specified period.
func (o *OHLC) GetTWAP() (float64, error) {
if o == nil {
return 0, fmt.Errorf("get twap %w", errNilOHLC)
}
if len(o.Open) == 0 || len(o.High) == 0 || len(o.Low) == 0 || len(o.Close) == 0 {
return 0, fmt.Errorf("get twap %w", errNoData)
}
if len(o.Open) != len(o.High) || len(o.Open) != len(o.Low) || len(o.Open) != len(o.Close) {
return 0, fmt.Errorf("get twap %w", errDataLengthMismatch)
}
var cumAveragePrice float64
for x := range o.Close {
avgPrice, err := o.GetAveragePrice(x)
if err != nil {
return 0, fmt.Errorf("get twap %w", err)
}
cumAveragePrice += avgPrice
}
return cumAveragePrice / float64(len(o.Close)), nil
}
// GetTypicalPrice returns the typical average price from the high, low and
// close values.
func (c *Candle) GetTypicalPrice() float64 {
return (c.High + c.Low + c.Close) / 3
}
// GetTypicalPrice returns the typical average price from the high, low and
// close values.
func (o *OHLC) GetTypicalPrice(element int) (float64, error) {
if o == nil {
return 0, fmt.Errorf("get typical price %w", errNilOHLC)
}
if element < 0 {
return 0, fmt.Errorf("get typical price %w", errInvalidElement)
}
check := element + 1
if check > len(o.High) || check > len(o.Low) || check > len(o.Close) {
return 0, fmt.Errorf("get typical price %w", errElementExceedsDataLength)
}
return (o.High[element] + o.Low[element] + o.Close[element]) / 3, nil
}
// GetVWAPs returns the Volume Weighted Averages prices which are the cumulative
// average price with respect to the volume.
// NOTE: This assumes candles are sorted by time
// Based off: https://blog.quantinsti.com/vwap-strategy/
func (k *Item) GetVWAPs() ([]float64, error) {
if len(k.Candles) == 0 {
return nil, fmt.Errorf("get vwap %w", errNoData)
}
store := make([]float64, len(k.Candles))
var cumTotal, cumVolume float64
for x := range k.Candles {
cumTotal += k.Candles[x].GetTypicalPrice() * k.Candles[x].Volume
cumVolume += k.Candles[x].Volume
store[x] = cumTotal / cumVolume
}
return store, nil
}
// GetVWAPs returns the Volume Weighted Averages prices which are the cumulative
// average price with respect to the volume.
func (o *OHLC) GetVWAPs() ([]float64, error) {
if o == nil {
return nil, fmt.Errorf("get vwap %w", errNilOHLC)
}
if len(o.Open) == 0 || len(o.High) == 0 || len(o.Low) == 0 || len(o.Close) == 0 {
return nil, fmt.Errorf("get vwap %w", errNoData)
}
if len(o.Open) != len(o.High) || len(o.Open) != len(o.Low) || len(o.Open) != len(o.Close) {
return nil, fmt.Errorf("get vwap %w", errDataLengthMismatch)
}
store := make([]float64, len(o.High))
var cumTotal, cumVolume float64
for x := range o.High {
typPrice, err := o.GetTypicalPrice(x)
if err != nil {
return nil, fmt.Errorf("get vwap %w", err)
}
cumTotal += typPrice * o.Volume[x]
cumVolume += o.Volume[x]
store[x] = cumTotal / cumVolume
}
return store, nil
}

View File

@@ -0,0 +1,313 @@
package kline
import (
"errors"
"testing"
"time"
)
func TestGetAveragePrice(t *testing.T) {
t.Parallel()
c := Candle{}
if c.GetAveragePrice() != 0 {
t.Fatal("unexpected value")
}
c.High = 20
if c.GetAveragePrice() != 5 {
t.Fatal("unexpected value")
}
}
func TestGetAveragePrice_OHLC(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetAveragePrice(-1)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetAveragePrice(-1)
if !errors.Is(err, errInvalidElement) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidElement)
}
_, err = ohlc.GetAveragePrice(0)
if !errors.Is(err, errElementExceedsDataLength) {
t.Fatalf("received: '%v' but expected: '%v'", err, errElementExceedsDataLength)
}
ohlc.High = append(ohlc.High, 20)
ohlc.Open = append(ohlc.Open, 0)
ohlc.Low = append(ohlc.Low, 0)
ohlc.Close = append(ohlc.Close, 0)
avgPrice, err := ohlc.GetAveragePrice(0)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if avgPrice != 5 {
t.Fatal("unexpected value")
}
}
var twapdataset = []Candle{
{Time: time.Date(2020, 6, 17, 0, 0, 0, 0, time.UTC), Close: 351.59, Open: 355.15, High: 355.40, Low: 351.09},
{Time: time.Date(2020, 6, 16, 0, 0, 0, 0, time.UTC), Close: 352.08, Open: 351.46, High: 353.20, Low: 344.72},
{Time: time.Date(2020, 6, 15, 0, 0, 0, 0, time.UTC), Close: 342.99, Open: 333.25, High: 345.68, Low: 332.58},
{Time: time.Date(2020, 6, 12, 0, 0, 0, 0, time.UTC), Close: 338.80, Open: 344.72, High: 347.80, Low: 334.22},
{Time: time.Date(2020, 6, 11, 0, 0, 0, 0, time.UTC), Close: 335.90, Open: 349.31, High: 351.06, Low: 335.48},
{Time: time.Date(2020, 6, 10, 0, 0, 0, 0, time.UTC), Close: 352.84, Open: 347.90, High: 354.77, Low: 346.09},
{Time: time.Date(2020, 6, 9, 0, 0, 0, 0, time.UTC), Close: 343.99, Open: 332.14, High: 345.61, Low: 332.01},
{Time: time.Date(2020, 6, 8, 0, 0, 0, 0, time.UTC), Close: 333.46, Open: 330.25, High: 333.60, Low: 327.32},
{Time: time.Date(2020, 6, 5, 0, 0, 0, 0, time.UTC), Close: 331.50, Open: 323.35, High: 331.75, Low: 323.23},
{Time: time.Date(2020, 6, 4, 0, 0, 0, 0, time.UTC), Close: 322.32, Open: 324.39, High: 325.62, Low: 320.78},
{Time: time.Date(2020, 6, 3, 0, 0, 0, 0, time.UTC), Close: 325.12, Open: 324.66, High: 326.20, Low: 322.30},
{Time: time.Date(2020, 6, 2, 0, 0, 0, 0, time.UTC), Close: 323.34, Open: 320.75, High: 323.44, Low: 318.93},
{Time: time.Date(2020, 6, 1, 0, 0, 0, 0, time.UTC), Close: 321.85, Open: 317.75, High: 322.35, Low: 317.21},
{Time: time.Date(2020, 5, 29, 0, 0, 0, 0, time.UTC), Close: 317.94, Open: 319.25, High: 321.15, Low: 316.47},
{Time: time.Date(2020, 5, 28, 0, 0, 0, 0, time.UTC), Close: 318.25, Open: 316.77, High: 323.44, Low: 315.63},
{Time: time.Date(2020, 5, 27, 0, 0, 0, 0, time.UTC), Close: 318.11, Open: 316.14, High: 318.71, Low: 313.09},
{Time: time.Date(2020, 5, 26, 0, 0, 0, 0, time.UTC), Close: 316.73, Open: 323.50, High: 324.24, Low: 316.50},
{Time: time.Date(2020, 5, 22, 0, 0, 0, 0, time.UTC), Close: 318.89, Open: 315.77, High: 319.23, Low: 315.35},
{Time: time.Date(2020, 5, 21, 0, 0, 0, 0, time.UTC), Close: 316.85, Open: 318.66, High: 320.89, Low: 315.87},
{Time: time.Date(2020, 5, 20, 0, 0, 0, 0, time.UTC), Close: 319.23, Open: 316.68, High: 319.52, Low: 316.20},
{Time: time.Date(2020, 5, 19, 0, 0, 0, 0, time.UTC), Close: 313.14, Open: 315.03, High: 318.52, Low: 313.01},
{Time: time.Date(2020, 5, 18, 0, 0, 0, 0, time.UTC), Close: 314.96, Open: 313.17, High: 316.50, Low: 310.32},
}
func TestGetTWAP_OHLC(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetTWAP()
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetTWAP()
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Open = append(ohlc.Open, 20)
ohlc.High = append(ohlc.High, 20)
ohlc.Low = append(ohlc.Low, 20)
ohlc.Close = append(ohlc.Close, 20, 20)
_, err = ohlc.GetTWAP()
if !errors.Is(err, errDataLengthMismatch) {
t.Fatalf("received: '%v' but expected: '%v'", err, errDataLengthMismatch)
}
i := Item{}
i.Candles = twapdataset
ohlc = i.GetOHLC()
twap, err := ohlc.GetTWAP()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if twap != 328.147840909091 {
t.Fatal("unexpected value returned from data-set")
}
}
func TestGetTWAP(t *testing.T) {
t.Parallel()
candles := Item{}
if _, err := candles.GetTWAP(); !errors.Is(err, errNoData) {
t.Fatal(err)
}
candles.Candles = twapdataset
twap, err := candles.GetTWAP()
if err != nil {
t.Fatal(err)
}
if twap != 328.147840909091 {
t.Fatal("unexpected value returned from data-set")
}
}
var vwapdataset = []Candle{
{Time: time.Date(2019, 10, 10, 9, 31, 0, 0, time.UTC), Open: 245.2903, High: 245.516, Low: 244.7652, Close: 244.8702, Volume: 103033},
{Time: time.Date(2019, 10, 10, 9, 32, 0, 0, time.UTC), Open: 245.0807, High: 245.0807, Low: 244.55, Close: 244.66, Volume: 21168},
{Time: time.Date(2019, 10, 10, 9, 33, 0, 0, time.UTC), Open: 244.58, High: 245.8, Low: 244.55, Close: 245.6, Volume: 36544},
{Time: time.Date(2019, 10, 10, 9, 34, 0, 0, time.UTC), Open: 245.7097, High: 246.09, Low: 245.57, Close: 245.92, Volume: 30057},
{Time: time.Date(2019, 10, 10, 9, 35, 0, 0, time.UTC), Open: 245.62, High: 245.62, Low: 245.62, Close: 245.62, Volume: 26301},
{Time: time.Date(2019, 10, 10, 9, 36, 0, 0, time.UTC), Open: 245.7126, High: 246.44, Low: 245.7126, Close: 246.188, Volume: 31494},
{Time: time.Date(2019, 10, 10, 9, 37, 0, 0, time.UTC), Open: 246.46, High: 246.46, Low: 246.45, Close: 246.45, Volume: 24271},
{Time: time.Date(2019, 10, 10, 9, 38, 0, 0, time.UTC), Open: 246.755, High: 246.755, Low: 246.25, Close: 246.25, Volume: 37951},
{Time: time.Date(2019, 10, 10, 9, 39, 0, 0, time.UTC), Open: 246.2818, High: 246.655, Low: 246.2818, Close: 246.655, Volume: 15324},
{Time: time.Date(2019, 10, 10, 9, 40, 0, 0, time.UTC), Open: 246.78, High: 246.78, Low: 246.56, Close: 246.762, Volume: 23285},
{Time: time.Date(2019, 10, 10, 9, 41, 0, 0, time.UTC), Open: 246.75, High: 246.75, Low: 246.38, Close: 246.5, Volume: 23365},
{Time: time.Date(2019, 10, 10, 9, 42, 0, 0, time.UTC), Open: 246.17, High: 246.17, Low: 246.17, Close: 246.17, Volume: 16130},
{Time: time.Date(2019, 10, 10, 9, 43, 0, 0, time.UTC), Open: 246.135, High: 246.135, Low: 245.82, Close: 245.82, Volume: 27227},
{Time: time.Date(2019, 10, 10, 9, 44, 0, 0, time.UTC), Open: 245.9335, High: 245.9335, Low: 245.91, Close: 245.91, Volume: 14464},
{Time: time.Date(2019, 10, 10, 9, 45, 0, 0, time.UTC), Open: 246.41, High: 246.41, Low: 246.41, Close: 246.41, Volume: 17156},
{Time: time.Date(2019, 10, 10, 9, 46, 0, 0, time.UTC), Open: 246.44, High: 246.46, Low: 246.1683, Close: 246.1683, Volume: 23938},
{Time: time.Date(2019, 10, 10, 9, 47, 0, 0, time.UTC), Open: 246.2857, High: 246.57, Low: 246.2857, Close: 246.57, Volume: 70833},
{Time: time.Date(2019, 10, 10, 9, 48, 0, 0, time.UTC), Open: 246.6, High: 247.47, Low: 246.6, Close: 247.47, Volume: 59743},
{Time: time.Date(2019, 10, 10, 9, 49, 0, 0, time.UTC), Open: 247.49, High: 247.65, Low: 247.49, Close: 247.65, Volume: 71995},
{Time: time.Date(2019, 10, 10, 9, 50, 0, 0, time.UTC), Open: 247.685, High: 247.801, Low: 247.65, Close: 247.69, Volume: 46038},
{Time: time.Date(2019, 10, 10, 9, 51, 0, 0, time.UTC), Open: 247.95, High: 248.74, Low: 247.95, Close: 248.74, Volume: 103773},
{Time: time.Date(2019, 10, 10, 9, 52, 0, 0, time.UTC), Open: 248.56, High: 248.56, Low: 247.95, Close: 247.95, Volume: 73810},
{Time: time.Date(2019, 10, 10, 9, 53, 0, 0, time.UTC), Open: 247.93, High: 247.93, Low: 247.6614, Close: 247.6614, Volume: 29784},
{Time: time.Date(2019, 10, 10, 9, 54, 0, 0, time.UTC), Open: 247.74, High: 247.76, Low: 247.65, Close: 247.76, Volume: 37138},
{Time: time.Date(2019, 10, 10, 9, 55, 0, 0, time.UTC), Open: 247.93, High: 248.03, Low: 247.93, Close: 248.03, Volume: 53166},
{Time: time.Date(2019, 10, 10, 9, 56, 0, 0, time.UTC), Open: 247.91, High: 248.44, Low: 247.91, Close: 248.44, Volume: 40789},
{Time: time.Date(2019, 10, 10, 9, 57, 0, 0, time.UTC), Open: 248.52, High: 248.52, Low: 248.3154, Close: 248.3154, Volume: 51988},
{Time: time.Date(2019, 10, 10, 9, 58, 0, 0, time.UTC), Open: 248.4409, High: 248.62, Low: 248.4409, Close: 248.62, Volume: 53405},
{Time: time.Date(2019, 10, 10, 9, 59, 0, 0, time.UTC), Open: 248.9199, High: 248.9199, Low: 248.9199, Close: 248.9199, Volume: 85348},
{Time: time.Date(2019, 10, 10, 10, 0, 0, 0, time.UTC), Open: 248.91, High: 249.08, Low: 248.42, Close: 248.72, Volume: 58270},
}
func TestGetVWAPs(t *testing.T) {
t.Parallel()
candles := Item{}
if _, err := candles.GetVWAPs(); !errors.Is(err, errNoData) {
t.Fatal(err)
}
candles.Candles = vwapdataset
vwap, err := candles.GetVWAPs()
if err != nil {
t.Fatal(err)
}
assert(t, vwap[0], 245.05046666666664)
assert(t, vwap[1], 245.00156932123465)
assert(t, vwap[2], 245.07320400593073)
assert(t, vwap[3], 245.19714781780763)
assert(t, vwap[4], 245.248374356565)
assert(t, vwap[5], 245.35797872352975)
assert(t, vwap[6], 245.45540807301208)
assert(t, vwap[7], 245.57298124760712)
assert(t, vwap[8], 245.61797546720302)
assert(t, vwap[9], 245.6901232761351)
assert(t, vwap[10], 245.7435986712912)
assert(t, vwap[11], 245.76128302894574)
assert(t, vwap[12], 245.771994363731)
assert(t, vwap[13], 245.7768929849006)
assert(t, vwap[14], 245.80115004533573)
assert(t, vwap[15], 245.82471633454026)
assert(t, vwap[16], 245.90964645148168)
assert(t, vwap[17], 246.0356579876492)
assert(t, vwap[18], 246.20233204964117)
assert(t, vwap[19], 246.29892677543359)
assert(t, vwap[20], 246.57315726207088)
assert(t, vwap[21], 246.70305234595537)
assert(t, vwap[22], 246.73669536160304)
assert(t, vwap[23], 246.7746731036053)
assert(t, vwap[24], 246.83849361010806)
assert(t, vwap[25], 246.89338504378165)
assert(t, vwap[26], 246.96313273581723)
assert(t, vwap[27], 247.03640100225914)
assert(t, vwap[28], 247.16505290840146)
assert(t, vwap[29], 247.23522648930867)
}
func TestGetVWAPs_OHLC(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetVWAPs()
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetVWAPs()
if !errors.Is(err, errNoData) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
}
ohlc.Open = append(ohlc.Open, 20)
ohlc.High = append(ohlc.High, 20)
ohlc.Low = append(ohlc.Low, 20)
ohlc.Close = append(ohlc.Close, 20, 20)
_, err = ohlc.GetVWAPs()
if !errors.Is(err, errDataLengthMismatch) {
t.Fatalf("received: '%v' but expected: '%v'", err, errDataLengthMismatch)
}
ohlc = (&Item{Candles: vwapdataset}).GetOHLC()
vwap, err := ohlc.GetVWAPs()
if err != nil {
t.Fatal(err)
}
assert(t, vwap[0], 245.05046666666664)
assert(t, vwap[1], 245.00156932123465)
assert(t, vwap[2], 245.07320400593073)
assert(t, vwap[3], 245.19714781780763)
assert(t, vwap[4], 245.248374356565)
assert(t, vwap[5], 245.35797872352975)
assert(t, vwap[6], 245.45540807301208)
assert(t, vwap[7], 245.57298124760712)
assert(t, vwap[8], 245.61797546720302)
assert(t, vwap[9], 245.6901232761351)
assert(t, vwap[10], 245.7435986712912)
assert(t, vwap[11], 245.76128302894574)
assert(t, vwap[12], 245.771994363731)
assert(t, vwap[13], 245.7768929849006)
assert(t, vwap[14], 245.80115004533573)
assert(t, vwap[15], 245.82471633454026)
assert(t, vwap[16], 245.90964645148168)
assert(t, vwap[17], 246.0356579876492)
assert(t, vwap[18], 246.20233204964117)
assert(t, vwap[19], 246.29892677543359)
assert(t, vwap[20], 246.57315726207088)
assert(t, vwap[21], 246.70305234595537)
assert(t, vwap[22], 246.73669536160304)
assert(t, vwap[23], 246.7746731036053)
assert(t, vwap[24], 246.83849361010806)
assert(t, vwap[25], 246.89338504378165)
assert(t, vwap[26], 246.96313273581723)
assert(t, vwap[27], 247.03640100225914)
assert(t, vwap[28], 247.16505290840146)
assert(t, vwap[29], 247.23522648930867)
}
func TestGetTypicalPrice_OHLC(t *testing.T) {
t.Parallel()
var ohlc *OHLC
_, err := ohlc.GetTypicalPrice(-1)
if !errors.Is(err, errNilOHLC) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilOHLC)
}
ohlc = &OHLC{}
_, err = ohlc.GetTypicalPrice(-1)
if !errors.Is(err, errInvalidElement) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidElement)
}
_, err = ohlc.GetTypicalPrice(0)
if !errors.Is(err, errElementExceedsDataLength) {
t.Fatalf("received: '%v' but expected: '%v'", err, errElementExceedsDataLength)
}
ohlc.High = append(ohlc.High, 15)
ohlc.Low = append(ohlc.Low, 0)
ohlc.Close = append(ohlc.Close, 0)
avgPrice, err := ohlc.GetTypicalPrice(0)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if avgPrice != 5 {
t.Fatal("unexpected value")
}
}
func assert(t *testing.T, received, expected float64) {
t.Helper()
if received != expected {
t.Fatalf("received: '%v' but expected: '%v'", received, expected)
}
}

View File

@@ -989,7 +989,7 @@ func TestValidateCandlesRequest(t *testing.T) {
t.Error(err)
}
_, err = z.validateCandlesRequest(currency.EMPTYPAIR, asset.Spot, time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC), time.Date(2020, 1, 1, 1, 1, 1, 3, time.UTC), kline.OneHour)
if err != nil && err.Error() != "pair not enabled" {
if !errors.Is(err, kline.ErrValidatingParams) {
t.Error(err)
}
var p currency.Pair