Merge pull request #543 from austin362667/strategy/factorzoo

strategy: factorzoo
This commit is contained in:
Yo-An Lin 2022-04-20 22:30:27 +08:00 committed by GitHub
commit 1dea293fca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 400 additions and 1 deletions

30
config/factorzoo.yaml Normal file
View File

@ -0,0 +1,30 @@
sessions:
binance:
exchange: binance
envVarPrefix: binance
# futures: true
exchangeStrategies:
- on: binance
factorzoo:
symbol: BTCUSDT
interval: 12h # T:20/12h
quantity: 0.95
backtest:
sessions:
- binance
# for testing max draw down (MDD) at 03-12
# see here for more details
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2022-03-15"
endTime: "2022-04-13"
symbols:
- BTCUSDT
account:
binance:
balances:
BTC: 1.0
USDT: 45_000.0

1
go.mod
View File

@ -88,6 +88,7 @@ require (
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/sajari/regression v1.0.1 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/spf13/afero v1.5.1 // indirect

2
go.sum
View File

@ -412,6 +412,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sajari/regression v1.0.1 h1:iTVc6ZACGCkoXC+8NdqH5tIreslDTT/bXxT6OmHR5PE=
github.com/sajari/regression v1.0.1/go.mod h1:NeG/XTW1lYfGY7YV/Z0nYDV/RGh3wxwd1yW46835flM=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=

View File

@ -6,6 +6,8 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/bollmaker"
_ "github.com/c9s/bbgo/pkg/strategy/emastop"
_ "github.com/c9s/bbgo/pkg/strategy/etf"
_ "github.com/c9s/bbgo/pkg/strategy/ewoDgtrd"
_ "github.com/c9s/bbgo/pkg/strategy/factorzoo"
_ "github.com/c9s/bbgo/pkg/strategy/flashcrash"
_ "github.com/c9s/bbgo/pkg/strategy/funding"
_ "github.com/c9s/bbgo/pkg/strategy/grid"
@ -23,5 +25,4 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/xmaker"
_ "github.com/c9s/bbgo/pkg/strategy/xnav"
_ "github.com/c9s/bbgo/pkg/strategy/xpuremaker"
_ "github.com/c9s/bbgo/pkg/strategy/ewoDgtrd"
)

View File

@ -0,0 +1,103 @@
package factorzoo
import (
"fmt"
"math"
"time"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
var zeroTime time.Time
type KLineValueMapper func(k types.KLine) float64
//go:generate callbackgen -type Correlation
type Correlation struct {
types.IntervalWindow
Values types.Float64Slice
EndTime time.Time
UpdateCallbacks []func(value float64)
}
func (inc *Correlation) Last() float64 {
if len(inc.Values) == 0 {
return 0.0
}
return inc.Values[len(inc.Values)-1]
}
func (inc *Correlation) calculateAndUpdate(klines []types.KLine) {
if len(klines) < inc.Window {
return
}
var end = len(klines) - 1
var lastKLine = klines[end]
if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) {
return
}
var recentT = klines[end-(inc.Window-1) : end+1]
correlation, err := calculateCORRELATION(recentT, inc.Window, KLineAmplitudeMapper, indicator.KLineVolumeMapper)
if err != nil {
log.WithError(err).Error("can not calculate correlation")
return
}
inc.Values.Push(correlation)
if len(inc.Values) > indicator.MaxNumOfVOL {
inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:]
}
inc.EndTime = klines[end].GetEndTime().Time()
inc.EmitUpdate(correlation)
}
func (inc *Correlation) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}
inc.calculateAndUpdate(window)
}
func (inc *Correlation) Bind(updater indicator.KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
func calculateCORRELATION(klines []types.KLine, window int, valA KLineValueMapper, valB KLineValueMapper) (float64, error) {
length := len(klines)
if length == 0 || length < window {
return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window)
}
sumA, sumB, sumAB, squareSumA, squareSumB := 0., 0., 0., 0., 0.
for _, k := range klines {
// sum of elements of array A
sumA += valA(k)
// sum of elements of array B
sumB += valB(k)
// sum of A[i] * B[i].
sumAB = sumAB + valA(k)*valB(k)
// sum of square of array elements.
squareSumA = squareSumA + valA(k)*valA(k)
squareSumB = squareSumB + valB(k)*valB(k)
}
// use formula for calculating correlation coefficient.
corr := (float64(window)*sumAB - sumA*sumB) /
math.Sqrt((float64(window)*squareSumA-sumA*sumA)*(float64(window)*squareSumB-sumB*sumB))
return corr, nil
}
func KLineAmplitudeMapper(k types.KLine) float64 {
return k.High.Div(k.Low).Float64()
}

