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

@@ -19,6 +19,7 @@ import (
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/pquerna/otp/totp"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gct-ta/indicators"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/common/file"
@@ -36,6 +37,7 @@ import (
"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/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/gctrpc"
@@ -71,6 +73,7 @@ var (
errNoAccountInformation = errors.New("account information does not exist")
errShutdownNotAllowed = errors.New("shutting down this bot instance is not allowed via gRPC, please enable by command line flag --grpcshutdown or config.json field grpcAllowBotShutdown")
errGRPCShutdownSignalIsNil = errors.New("cannot shutdown, gRPC shutdown channel is nil")
errInvalidStrategy = errors.New("invalid strategy")
)
// RPCServer struct
@@ -108,7 +111,15 @@ func (s *RPCServer) authenticateClient(ctx context.Context) (context.Context, er
password != s.Config.RemoteControl.Password {
return ctx, fmt.Errorf("username/password mismatch")
}
return exchange.ParseCredentialsMetadata(ctx, md)
ctx, err = exchange.ParseCredentialsMetadata(ctx, md)
if err != nil {
return ctx, err
}
if _, ok := md["verbose"]; ok {
ctx = request.WithVerbose(ctx)
}
return ctx, nil
}
// StartRPCServer starts a gRPC server with TLS auth
@@ -4575,3 +4586,166 @@ func (s *RPCServer) Shutdown(_ context.Context, _ *gctrpc.ShutdownRequest) (*gct
s.Engine.GRPCShutdownSignal = nil
return &gctrpc.ShutdownResponse{}, nil
}
// GetTechnicalAnalysis using the requested technical analysis method will
// return a set(s) of signals for price action analysis.
func (s *RPCServer) GetTechnicalAnalysis(ctx context.Context, r *gctrpc.GetTechnicalAnalysisRequest) (*gctrpc.GetTechnicalAnalysisResponse, error) {
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
as, err := asset.New(r.AssetType)
if err != nil {
return nil, err
}
pair, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote)
if err != nil {
return nil, err
}
klineInterval := kline.Interval(r.Interval)
err = exch.GetBase().ValidateKline(pair, as, klineInterval)
if err != nil {
return nil, err
}
klines, err := exch.GetHistoricCandlesExtended(ctx,
pair,
as,
r.Start.AsTime(),
r.End.AsTime(),
klineInterval)
if err != nil {
return nil, err
}
signals := make(map[string]*gctrpc.ListOfSignals)
switch strings.ToUpper(r.AlgorithmType) {
case "TWAP":
var price float64
price, err = klines.GetTWAP()
if err != nil {
return nil, err
}
signals["TWAP"] = &gctrpc.ListOfSignals{Signals: []float64{price}}
case "VWAP":
var prices []float64
prices, err = klines.GetVWAPs()
if err != nil {
return nil, err
}
signals["VWAP"] = &gctrpc.ListOfSignals{Signals: prices}
case "ATR":
var prices []float64
prices, err = klines.GetAverageTrueRange(r.Period)
if err != nil {
return nil, err
}
signals["ATR"] = &gctrpc.ListOfSignals{Signals: prices}
case "BBANDS":
var bollinger *kline.Bollinger
bollinger, err = klines.GetBollingerBands(r.Period,
r.StandardDeviationUp,
r.StandardDeviationDown,
indicators.MaType(r.MovingAverageType))
if err != nil {
return nil, err
}
signals["UPPER"] = &gctrpc.ListOfSignals{Signals: bollinger.Upper}
signals["MIDDLE"] = &gctrpc.ListOfSignals{Signals: bollinger.Middle}
signals["LOWER"] = &gctrpc.ListOfSignals{Signals: bollinger.Lower}
case "COCO":
otherExch := exch
if r.OtherExchange != "" {
otherExch, err = s.GetExchangeByName(r.OtherExchange)
if err != nil {
return nil, err
}
}
otherAs := as
if r.OtherAssetType != "" {
otherAs, err = asset.New(r.OtherAssetType)
if err != nil {
return nil, err
}
}
if r.OtherPair.String() == "" {
return nil, errors.New("other pair is empty, to compare this must be specified")
}
var otherPair currency.Pair
otherPair, err = currency.NewPairFromStrings(r.OtherPair.Base, r.OtherPair.Quote)
if err != nil {
return nil, err
}
var otherKlines kline.Item
otherKlines, err = otherExch.GetHistoricCandlesExtended(ctx,
otherPair, otherAs, r.Start.AsTime(), r.End.AsTime(), klineInterval)
if err != nil {
return nil, err
}
var correlation []float64
correlation, err = klines.GetCorrelationCoefficient(&otherKlines, r.Period)
if err != nil {
return nil, err
}
signals["COCO"] = &gctrpc.ListOfSignals{Signals: correlation}
case "SMA":
var prices []float64
prices, err = klines.GetSimpleMovingAverageOnClose(r.Period)
if err != nil {
return nil, err
}
signals["SMA"] = &gctrpc.ListOfSignals{Signals: prices}
case "EMA":
var prices []float64
prices, err = klines.GetExponentialMovingAverageOnClose(r.Period)
if err != nil {
return nil, err
}
signals["EMA"] = &gctrpc.ListOfSignals{Signals: prices}
case "MACD":
var macd *kline.MACD
macd, err = klines.GetMovingAverageConvergenceDivergenceOnClose(r.FastPeriod,
r.SlowPeriod,
r.Period)
if err != nil {
return nil, err
}
signals["MACD"] = &gctrpc.ListOfSignals{Signals: macd.Results}
signals["SIGNAL"] = &gctrpc.ListOfSignals{Signals: macd.SignalVals}
signals["HISTOGRAM"] = &gctrpc.ListOfSignals{Signals: macd.Histogram}
case "MFI":
var prices []float64
prices, err = klines.GetMoneyFlowIndex(r.Period)
if err != nil {
return nil, err
}
signals["MFI"] = &gctrpc.ListOfSignals{Signals: prices}
case "OBV":
var prices []float64
prices, err = klines.GetOnBalanceVolume()
if err != nil {
return nil, err
}
signals["OBV"] = &gctrpc.ListOfSignals{Signals: prices}
case "RSI":
var prices []float64
prices, err = klines.GetRelativeStrengthIndexOnClose(r.Period)
if err != nil {
return nil, err
}
signals["RSI"] = &gctrpc.ListOfSignals{Signals: prices}
default:
return nil, fmt.Errorf("%w '%s'", errInvalidStrategy, r.AlgorithmType)
}
return &gctrpc.GetTechnicalAnalysisResponse{Signals: signals}, nil
}

View File

@@ -72,22 +72,29 @@ func (f fExchange) GetHistoricCandles(ctx context.Context, p currency.Pair, a as
}, nil
}
func generateCandles(amount int, timeStart time.Time, interval kline.Interval) []kline.Candle {
candy := make([]kline.Candle, amount)
for x := 0; x < amount; x++ {
candy[x] = kline.Candle{
Time: timeStart,
Open: 1337,
High: 1337,
Low: 1337,
Close: 1337,
Volume: 1337,
}
timeStart = timeStart.Add(interval.Duration())
}
return candy
}
func (f fExchange) GetHistoricCandlesExtended(ctx context.Context, p currency.Pair, a asset.Item, timeStart, _ time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{
Exchange: fakeExchangeName,
Pair: p,
Asset: a,
Interval: interval,
Candles: []kline.Candle{
{
Time: timeStart,
Open: 1337,
High: 1337,
Low: 1337,
Close: 1337,
Volume: 1337,
},
},
Candles: generateCandles(33, timeStart, interval),
}, nil
}
@@ -2340,3 +2347,275 @@ func TestShutdown(t *testing.T) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetTechnicalAnalysis(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-usd")
if !errors.Is(err, nil) {
t.Fatalf("received '%v', expected '%v'", err, nil)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Futures] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
b.Features.Enabled.Kline.Intervals = map[string]bool{
kline.OneDay.Word(): true,
}
em.Add(fExchange{IBotExchange: exch})
s := RPCServer{
Engine: &Engine{
ExchangeManager: em,
currencyStateManager: &CurrencyStateManager{
started: 1,
iExchangeManager: em,
},
},
}
_, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{})
if !errors.Is(err, ErrExchangeNameIsEmpty) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrExchangeNameIsEmpty)
}
_, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
})
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
_, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "upsideprofitcontract",
Pair: &gctrpc.CurrencyPair{},
})
if !errors.Is(err, kline.ErrValidatingParams) {
t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrValidatingParams)
}
_, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
})
if !errors.Is(err, errInvalidStrategy) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidStrategy)
}
resp, err := s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "twap",
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if resp.Signals["TWAP"].Signals[0] != 1337 {
t.Fatalf("received: '%v' but expected: '%v'", resp.Signals["TWAP"].Signals[0], 1337)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "vwap",
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["VWAP"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["VWAP"].Signals), 33)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "atr",
Period: 9,
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["ATR"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["ATR"].Signals), 33)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "bbands",
Period: 9,
StandardDeviationUp: 0.5,
StandardDeviationDown: 0.5,
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["UPPER"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["UPPER"].Signals), 33)
}
if len(resp.Signals["MIDDLE"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["MIDDLE"].Signals), 33)
}
if len(resp.Signals["LOWER"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["LOWER"].Signals), 33)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
OtherPair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "COCO",
Period: 9,
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["COCO"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["COCO"].Signals), 33)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "sma",
Period: 9,
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["SMA"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["SMA"].Signals), 33)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "ema",
Period: 9,
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["EMA"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["EMA"].Signals), 33)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "macd",
Period: 9,
FastPeriod: 12,
SlowPeriod: 26,
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["MACD"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["MACD"].Signals), 33)
}
if len(resp.Signals["SIGNAL"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["SIGNAL"].Signals), 33)
}
if len(resp.Signals["HISTOGRAM"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["HISTOGRAM"].Signals), 33)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "mfi",
Period: 9,
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["MFI"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["MFI"].Signals), 33)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "obv",
Period: 9,
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["OBV"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["OBV"].Signals), 33)
}
resp, err = s.GetTechnicalAnalysis(context.Background(), &gctrpc.GetTechnicalAnalysisRequest{
Exchange: fakeExchangeName,
AssetType: "spot",
Pair: &gctrpc.CurrencyPair{Base: "btc", Quote: "usd"},
Interval: int64(kline.OneDay),
AlgorithmType: "rsi",
Period: 9,
})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(resp.Signals["RSI"].Signals) != 33 {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["RSI"].Signals), 33)
}
}