Merge pull request #867 from austin362667/austin362667/factorzoo

strategy: factorzoo:  upgrade indicators and add comments
This commit is contained in:
Yo-An Lin 2022-08-09 11:45:06 +08:00 committed by GitHub
commit 971db3399f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 928 additions and 348 deletions

View File

@ -2,29 +2,43 @@ sessions:
binance:
exchange: binance
envVarPrefix: binance
# futures: true
exchangeStrategies:
- on: binance
factorzoo:
symbol: BTCUSDT
interval: 12h # T:20/12h
quantity: 0.95
symbol: BTCBUSD
linear:
enabled: true
interval: 1d
quantity: 1.0
window: 5
exits:
- trailingStop:
callbackRate: 1%
activationRatio: 1%
closePosition: 100%
minProfit: 15%
interval: 1m
side: buy
- trailingStop:
callbackRate: 1%
activationRatio: 1%
closePosition: 100%
minProfit: 15%
interval: 1m
side: sell
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"
startTime: "2021-01-01"
endTime: "2022-08-31"
symbols:
- BTCUSDT
- BTCBUSD
accounts:
binance:
balances:
BTC: 1.0
USDT: 45_000.0
BUSD: 40_000.0

View File

@ -1,103 +0,0 @@
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

@ -1,15 +0,0 @@
// 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,15 @@
// Code generated by "callbackgen -type MOM"; DO NOT EDIT.
package factorzoo
import ()
func (inc *MOM) OnUpdate(cb func(val float64)) {
inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb)
}
func (inc *MOM) EmitUpdate(val float64) {
for _, cb := range inc.UpdateCallbacks {
cb(val)
}
}

View File

@ -0,0 +1,113 @@
package factorzoo
import (
"fmt"
"time"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
// gap jump momentum
// if the gap between current open price and previous close price gets larger
// meaning an opening price jump was happened, the larger momentum we get is our alpha, MOM
//go:generate callbackgen -type MOM
type MOM struct {
types.SeriesBase
types.IntervalWindow
// Values
Values types.Float64Slice
LastValue float64
opens *types.Queue
closes *types.Queue
EndTime time.Time
UpdateCallbacks []func(val float64)
}
func (inc *MOM) Index(i int) float64 {
if inc.Values == nil {
return 0
}
return inc.Values.Index(i)
}
func (inc *MOM) Last() float64 {
if inc.Values.Length() == 0 {
return 0
}
return inc.Values.Last()
}
func (inc *MOM) Length() int {
if inc.Values == nil {
return 0
}
return inc.Values.Length()
}
//var _ types.SeriesExtend = &MOM{}
func (inc *MOM) Update(open, close float64) {
if inc.SeriesBase.Series == nil {
inc.SeriesBase.Series = inc
inc.opens = types.NewQueue(inc.Window)
inc.closes = types.NewQueue(inc.Window + 1)
}
inc.opens.Update(open)
inc.closes.Update(close)
if inc.opens.Length() >= inc.Window && inc.closes.Length() >= inc.Window {
gap := inc.opens.Last()/inc.closes.Index(1) - 1
inc.Values.Push(gap)
}
}
func (inc *MOM) CalculateAndUpdate(allKLines []types.KLine) {
if len(inc.Values) == 0 {
for _, k := range allKLines {
inc.PushK(k)
}
inc.EmitUpdate(inc.Last())
} else {
k := allKLines[len(allKLines)-1]
inc.PushK(k)
inc.EmitUpdate(inc.Last())
}
}
func (inc *MOM) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}
inc.CalculateAndUpdate(window)
}
func (inc *MOM) Bind(updater indicator.KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
func (inc *MOM) PushK(k types.KLine) {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
return
}
inc.Update(k.Open.Float64(), k.Close.Float64())
inc.EndTime = k.EndTime.Time()
inc.EmitUpdate(inc.Last())
}
func calculateMomentum(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)
}
momentum := (1 - valA(klines[length-1])/valB(klines[length-1])) * -1
return momentum, nil
}

View File

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

View File