View File

@ -0,0 +1,15 @@
// Code generated by "callbackgen -type Correlation"; DO NOT EDIT.
package factorzoo
import ()
func (inc *Correlation) OnUpdate(cb func(value float64)) {
inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb)
}
func (inc *Correlation) EmitUpdate(value float64) {
for _, cb := range inc.UpdateCallbacks {
cb(value)
}
}

View File

@ -0,0 +1,247 @@
package factorzoo
import (
"context"
"fmt"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/sajari/regression"
"github.com/sirupsen/logrus"
)
const ID = "factorzoo"
var three = fixedpoint.NewFromInt(3)
var log = logrus.WithField("strategy", ID)
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type IntervalWindowSetting struct {
types.IntervalWindow
}
type Strategy struct {
Symbol string `json:"symbol"`
Market types.Market
Interval types.Interval `json:"interval"`
Quantity fixedpoint.Value `json:"quantity"`
Position *types.Position `json:"position,omitempty"`
activeMakerOrders *bbgo.LocalActiveOrderBook
orderStore *bbgo.OrderStore
tradeCollector *bbgo.TradeCollector
session *bbgo.ExchangeSession
book *types.StreamOrderBook
prevClose fixedpoint.Value
pvDivergenceSetting *IntervalWindowSetting `json:"pvDivergence"`
pvDivergence *Correlation
Ret []float64
Alpha [][]float64
T int64
prevER fixedpoint.Value
}
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
log.Infof("subscribe %s", s.Symbol)
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()})
}
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
base := s.Position.GetBase()
if base.IsZero() {
return fmt.Errorf("no opened %s position", s.Position.Symbol)
}
// make it negative
quantity := base.Mul(percentage).Abs()
side := types.SideTypeBuy
if base.Sign() > 0 {
side = types.SideTypeSell
}
if quantity.Compare(s.Market.MinQuantity) < 0 {
return fmt.Errorf("order quantity %v is too small, less than %v", quantity, s.Market.MinQuantity)
}
submitOrder := types.SubmitOrder{
Symbol: s.Symbol,
Side: side,
Type: types.OrderTypeMarket,
Quantity: quantity,
Market: s.Market,
}
//s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder)
createdOrders, err := s.session.Exchange.SubmitOrders(ctx, submitOrder)
if err != nil {
log.WithError(err).Errorf("can not place position close order")
}
s.orderStore.Add(createdOrders...)
s.activeMakerOrders.Add(createdOrders...)
return err
}
func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor, er fixedpoint.Value) {
//if s.prevER.Sign() < 0 && er.Sign() > 0 {
if er.Sign() >= 0 {
submitOrder := types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeMarket,
Quantity: s.Quantity, //er.Abs().Mul(fixedpoint.NewFromInt(20)),
}
createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder)
if err != nil {
log.WithError(err).Errorf("can not place orders")
}
s.orderStore.Add(createdOrders...)
s.activeMakerOrders.Add(createdOrders...)
//} else if s.prevER.Sign() > 0 && er.Sign() < 0 {
} else {
submitOrder := types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeMarket,
Quantity: s.Quantity, //er.Abs().Mul(fixedpoint.NewFromInt(20)),
}
createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder)
if err != nil {
log.WithError(err).Errorf("can not place orders")
}
s.orderStore.Add(createdOrders...)
s.activeMakerOrders.Add(createdOrders...)
}
s.prevER = er
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
// initial required information
s.session = session
s.prevClose = fixedpoint.Zero
// first we need to get market data store(cached market data) from the exchange session
st, _ := session.MarketDataStore(s.Symbol)
// setup the time frame size
iw := types.IntervalWindow{Window: 50, Interval: s.Interval}
// construct CORR indicator
s.pvDivergence = &Correlation{IntervalWindow: iw}
// bind indicator to the data store, so that our callback could be triggered
s.pvDivergence.Bind(st)
//s.pvDivergence.OnUpdate(func(corr float64) {
// //fmt.Printf("now we've got corr: %f\n", corr)
//})
s.Alpha = [][]float64{{}, {}, {}, {}, {}}
s.Ret = []float64{}
//thetas := []float64{0, 0, 0, 0}
preCompute := 0
s.activeMakerOrders = bbgo.NewLocalActiveOrderBook(s.Symbol)
s.activeMakerOrders.BindStream(session.UserDataStream)
s.orderStore = bbgo.NewOrderStore(s.Symbol)
s.orderStore.BindStream(session.UserDataStream)
if s.Position == nil {
s.Position = types.NewPositionFromMarket(s.Market)
}
s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore)
s.tradeCollector.BindStream(session.UserDataStream)
session.UserDataStream.OnStart(func() {
log.Infof("connected")
})
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
return
}
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
// amplitude volume divergence
corr := fixedpoint.NewFromFloat(s.pvDivergence.Last()).Neg()
// price mean reversion
rev := fixedpoint.NewFromInt(1).Div(kline.Close)
// alpha150 from GTJA's 191 paper
a150 := kline.High.Add(kline.Low).Add(kline.Close).Div(three).Mul(kline.Volume)
// momentum from WQ's 101 paper
mom := fixedpoint.One.Sub(kline.Open.Div(kline.Close)).Mul(fixedpoint.NegOne)
// opening gap
ogap := kline.Open.Div(s.prevClose)
log.Infof("corr: %f, rev: %f, a150: %f, mom: %f, ogap: %f", corr.Float64(), rev.Float64(), a150.Float64(), mom.Float64(), ogap.Float64())
s.Alpha[0] = append(s.Alpha[0], corr.Float64())
s.Alpha[1] = append(s.Alpha[1], rev.Float64())
s.Alpha[2] = append(s.Alpha[2], a150.Float64())
s.Alpha[3] = append(s.Alpha[3], mom.Float64())
s.Alpha[4] = append(s.Alpha[4], ogap.Float64())
//s.Alpha[5] = append(s.Alpha[4], 1.0) // constant
ret := kline.Close.Sub(s.prevClose).Div(s.prevClose).Float64()
s.Ret = append(s.Ret, ret)
log.Infof("Current Return: %f", s.Ret[len(s.Ret)-1])
// accumulate enough data for cross-sectional regression, not time-series regression
s.T = 20
if preCompute < int(s.T)+1 {
preCompute++
} else {
s.ClosePosition(ctx, fixedpoint.One)
s.tradeCollector.Process()
// rolling regression for last 20 interval alphas
r := new(regression.Regression)
r.SetObserved("Return Rate Per Timeframe")
r.SetVar(0, "Corr")
r.SetVar(1, "Rev")
r.SetVar(2, "A150")
r.SetVar(3, "Mom")
r.SetVar(4, "OGap")
var rdp regression.DataPoints
for i := 1; i <= int(s.T); i++ {
// alphas[t-1], previous alphas, dot not take current alpha into account, will cause look-ahead bias
as := []float64{s.Alpha[0][len(s.Alpha[0])-(i+2)], s.Alpha[1][len(s.Alpha[1])-(i+2)], s.Alpha[2][len(s.Alpha[2])-(i+2)], s.Alpha[3][len(s.Alpha[3])-(i+2)], s.Alpha[4][len(s.Alpha[4])-(i+2)]}
// alphas[t], current return rate
rt := s.Ret[len(s.Ret)-(i+1)]
rdp = append(rdp, regression.DataPoint(rt, as))
}
r.Train(rdp...)
r.Run()
fmt.Printf("Regression formula:\n%v\n", r.Formula)
//prediction := r.Coeff(0)*corr.Float64() + r.Coeff(1)*rev.Float64() + r.Coeff(2)*factorzoo.Float64() + r.Coeff(3)*mom.Float64() + r.Coeff(4)
prediction, _ := r.Predict([]float64{corr.Float64(), rev.Float64(), a150.Float64(), mom.Float64(), ogap.Float64()})
log.Infof("Predicted Return: %f", prediction)
s.placeOrders(ctx, orderExecutor, fixedpoint.NewFromFloat(prediction))
s.tradeCollector.Process()
}
s.prevClose = kline.Close
})
return nil
}