diff --git a/config/factorzoo.yaml b/config/factorzoo.yaml index df83d50ee..1de7a576e 100644 --- a/config/factorzoo.yaml +++ b/config/factorzoo.yaml @@ -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 diff --git a/pkg/strategy/factorzoo/correlation.go b/pkg/strategy/factorzoo/correlation.go deleted file mode 100644 index 7c094d58b..000000000 --- a/pkg/strategy/factorzoo/correlation.go +++ /dev/null @@ -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() -} diff --git a/pkg/strategy/factorzoo/correlation_callbacks.go b/pkg/strategy/factorzoo/correlation_callbacks.go deleted file mode 100644 index 2ef6323ea..000000000 --- a/pkg/strategy/factorzoo/correlation_callbacks.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/strategy/factorzoo/factors/mom_callbacks.go b/pkg/strategy/factorzoo/factors/mom_callbacks.go new file mode 100644 index 000000000..055aa51c3 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/mom_callbacks.go @@ -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) + } +} diff --git a/pkg/strategy/factorzoo/factors/momentum.go b/pkg/strategy/factorzoo/factors/momentum.go new file mode 100644 index 000000000..55d6e94d8 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/momentum.go @@ -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 +} diff --git a/pkg/strategy/factorzoo/factors/pmr_callbacks.go b/pkg/strategy/factorzoo/factors/pmr_callbacks.go new file mode 100644 index 000000000..a90c99ac2 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/pmr_callbacks.go @@ -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) + } +} diff --git a/pkg/strategy/factorzoo/factors/price_mean_reversion.go b/pkg/strategy/factorzoo/factors/price_mean_reversion.go new file mode 100644 index 000000000..9b1ad8846 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/price_mean_reversion.go @@ -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 +} diff --git a/pkg/strategy/factorzoo/factors/price_volume_divergence.go b/pkg/strategy/factorzoo/factors/price_volume_divergence.go new file mode 100644 index 000000000..033123b5f --- /dev/null +++ b/pkg/strategy/factorzoo/factors/price_volume_divergence.go @@ -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 +} diff --git a/pkg/strategy/factorzoo/factors/pvd_callbacks.go b/pkg/strategy/factorzoo/factors/pvd_callbacks.go new file mode 100644 index 000000000..f8dead4a8 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/pvd_callbacks.go @@ -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) + } +} diff --git a/pkg/strategy/factorzoo/factors/return_rate.go b/pkg/strategy/factorzoo/factors/return_rate.go new file mode 100644 index 000000000..29efc9d5e --- /dev/null +++ b/pkg/strategy/factorzoo/factors/return_rate.go @@ -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 +//} diff --git a/pkg/strategy/factorzoo/factors/rr_callbacks.go b/pkg/strategy/factorzoo/factors/rr_callbacks.go new file mode 100644 index 000000000..301837d57 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/rr_callbacks.go @@ -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) + } +} diff --git a/pkg/strategy/factorzoo/factors/vmom_callbacks.go b/pkg/strategy/factorzoo/factors/vmom_callbacks.go new file mode 100644 index 000000000..9ef858e38 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/vmom_callbacks.go @@ -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) + } +} diff --git a/pkg/strategy/factorzoo/factors/volume_momentum.go b/pkg/strategy/factorzoo/factors/volume_momentum.go new file mode 100644 index 000000000..ac2c65819 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/volume_momentum.go @@ -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 +} diff --git a/pkg/strategy/factorzoo/linear_regression.go b/pkg/strategy/factorzoo/linear_regression.go new file mode 100644 index 000000000..d3986a34e --- /dev/null +++ b/pkg/strategy/factorzoo/linear_regression.go @@ -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 +} diff --git a/pkg/strategy/factorzoo/strategy.go b/pkg/strategy/factorzoo/strategy.go index 5ed9d7a84..d038a7e16 100644 --- a/pkg/strategy/factorzoo/strategy.go +++ b/pkg/strategy/factorzoo/strategy.go @@ -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 { - Symbol string `json:"symbol"` - Market types.Market - Interval types.Interval `json:"interval"` - Quantity fixedpoint.Value `json:"quantity"` + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market - 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"` - session *bbgo.ExchangeSession - book *types.StreamOrderBook + activeOrders *bbgo.ActiveOrderBook - prevClose fixedpoint.Value + Linear *Linear `json:"linear"` - pvDivergenceSetting *IntervalWindowSetting `json:"pvDivergence"` - pvDivergence *Correlation + ExitMethods bbgo.ExitMethodSet `json:"exits"` - Ret []float64 - Alpha [][]float64 + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor - T int64 - prevER fixedpoint.Value + // StrategyController + bbgo.StrategyController +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Linear.Interval}) + + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } + + 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 + s.OnEmergencyStop(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + //_ = s.ClosePosition(ctx, fixedpoint.One) + }) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + // initial required information + s.session = session - if kline.Symbol != s.Symbol || kline.Interval != s.Interval { - return - } + 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) - if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { - log.WithError(err).Errorf("graceful cancel order error") - } + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } - // 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) + if s.Linear != nil { + s.Linear.Bind(session, s.orderExecutor) + } - 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 + 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