@ -0,0 +1,108 @@
package factorzoo
import (
"time"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
"gonum.org/v1/gonum/stat"
)
// price mean reversion
// assume that the quotient of SMA over close price will dynamically revert into one.
// so this fraction value is our alpha, PMR
//go:generate callbackgen -type PMR
type PMR struct {
types.IntervalWindow
types.SeriesBase
Values types.Float64Slice
SMA *indicator.SMA
EndTime time.Time
updateCallbacks []func(value float64)
}
var _ types.SeriesExtend = &PMR{}
func (inc *PMR) Update(price float64) {
if inc.SeriesBase.Series == nil {
inc.SeriesBase.Series = inc
inc.SMA = &indicator.SMA{IntervalWindow: inc.IntervalWindow}
}
inc.SMA.Update(price)
if inc.SMA.Length() >= inc.Window {
reversion := inc.SMA.Last() / price
inc.Values.Push(reversion)
}
}
func (inc *PMR) Last() float64 {
if len(inc.Values) == 0 {
return 0
}
return inc.Values[len(inc.Values)-1]
}
func (inc *PMR) Index(i int) float64 {
if i >= len(inc.Values) {
return 0
}
return inc.Values[len(inc.Values)-1-i]
}
func (inc *PMR) Length() int {
return len(inc.Values)
}
func (inc *PMR) CalculateAndUpdate(allKLines []types.KLine) {
if len(inc.Values) == 0 {
for _, k := range allKLines {
inc.PushK(k)
}
inc.EmitUpdate(inc.Last())
} else {
k := allKLines[len(allKLines)-1]
inc.PushK(k)
inc.EmitUpdate(inc.Last())
}
}
func (inc *PMR) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}
inc.CalculateAndUpdate(window)
}
func (inc *PMR) Bind(updater indicator.KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
func (inc *PMR) PushK(k types.KLine) {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
return
}
inc.Update(indicator.KLineClosePriceMapper(k))
inc.EndTime = k.EndTime.Time()
inc.EmitUpdate(inc.Last())
}
func CalculateKLinesPMR(allKLines []types.KLine, window int) float64 {
return pmr(indicator.MapKLinePrice(allKLines, indicator.KLineClosePriceMapper), window)
}
func pmr(prices []float64, window int) float64 {
var end = len(prices) - 1
if end == 0 {
return prices[0]
}
reversion := -stat.Mean(prices[end-window:end], nil) / prices[end]
return reversion
}

View File

@ -0,0 +1,115 @@
package factorzoo
import (
"time"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
"gonum.org/v1/gonum/stat"
)
// price volume divergence
// if the correlation of two time series gets smaller, they are diverging.
// so the negative value of the correlation of close price and volume is our alpha, PVD
var zeroTime time.Time
type KLineValueMapper func(k types.KLine) float64
//go:generate callbackgen -type PVD
type PVD struct {
types.IntervalWindow
types.SeriesBase
Values types.Float64Slice
Prices *types.Queue
Volumes *types.Queue
EndTime time.Time
updateCallbacks []func(value float64)
}
var _ types.SeriesExtend = &PVD{}
func (inc *PVD) Update(price float64, volume float64) {
if inc.SeriesBase.Series == nil {
inc.SeriesBase.Series = inc
inc.Prices = types.NewQueue(inc.Window)
inc.Volumes = types.NewQueue(inc.Window)
}
inc.Prices.Update(price)
inc.Volumes.Update(volume)
if inc.Prices.Length() >= inc.Window && inc.Volumes.Length() >= inc.Window {
divergence := -types.Correlation(inc.Prices, inc.Volumes, inc.Window)
inc.Values.Push(divergence)
}
}
func (inc *PVD) Last() float64 {
if len(inc.Values) == 0 {
return 0
}
return inc.Values[len(inc.Values)-1]
}
func (inc *PVD) Index(i int) float64 {
if i >= len(inc.Values) {
return 0
}
return inc.Values[len(inc.Values)-1-i]
}
func (inc *PVD) Length() int {
return len(inc.Values)
}
func (inc *PVD) CalculateAndUpdate(allKLines []types.KLine) {
if len(inc.Values) == 0 {
for _, k := range allKLines {
inc.PushK(k)
}
inc.EmitUpdate(inc.Last())
} else {
k := allKLines[len(allKLines)-1]
inc.PushK(k)
inc.EmitUpdate(inc.Last())
}
}
func (inc *PVD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}
inc.CalculateAndUpdate(window)
}
func (inc *PVD) Bind(updater indicator.KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
func (inc *PVD) PushK(k types.KLine) {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
return
}
inc.Update(indicator.KLineClosePriceMapper(k), indicator.KLineVolumeMapper(k))
inc.EndTime = k.EndTime.Time()
inc.EmitUpdate(inc.Last())
}
func CalculateKLinesPVD(allKLines []types.KLine, window int) float64 {
return pvd(indicator.MapKLinePrice(allKLines, indicator.KLineClosePriceMapper), indicator.MapKLinePrice(allKLines, indicator.KLineVolumeMapper), window)
}
func pvd(prices []float64, volumes []float64, window int) float64 {
var end = len(prices) - 1
if end == 0 {
return prices[0]
}
divergence := -stat.Correlation(prices[end-window:end], volumes[end-window:end], nil)
return divergence
}

