mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
Merge pull request #867 from austin362667/austin362667/factorzoo
strategy: factorzoo: upgrade indicators and add comments
This commit is contained in:
commit
971db3399f
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
15
pkg/strategy/factorzoo/factors/mom_callbacks.go
Normal file
15
pkg/strategy/factorzoo/factors/mom_callbacks.go
Normal 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)
|
||||
}
|
||||
}
|
113
pkg/strategy/factorzoo/factors/momentum.go
Normal file
113
pkg/strategy/factorzoo/factors/momentum.go
Normal 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
|
||||
}
|
15
pkg/strategy/factorzoo/factors/pmr_callbacks.go
Normal file
15
pkg/strategy/factorzoo/factors/pmr_callbacks.go
Normal 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)
|
||||
}
|
||||
}
|
108
pkg/strategy/factorzoo/factors/price_mean_reversion.go
Normal file
108
pkg/strategy/factorzoo/factors/price_mean_reversion.go
Normal 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
|
||||
}
|
115
pkg/strategy/factorzoo/factors/price_volume_divergence.go
Normal file
115
pkg/strategy/factorzoo/factors/price_volume_divergence.go
Normal 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
|
||||
}
|
15
pkg/strategy/factorzoo/factors/pvd_callbacks.go
Normal file
15
pkg/strategy/factorzoo/factors/pvd_callbacks.go
Normal 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)
|
||||
}
|
||||
}
|
112
pkg/strategy/factorzoo/factors/return_rate.go
Normal file
112
pkg/strategy/factorzoo/factors/return_rate.go
Normal 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
|
||||
//}
|
15
pkg/strategy/factorzoo/factors/rr_callbacks.go
Normal file
15
pkg/strategy/factorzoo/factors/rr_callbacks.go
Normal 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)
|
||||
}
|
||||
}
|
15
pkg/strategy/factorzoo/factors/vmom_callbacks.go
Normal file
15
pkg/strategy/factorzoo/factors/vmom_callbacks.go
Normal 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)
|
||||
}
|
||||
}
|
115
pkg/strategy/factorzoo/factors/volume_momentum.go
Normal file
115
pkg/strategy/factorzoo/factors/volume_momentum.go
Normal 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
|
||||
}
|
193
pkg/strategy/factorzoo/linear_regression.go
Normal file
193
pkg/strategy/factorzoo/linear_regression.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user