Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: [indicator] adding a bunch of new v2 indicators #1366

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 19 additions & 16 deletions pkg/bbgo/indicator_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package bbgo
import (
"github.com/sirupsen/logrus"

"github.com/c9s/bbgo/pkg/indicator/v2"
indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
"github.com/c9s/bbgo/pkg/indicator/v2/momentum"
"github.com/c9s/bbgo/pkg/indicator/v2/trend"
"github.com/c9s/bbgo/pkg/indicator/v2/volatility"
go-dockly marked this conversation as resolved.
Show resolved Hide resolved
"github.com/c9s/bbgo/pkg/types"
)

Expand Down Expand Up @@ -73,34 +76,34 @@ func (i *IndicatorSet) VOLUME(interval types.Interval) *indicatorv2.PriceStream
return indicatorv2.Volumes(i.KLines(interval))
}

func (i *IndicatorSet) RSI(iw types.IntervalWindow) *indicatorv2.RSIStream {
return indicatorv2.RSI2(i.CLOSE(iw.Interval), iw.Window)
func (i *IndicatorSet) RSI(iw types.IntervalWindow) *momentum.RSIStream {
return momentum.RSI2(i.CLOSE(iw.Interval), iw.Window)
go-dockly marked this conversation as resolved.
Show resolved Hide resolved
}

func (i *IndicatorSet) EMA(iw types.IntervalWindow) *indicatorv2.EWMAStream {
func (i *IndicatorSet) EMA(iw types.IntervalWindow) *trend.EWMAStream {
return i.EWMA(iw)
}

func (i *IndicatorSet) EWMA(iw types.IntervalWindow) *indicatorv2.EWMAStream {
return indicatorv2.EWMA2(i.CLOSE(iw.Interval), iw.Window)
func (i *IndicatorSet) EWMA(iw types.IntervalWindow) *trend.EWMAStream {
return trend.EWMA2(i.CLOSE(iw.Interval), iw.Window)
}

func (i *IndicatorSet) STOCH(iw types.IntervalWindow, dPeriod int) *indicatorv2.StochStream {
return indicatorv2.Stoch(i.KLines(iw.Interval), iw.Window, dPeriod)
func (i *IndicatorSet) STOCH(iw types.IntervalWindow, dPeriod int) *momentum.StochStream {
return momentum.Stoch(i.KLines(iw.Interval), iw.Window, dPeriod)
}

func (i *IndicatorSet) BOLL(iw types.IntervalWindow, k float64) *indicatorv2.BOLLStream {
return indicatorv2.BOLL(i.CLOSE(iw.Interval), iw.Window, k)
func (i *IndicatorSet) BOLL(iw types.IntervalWindow, k float64) *volatility.BollingerStream {
return volatility.BollingerBand(i.CLOSE(iw.Interval), iw.Window, k)
}

func (i *IndicatorSet) MACD(interval types.Interval, shortWindow, longWindow, signalWindow int) *indicatorv2.MACDStream {
return indicatorv2.MACD2(i.CLOSE(interval), shortWindow, longWindow, signalWindow)
func (i *IndicatorSet) MACD(interval types.Interval, shortWindow, longWindow, signalWindow int) *trend.MACDStream {
return trend.MACD2(i.CLOSE(interval), shortWindow, longWindow, signalWindow)
}

func (i *IndicatorSet) ATR(interval types.Interval, window int) *indicatorv2.ATRStream {
return indicatorv2.ATR2(i.KLines(interval), window)
func (i *IndicatorSet) ATR(interval types.Interval, window int) *volatility.ATRStream {
return volatility.ATR2(i.KLines(interval), window)
}

func (i *IndicatorSet) ATRP(interval types.Interval, window int) *indicatorv2.ATRPStream {
return indicatorv2.ATRP2(i.KLines(interval), window)
func (i *IndicatorSet) ATRP(interval types.Interval, window int) *volatility.ATRPStream {
return volatility.ATRP2(i.KLines(interval), window)
}
101 changes: 101 additions & 0 deletions pkg/datasource/csvsource/bybit_tick_downloader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package csvsource

import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)

func Download(symbol string, start time.Time) {
for {
var (
path = fmt.Sprintf("pkg/datasource/csv/testdata/bybit/%s/", symbol)
go-dockly marked this conversation as resolved.
Show resolved Hide resolved
fileName = fmt.Sprintf("%s%s.csv", strings.ToUpper(symbol), start.Format("2006-01-02"))
)

if fileExists(path + fileName) {
go-dockly marked this conversation as resolved.
Show resolved Hide resolved
start = start.AddDate(0, 0, 1)
continue
}

var url = fmt.Sprintf("https://public.bybit.com/trading/%s/%s.gz",
strings.ToUpper(symbol),
fileName)

fmt.Println("fetching ", url)
go-dockly marked this conversation as resolved.
Show resolved Hide resolved

err := readCSVFromUrl(url, path, fileName)
if err != nil {
fmt.Println(err)
break
}

start = start.AddDate(0, 0, 1)
}
}

func readCSVFromUrl(url, path, fileName string) error {
go-dockly marked this conversation as resolved.
Show resolved Hide resolved
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}

csvContent, err := gUnzipData(body)
if err != nil {
return err
}

if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
return err
}
}

err = os.WriteFile(path+fileName, csvContent, 0666)
if err != nil {
return err
}

return nil
}

func gUnzipData(data []byte) (resData []byte, err error) {
b := bytes.NewBuffer(data)

var r io.Reader
r, err = gzip.NewReader(b)
if err != nil {
return
}

var resB bytes.Buffer
_, err = resB.ReadFrom(r)
if err != nil {
return
}

resData = resB.Bytes()

return
}

func fileExists(fileName string) bool {
info, err := os.Stat(fileName)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
197 changes: 197 additions & 0 deletions pkg/datasource/csvsource/bybit_tick_to_kline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package csvsource

import (
"encoding/csv"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/pkg/errors"

"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)

var klines []types.KLine

type BybitCsvTick struct {
Timestamp int64 `json:"timestamp"`
Symbol string `json:"symbol"`
Side string `json:"side"`
TickDirection string `json:"tickDirection"`
Size fixedpoint.Value `json:"size"`
Price fixedpoint.Value `json:"price"`
HomeNotional fixedpoint.Value `json:"homeNotional"`
ForeignNotional fixedpoint.Value `json:"foreignNotional"`
}

func ConvertTicksToKLines(symbol string, interval time.Duration) error {
err := filepath.Walk(
fmt.Sprintf("pkg/datasource/csv/testdata/bybit/%s/", symbol),
go-dockly marked this conversation as resolved.
Show resolved Hide resolved
func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Println(err)
return err
}
fmt.Printf("dir: %v: name: %s\n", info.IsDir(), path)
go-dockly marked this conversation as resolved.
Show resolved Hide resolved
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
reader := csv.NewReader(file)
data, err := reader.ReadAll()
if err != nil {
return err
}
for idx, row := range data {
// skip header
if idx == 0 {
continue
}
if idx == 1 {
continue
}
timestamp, err := strconv.ParseInt(strings.Split(row[0], ".")[0], 10, 64)
if err != nil {
return err
}
size, err := strconv.ParseFloat(row[3], 64)
if err != nil {
return err
}
price, err := strconv.ParseFloat(row[4], 64)
if err != nil {
return err
}
homeNotional, err := strconv.ParseFloat(row[5], 64)
if err != nil {
return err
}
foreignNotional, err := strconv.ParseFloat(row[9], 64)
if err != nil {
return err
}
ConvertBybitCsvTickToCandles(BybitCsvTick{
Timestamp: int64(timestamp),
Symbol: row[1],
Side: row[2],
Size: fixedpoint.NewFromFloat(size),
Price: fixedpoint.NewFromFloat(price),
TickDirection: row[5],
HomeNotional: fixedpoint.NewFromFloat(homeNotional),
ForeignNotional: fixedpoint.NewFromFloat(foreignNotional),
}, interval)
}
}
return nil
})
if err != nil {
return err
}

return WriteKLines(fmt.Sprintf("pkg/datasource/csv/testdata/%s_%s.csv", symbol, interval.String()), klines)
}

// WriteKLines write csv to path.
func WriteKLines(path string, prices []types.KLine) (err error) {
file, err := os.Create(path)
if err != nil {
return errors.Wrap(err, "failed to open file")
}
defer func() {
err = file.Close()
if err != nil {
panic("failed to close file")
}
}()
w := csv.NewWriter(file)
defer w.Flush()
// Using Write
for _, record := range prices {
row := []string{strconv.Itoa(int(record.StartTime.UnixMilli())), dtos(record.Open), dtos(record.High), dtos(record.Low), dtos(record.Close), dtos(record.Volume)}
if err := w.Write(row); err != nil {
return errors.Wrap(err, "writing record to file")
}
}
if err != nil {
return err
}

return nil
}

func dtos(n fixedpoint.Value) string {
return fmt.Sprintf("%f", n.Float64())
}

func ConvertBybitCsvTickToCandles(tick BybitCsvTick, interval time.Duration) {
var (
currentCandle = types.KLine{}
high = fixedpoint.Zero
low = fixedpoint.Zero
tickTimeStamp = time.Unix(tick.Timestamp, 0)
)
isOpen, isCLose, openTime := detCandleStart(tickTimeStamp, interval)

if isOpen {
klines = append(klines, types.KLine{
StartTime: types.NewTimeFromUnix(openTime.Unix(), 0),
Open: tick.Price,
High: tick.Price,
Low: tick.Price,
Close: tick.Price,
Volume: tick.HomeNotional,
})
return
}

currentCandle = klines[len(klines)-1]

if tick.Price > currentCandle.High {
high = tick.Price
} else {
high = currentCandle.High
}

if tick.Price < currentCandle.Low {
low = tick.Price
} else {
low = currentCandle.Low
}

kline := types.KLine{
StartTime: currentCandle.StartTime,
Open: currentCandle.Open,
High: high,
Low: low,
Close: tick.Price,
Volume: currentCandle.Volume.Add(tick.HomeNotional),
}
if isCLose {
klines = append(klines, kline)
} else {
klines[len(klines)-1] = kline
}
}

func detCandleStart(ts time.Time, interval time.Duration) (isOpen, isClose bool, t time.Time) {
if len(klines) == 0 {
start := time.Date(ts.Year(), ts.Month(), ts.Day(), ts.Hour()+1, 0, 0, 0, ts.Location())
if t.Minute() < int(interval.Minutes()) { // supported intervals 5 10 15 30
start = time.Date(ts.Year(), ts.Month(), ts.Day(), ts.Hour(), int(interval.Minutes()), 0, 0, ts.Location())
}
return true, false, start
} else {
current := klines[len(klines)-1]
end := current.StartTime.Time().Add(interval)
if end.After(ts) {
return false, true, end
}
}

return false, false, t
}
Loading