View File

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

View File

@ -0,0 +1,112 @@
package factorzoo
import (
"time"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
// simply internal return rate over certain timeframe(interval)
//go:generate callbackgen -type RR
type RR struct {
types.IntervalWindow
types.SeriesBase
prices *types.Queue
Values types.Float64Slice
EndTime time.Time
updateCallbacks []func(value float64)
}
var _ types.SeriesExtend = &RR{}
func (inc *RR) Update(price float64) {
if inc.SeriesBase.Series == nil {
inc.SeriesBase.Series = inc
inc.prices = types.NewQueue(inc.Window)
}
inc.prices.Update(price)
irr := inc.prices.Last()/inc.prices.Index(1) - 1
inc.Values.Push(irr)
}
func (inc *RR) Last() float64 {
if len(inc.Values) == 0 {
return 0
}
return inc.Values[len(inc.Values)-1]
}
func (inc *RR) Index(i int) float64 {
if i >= len(inc.Values) {
return 0
}
return inc.Values[len(inc.Values)-1-i]
}
func (inc *RR) Length() int {
return len(inc.Values)
}
func (inc *RR) CalculateAndUpdate(allKLines []types.KLine) {
if len(inc.Values) == 0 {
for _, k := range allKLines {
inc.PushK(k)
}
inc.EmitUpdate(inc.Last())
} else {
k := allKLines[len(allKLines)-1]
inc.PushK(k)
inc.EmitUpdate(inc.Last())
}
}
func (inc *RR) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}
inc.CalculateAndUpdate(window)
}
func (inc *RR) Bind(updater indicator.KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
func (inc *RR) BindK(target indicator.KLineClosedEmitter, symbol string, interval types.Interval) {
target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK))
}
func (inc *RR) PushK(k types.KLine) {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
return
}
inc.Update(indicator.KLineClosePriceMapper(k))
inc.EndTime = k.EndTime.Time()
inc.EmitUpdate(inc.Last())
}
func (inc *RR) LoadK(allKLines []types.KLine) {
for _, k := range allKLines {
inc.PushK(k)
}
inc.EmitUpdate(inc.Last())
}
//func calculateReturn(klines []types.KLine, window int, val 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)
// }
//
// rate := val(klines[length-1])/val(klines[length-2]) - 1
//
// return rate, nil
//}

View File

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

View File

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

View File

