mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-27 01:05:15 +00:00
280 lines
8.0 KiB
Go
280 lines
8.0 KiB
Go
package factorzoo
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/sajari/regression"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/c9s/bbgo/pkg/bbgo"
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
"github.com/c9s/bbgo/pkg/indicator"
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
)
|
|
|
|
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.ActiveOrderBook
|
|
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) InstanceID() string {
|
|
return fmt.Sprintf("%s:%s", ID, s.Symbol)
|
|
}
|
|
|
|
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
|
log.Infof("subscribe %s", s.Symbol)
|
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
|
}
|
|
|
|
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)
|
|
// })
|
|
windowSize := 360/s.Interval.Minutes()
|
|
if windowSize == 0 {
|
|
windowSize = 3
|
|
}
|
|
drift := &indicator.Drift{IntervalWindow: types.IntervalWindow{Window: windowSize, Interval: s.Interval}}
|
|
drift.Bind(st)
|
|
|
|
s.Alpha = [][]float64{{}, {}, {}, {}, {}, {}}
|
|
s.Ret = []float64{}
|
|
// thetas := []float64{0, 0, 0, 0}
|
|
preCompute := 0
|
|
|
|
s.activeMakerOrders = bbgo.NewActiveOrderBook(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")
|
|
})
|
|
|
|
s.T = 20
|
|
|
|
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)
|
|
|
|
driftVal := drift.Last()
|
|
|
|
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[5], driftVal)
|
|
|
|
// 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
|
|
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")
|
|
r.SetVar(5, "Drift")
|
|
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)],
|
|
s.Alpha[5][len(s.Alpha[5])-(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(),
|
|
driftVal,
|
|
})
|
|
log.Infof("Predicted Return: %f", prediction)
|
|
|
|
s.placeOrders(ctx, orderExecutor, fixedpoint.NewFromFloat(prediction))
|
|
s.tradeCollector.Process()
|
|
}
|
|
|
|
s.prevClose = kline.Close
|
|
|
|
})
|
|
|
|
return nil
|
|
}
|