@ -0,0 +1,115 @@
package factorzoo
import (
"fmt"
"time"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
// quarterly volume momentum
// assume that the quotient of volume SMA over latest volume will dynamically revert into one.
// so this fraction value is our alpha, PMR
//go:generate callbackgen -type VMOM
type VMOM struct {
types.SeriesBase
types.IntervalWindow
// Values
Values types.Float64Slice
LastValue float64
volumes *types.Queue
EndTime time.Time
UpdateCallbacks []func(val float64)
}
func (inc *VMOM) Index(i int) float64 {
if inc.Values == nil {
return 0
}
return inc.Values.Index(i)
}
func (inc *VMOM) Last() float64 {
if inc.Values.Length() == 0 {
return 0
}
return inc.Values.Last()
}
func (inc *VMOM) Length() int {
if inc.Values == nil {
return 0
}
return inc.Values.Length()
}
var _ types.SeriesExtend = &VMOM{}
func (inc *VMOM) Update(volume float64) {
if inc.SeriesBase.Series == nil {
inc.SeriesBase.Series = inc
inc.volumes = types.NewQueue(inc.Window)
}
inc.volumes.Update(volume)
if inc.volumes.Length() >= inc.Window {
v := inc.volumes.Last() / inc.volumes.Mean()
inc.Values.Push(v)
}
}
func (inc *VMOM) CalculateAndUpdate(allKLines []types.KLine) {
if len(inc.Values) == 0 {
for _, k := range allKLines {
inc.PushK(k)
}
inc.EmitUpdate(inc.Last())
} else {
k := allKLines[len(allKLines)-1]
inc.PushK(k)
inc.EmitUpdate(inc.Last())
}
}
func (inc *VMOM) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}
inc.CalculateAndUpdate(window)
}
func (inc *VMOM) Bind(updater indicator.KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
func (inc *VMOM) PushK(k types.KLine) {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
return
}
inc.Update(k.Volume.Float64())
inc.EndTime = k.EndTime.Time()
inc.EmitUpdate(inc.Last())
}
func calculateVolumeMomentum(klines []types.KLine, window int, valV KLineValueMapper, valP 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)
}
vma := 0.
for _, p := range klines[length-window : length-1] {
vma += valV(p)
}
vma /= float64(window)
momentum := valV(klines[length-1]) / vma //* (valP(klines[length-1-2]) / valP(klines[length-1]))
return momentum, nil
}

View File

@ -0,0 +1,193 @@
package factorzoo
import (
"context"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/strategy/factorzoo/factors"
"github.com/c9s/bbgo/pkg/types"
)
type Linear struct {
Symbol string
Market types.Market `json:"-"`
types.IntervalWindow
// MarketOrder is the option to enable market order short.
MarketOrder bool `json:"marketOrder"`
Quantity fixedpoint.Value `json:"quantity"`
StopEMARange fixedpoint.Value `json:"stopEMARange"`
StopEMA *types.IntervalWindow `json:"stopEMA"`
// Xs (input), factors & indicators
divergence *factorzoo.PVD // price volume divergence
reversion *factorzoo.PMR // price mean reversion
momentum *factorzoo.MOM // price momentum from paper, alpha 101
drift *indicator.Drift // GBM
volume *factorzoo.VMOM // quarterly volume momentum
// Y (output), internal rate of return
irr *factorzoo.RR
orderExecutor *bbgo.GeneralOrderExecutor
session *bbgo.ExchangeSession
activeOrders *bbgo.ActiveOrderBook
bbgo.QuantityOrAmount
}
func (s *Linear) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
}
func (s *Linear) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
s.session = session
s.orderExecutor = orderExecutor
position := orderExecutor.Position()
symbol := position.Symbol
store, _ := session.MarketDataStore(symbol)
// initialize factor indicators
s.divergence = &factorzoo.PVD{IntervalWindow: types.IntervalWindow{Window: 60, Interval: s.Interval}}
s.divergence.Bind(store)
s.reversion = &factorzoo.PMR{IntervalWindow: types.IntervalWindow{Window: 60, Interval: s.Interval}}
s.reversion.Bind(store)
s.drift = &indicator.Drift{IntervalWindow: types.IntervalWindow{Window: 7, Interval: s.Interval}}
s.drift.Bind(store)
s.momentum = &factorzoo.MOM{IntervalWindow: types.IntervalWindow{Window: 1, Interval: s.Interval}}
s.momentum.Bind(store)
s.volume = &factorzoo.VMOM{IntervalWindow: types.IntervalWindow{Window: 90, Interval: s.Interval}}
s.volume.Bind(store)
s.irr = &factorzoo.RR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}}
s.irr.Bind(store)
predLst := types.NewQueue(s.Window)
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) {
ctx := context.Background()
// graceful cancel all active orders
_ = orderExecutor.GracefulCancel(ctx)
// take past window days' values to predict future return
// (e.g., 5 here in default configuration file)
a := []types.Float64Slice{
s.divergence.Values[len(s.divergence.Values)-s.Window-2 : len(s.divergence.Values)-2],
s.reversion.Values[len(s.reversion.Values)-s.Window-2 : len(s.reversion.Values)-2],
s.drift.Values[len(s.drift.Values)-s.Window-2 : len(s.drift.Values)-2],
s.momentum.Values[len(s.momentum.Values)-s.Window-2 : len(s.momentum.Values)-2],
s.volume.Values[len(s.volume.Values)-s.Window-2 : len(s.volume.Values)-2],
}
// e.g., s.window is 5
// factors array from day -4 to day 0, [[0.1, 0.2, 0.35, 0.3 , 0.25], [1.1, -0.2, 1.35, -0.3 , -0.25], ...]
// the binary(+/-) daily return rate from day -3 to day 1, [0, 1, 1, 0, 0]
// then we take the latest available factors array into linear regression model
b := []types.Float64Slice{filter(s.irr.Values[len(s.irr.Values)-s.Window-1:len(s.irr.Values)-1], binary)}
var x []types.Series
var y []types.Series
x = append(x, &a[0])
x = append(x, &a[1])
x = append(x, &a[2])
x = append(x, &a[3])
x = append(x, &a[4])
//x = append(x, &a[5])
y = append(y, &b[0])
model := types.LogisticRegression(x, y[0], s.Window, 8000, 0.0001)
// use the last value from indicators, or the SeriesExtends' predict function. (e.g., look back: 5)
input := []float64{
s.divergence.Last(),
s.reversion.Last(),
s.drift.Last(),
s.momentum.Last(),
s.volume.Last(),
}
pred := model.Predict(input)
predLst.Update(pred)
qty := s.Quantity //s.QuantityOrAmount.CalculateQuantity(kline.Close)
// the scale of pred is from 0.0 to 1.0
// 0.5 can be used as the threshold
// we use the time-series rolling prediction values here
if pred > predLst.Mean() {
if position.IsShort() {
s.ClosePosition(ctx, one)
s.placeMarketOrder(ctx, types.SideTypeBuy, qty, symbol)
} else if position.IsClosed() {
s.placeMarketOrder(ctx, types.SideTypeBuy, qty, symbol)
}
} else if pred < predLst.Mean() {
if position.IsLong() {
s.ClosePosition(ctx, one)
s.placeMarketOrder(ctx, types.SideTypeSell, qty, symbol)
} else if position.IsClosed() {
s.placeMarketOrder(ctx, types.SideTypeSell, qty, symbol)
}
}
// pass if position is opened and not dust, and remain the same direction with alpha signal
// alpha-weighted inventory and cash
//alpha := fixedpoint.NewFromFloat(s.r1.Last())
//targetBase := s.QuantityOrAmount.CalculateQuantity(kline.Close).Mul(alpha)
////s.ClosePosition(ctx, one)
//diffQty := targetBase.Sub(position.Base)
//log.Info(alpha.Float64(), position.Base, diffQty.Float64())
//
//if diffQty.Sign() > 0 {
// s.placeMarketOrder(ctx, types.SideTypeBuy, diffQty.Abs(), symbol)
//} else if diffQty.Sign() < 0 {
// s.placeMarketOrder(ctx, types.SideTypeSell, diffQty.Abs(), symbol)
//}
}))
if !bbgo.IsBackTesting {
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
})
}
}
func (s *Linear) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
return s.orderExecutor.ClosePosition(ctx, percentage)
}
func (s *Linear) placeMarketOrder(ctx context.Context, side types.SideType, quantity fixedpoint.Value, symbol string) {
market, _ := s.session.Market(symbol)
_, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: symbol,
Market: market,
Side: side,
Type: types.OrderTypeMarket,
Quantity: quantity,
//TimeInForce: types.TimeInForceGTC,
Tag: "linear",
})
if err != nil {
log.WithError(err).Errorf("can not place market order")
}
}
func binary(val float64) float64 {
if val > 0. {
return 1.
} else {
return 0.
}
}
func filter(data []float64, f func(float64) float64) []float64 {
fltd := make([]float64, 0)
for _, e := range data {
//if f(e) >= 0. {
fltd = append(fltd, f(e))
//}
}
return fltd
}

View File

@ -3,19 +3,19 @@ package factorzoo
import (
"context"
"fmt"
"github.com/sajari/regression"
"github.com/sirupsen/logrus"
"os"
"sync"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
"github.com/sirupsen/logrus"
)
const ID = "factorzoo"
var three = fixedpoint.NewFromInt(3)
var one = fixedpoint.One
var zero = fixedpoint.Zero
var log = logrus.WithField("strategy", ID)
@ -28,30 +28,38 @@ type IntervalWindowSetting struct {
}
type Strategy struct {
Environment *bbgo.Environment
Symbol string `json:"symbol"`
Market types.Market
Interval types.Interval `json:"interval"`
Quantity fixedpoint.Value `json:"quantity"`
Position *types.Position `json:"position,omitempty"`
types.IntervalWindow
activeMakerOrders *bbgo.ActiveOrderBook
orderStore *bbgo.OrderStore
tradeCollector *bbgo.TradeCollector
// persistence fields
Position *types.Position `persistence:"position"`
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
TradeStats *types.TradeStats `persistence:"trade_stats"`
activeOrders *bbgo.ActiveOrderBook
Linear *Linear `json:"linear"`
ExitMethods bbgo.ExitMethodSet `json:"exits"`
session *bbgo.ExchangeSession
book *types.StreamOrderBook
orderExecutor *bbgo.GeneralOrderExecutor
prevClose fixedpoint.Value
// StrategyController
bbgo.StrategyController
}
pvDivergenceSetting *IntervalWindowSetting `json:"pvDivergence"`
pvDivergence *Correlation
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Linear.Interval})
Ret []float64
Alpha [][]float64
if !bbgo.IsBackTesting {
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
}
T int64
prevER fixedpoint.Value
s.ExitMethods.SetAndSubscribe(session, s)
}
func (s *Strategy) ID() string {
@ -62,217 +70,62 @@ 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)
var instanceID = s.InstanceID()
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)
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
session.UserDataStream.OnStart(func() {
log.Infof("connected")
if s.TradeStats == nil {
s.TradeStats = types.NewTradeStats(s.Symbol)
}
// StrategyController
s.Status = types.StrategyStatusRunning
s.OnSuspend(func() {
// Cancel active orders
_ = s.orderExecutor.GracefulCancel(ctx)
})
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,
s.OnEmergencyStop(func() {
// Cancel active orders
_ = s.orderExecutor.GracefulCancel(ctx)
// Close 100% position
//_ = s.ClosePosition(ctx, fixedpoint.One)
})
log.Infof("Predicted Return: %f", prediction)
s.placeOrders(ctx, orderExecutor, fixedpoint.NewFromFloat(prediction))
s.tradeCollector.Process()
// initial required information
s.session = session
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.orderExecutor.BindEnvironment(s.Environment)
s.orderExecutor.BindProfitStats(s.ProfitStats)
s.orderExecutor.BindTradeStats(s.TradeStats)
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(s)
})
s.orderExecutor.Bind()
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
for _, method := range s.ExitMethods {
method.Bind(session, s.orderExecutor)
}
s.prevClose = kline.Close
if s.Linear != nil {
s.Linear.Bind(session, s.orderExecutor)
}
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
_ = s.orderExecutor.GracefulCancel(ctx)
})
return nil