From 7de997533653ce766fe2db768a8b7220a4931c05 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 21 Oct 2022 16:14:47 +0800 Subject: [PATCH 01/37] indicator/linreg: LinReg indicator --- pkg/indicator/linreg.go | 98 +++++++++++++++++++++++++++++++ pkg/indicator/linreg_callbacks.go | 15 +++++ 2 files changed, 113 insertions(+) create mode 100644 pkg/indicator/linreg.go create mode 100644 pkg/indicator/linreg_callbacks.go diff --git a/pkg/indicator/linreg.go b/pkg/indicator/linreg.go new file mode 100644 index 000000000..e48c82a2c --- /dev/null +++ b/pkg/indicator/linreg.go @@ -0,0 +1,98 @@ +package indicator + +import ( + "github.com/sirupsen/logrus" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +var log = logrus.WithField("indicator", "supertrend") + +// LinReg is Linear Regression baseline +//go:generate callbackgen -type LinReg +type LinReg struct { + types.SeriesBase + types.IntervalWindow + + // Values are the slopes of linear regression baseline + Values floats.Slice + klines types.KLineWindow + + EndTime time.Time + UpdateCallbacks []func(value float64) +} + +// Last slope of linear regression baseline +func (lr *LinReg) Last() float64 { + if lr.Values.Length() == 0 { + return 0.0 + } + return lr.Values.Last() +} + +// Index returns the slope of specified index +func (lr *LinReg) Index(i int) float64 { + if i >= lr.Values.Length() { + return 0.0 + } + + return lr.Values.Index(i) +} + +// Length of the slope values +func (lr *LinReg) Length() int { + return lr.Values.Length() +} + +var _ types.SeriesExtend = &LinReg{} + +// Update Linear Regression baseline slope +func (lr *LinReg) Update(kline types.KLine) { + lr.klines.Add(kline) + lr.klines.Truncate(lr.Window) + if len(lr.klines) < lr.Window { + lr.Values.Push(0) + return + } + + var sumX, sumY, sumXSqr, sumXY float64 = 0, 0, 0, 0 + end := len(lr.klines) - 1 // The last kline + for i := end; i >= end-lr.Window+1; i-- { + val := lr.klines[i].GetClose().Float64() + per := float64(end - i + 1) + sumX += per + sumY += val + sumXSqr += per * per + sumXY += val * per + } + length := float64(lr.Window) + slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX) + average := sumY / length + endPrice := average - slope*sumX/length + slope + startPrice := endPrice + slope*(length-1) + lr.Values.Push((endPrice - startPrice) / (length - 1)) + + log.Debugf("linear regression baseline slope: %f", lr.Last()) +} + +func (lr *LinReg) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, lr.PushK)) +} + +func (lr *LinReg) PushK(k types.KLine) { + var zeroTime = time.Time{} + if lr.EndTime != zeroTime && k.EndTime.Before(lr.EndTime) { + return + } + + lr.Update(k) + lr.EndTime = k.EndTime.Time() +} + +func (lr *LinReg) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + lr.PushK(k) + } +} diff --git a/pkg/indicator/linreg_callbacks.go b/pkg/indicator/linreg_callbacks.go new file mode 100644 index 000000000..c719c8f51 --- /dev/null +++ b/pkg/indicator/linreg_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type LinReg"; DO NOT EDIT. + +package indicator + +import () + +func (lr *LinReg) OnUpdate(cb func(value float64)) { + lr.UpdateCallbacks = append(lr.UpdateCallbacks, cb) +} + +func (lr *LinReg) EmitUpdate(value float64) { + for _, cb := range lr.UpdateCallbacks { + cb(value) + } +} From df05cf65d231c8a7d2b105965455311b80419e02 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 21 Oct 2022 16:15:55 +0800 Subject: [PATCH 02/37] feature/dynamicSpread: dynamicSpread as a common package --- pkg/dynamicmetric/dynamic_spread.go | 275 ++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 pkg/dynamicmetric/dynamic_spread.go diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go new file mode 100644 index 000000000..2f4d66360 --- /dev/null +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -0,0 +1,275 @@ +package dynamicmetric + +import ( + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "math" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type DynamicSpread struct { + // AmpSpread calculates spreads based on kline amplitude + AmpSpread *DynamicSpreadAmp `json:"amplitude"` + + // WeightedBollWidthRatioSpread calculates spreads based on two Bollinger Bands + WeightedBollWidthRatioSpread *DynamicSpreadBollWidthRatio `json:"weightedBollWidth"` + + // deprecated + Enabled *bool `json:"enabled"` + + // deprecated + types.IntervalWindow + + // deprecated. AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // deprecated. BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` +} + +// Initialize dynamic spread +func (ds *DynamicSpread) Initialize(symbol string, session *bbgo.ExchangeSession) { + switch { + case ds.AmpSpread != nil: + ds.AmpSpread.initialize(symbol, session) + case ds.WeightedBollWidthRatioSpread != nil: + ds.WeightedBollWidthRatioSpread.initialize(symbol, session) + case ds.Enabled != nil && *ds.Enabled: + // backward compatibility + ds.AmpSpread = &DynamicSpreadAmp{ + IntervalWindow: ds.IntervalWindow, + AskSpreadScale: ds.AskSpreadScale, + BidSpreadScale: ds.BidSpreadScale, + } + ds.AmpSpread.initialize(symbol, session) + } +} + +func (ds *DynamicSpread) IsEnabled() bool { + return ds.AmpSpread != nil || ds.WeightedBollWidthRatioSpread != nil +} + +// GetAskSpread returns current ask spread +func (ds *DynamicSpread) GetAskSpread() (askSpread float64, err error) { + switch { + case ds.AmpSpread != nil: + return ds.AmpSpread.getAskSpread() + case ds.WeightedBollWidthRatioSpread != nil: + return ds.WeightedBollWidthRatioSpread.getAskSpread() + default: + return 0, errors.New("dynamic spread is not enabled") + } +} + +// GetBidSpread returns current dynamic bid spread +func (ds *DynamicSpread) GetBidSpread() (bidSpread float64, err error) { + switch { + case ds.AmpSpread != nil: + return ds.AmpSpread.getBidSpread() + case ds.WeightedBollWidthRatioSpread != nil: + return ds.WeightedBollWidthRatioSpread.getBidSpread() + default: + return 0, errors.New("dynamic spread is not enabled") + } +} + +// DynamicSpreadAmp uses kline amplitude to calculate spreads +type DynamicSpreadAmp struct { + types.IntervalWindow + + // AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` + + dynamicAskSpread *indicator.SMA + dynamicBidSpread *indicator.SMA +} + +// initialize amplitude dynamic spread and preload SMAs +func (ds *DynamicSpreadAmp) initialize(symbol string, session *bbgo.ExchangeSession) { + ds.dynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} + ds.dynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} + + // Subscribe kline + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: ds.Interval, + }) + + // Update on kline closed + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, ds.Interval, func(kline types.KLine) { + ds.update(kline) + })) + + // Preload + kLineStore, _ := session.MarketDataStore(symbol) + if klines, ok := kLineStore.KLinesOfInterval(ds.Interval); ok { + for i := 0; i < len(*klines); i++ { + ds.update((*klines)[i]) + } + } +} + +// update amplitude dynamic spread with kline +func (ds *DynamicSpreadAmp) update(kline types.KLine) { + // ampl is the amplitude of kline + ampl := (kline.GetHigh().Float64() - kline.GetLow().Float64()) / kline.GetOpen().Float64() + + switch kline.Direction() { + case types.DirectionUp: + ds.dynamicAskSpread.Update(ampl) + ds.dynamicBidSpread.Update(0) + case types.DirectionDown: + ds.dynamicBidSpread.Update(ampl) + ds.dynamicAskSpread.Update(0) + default: + ds.dynamicAskSpread.Update(0) + ds.dynamicBidSpread.Update(0) + } +} + +func (ds *DynamicSpreadAmp) getAskSpread() (askSpread float64, err error) { + if ds.AskSpreadScale != nil && ds.dynamicAskSpread.Length() >= ds.Window { + askSpread, err = ds.AskSpreadScale.Scale(ds.dynamicAskSpread.Last()) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return askSpread, nil + } + + return 0, errors.New("incomplete dynamic spread settings or not enough data yet") +} + +func (ds *DynamicSpreadAmp) getBidSpread() (bidSpread float64, err error) { + if ds.BidSpreadScale != nil && ds.dynamicBidSpread.Length() >= ds.Window { + bidSpread, err = ds.BidSpreadScale.Scale(ds.dynamicBidSpread.Last()) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicBidSpread") + return 0, err + } + + return bidSpread, nil + } + + return 0, errors.New("incomplete dynamic spread settings or not enough data yet") +} + +// BollingerSetting is for Bollinger Band settings +type BollingerSetting struct { + types.IntervalWindow + BandWidth float64 `json:"bandWidth"` +} + +type DynamicSpreadBollWidthRatio struct { + // AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` + + // Sensitivity factor of the weighting function: 1 / (1 + exp(-(x - mid) * sensitivity / width)) + // A positive number. The greater factor, the sharper weighting function. Default set to 1.0 . + Sensitivity float64 `json:"sensitivity"` + + DefaultBollinger *BollingerSetting `json:"defaultBollinger"` + NeutralBollinger *BollingerSetting `json:"neutralBollinger"` + + StandardIndicatorSet *bbgo.StandardIndicatorSet + + neutralBoll *indicator.BOLL + defaultBoll *indicator.BOLL +} + +func (ds *DynamicSpreadBollWidthRatio) initialize(symbol string, session *bbgo.ExchangeSession) { + ds.neutralBoll = ds.StandardIndicatorSet.BOLL(ds.NeutralBollinger.IntervalWindow, ds.NeutralBollinger.BandWidth) + ds.defaultBoll = ds.StandardIndicatorSet.BOLL(ds.DefaultBollinger.IntervalWindow, ds.DefaultBollinger.BandWidth) + + // Subscribe kline + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: ds.NeutralBollinger.Interval, + }) + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: ds.DefaultBollinger.Interval, + }) + + if ds.Sensitivity <= 0. { + ds.Sensitivity = 1. + } +} + +func (ds *DynamicSpreadBollWidthRatio) getAskSpread() (askSpread float64, err error) { + askSpread, err = ds.AskSpreadScale.Scale(ds.getWeightedBBWidthRatio(true)) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return askSpread, nil +} + +func (ds *DynamicSpreadBollWidthRatio) getBidSpread() (bidSpread float64, err error) { + bidSpread, err = ds.BidSpreadScale.Scale(ds.getWeightedBBWidthRatio(false)) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return bidSpread, nil +} + +func (ds *DynamicSpreadBollWidthRatio) getWeightedBBWidthRatio(positiveSigmoid bool) float64 { + // Weight the width of Boll bands with sigmoid function and calculate the ratio after integral. + // + // Given the default band: moving average default_BB_mid, band from default_BB_lower to default_BB_upper. + // And the neutral band: from neutral_BB_lower to neutral_BB_upper. + // And a sensitivity factor alpha, which is a positive constant. + // + // width of default BB w = default_BB_upper - default_BB_lower + // + // 1 x - default_BB_mid + // sigmoid weighting function f(y) = ------------- where y = -------------------- + // 1 + exp(-y) w / alpha + // Set the sigmoid weighting function: + // - To ask spread, the weighting density function d_weight(x) is sigmoid((x - default_BB_mid) / (w / alpha)) + // - To bid spread, the weighting density function d_weight(x) is sigmoid((default_BB_mid - x) / (w / alpha)) + // - The higher sensitivity factor alpha, the sharper weighting function. + // + // Then calculate the weighted band width ratio by taking integral of d_weight(x) from neutral_BB_lower to neutral_BB_upper: + // infinite integral of ask spread sigmoid weighting density function F(x) = (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) + // infinite integral of bid spread sigmoid weighting density function F(x) = x - (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) + // Note that we've rescaled the sigmoid function to fit default BB, + // the weighted default BB width is always calculated by integral(f of x from default_BB_lower to default_BB_upper) + // F(neutral_BB_upper) - F(neutral_BB_lower) + // weighted ratio = ------------------------------------------- + // F(default_BB_upper) - F(default_BB_lower) + // - The wider neutral band get greater ratio + // - To ask spread, the higher neutral band get greater ratio + // - To bid spread, the lower neutral band get greater ratio + + defaultMid := ds.defaultBoll.SMA.Last() + defaultUpper := ds.defaultBoll.UpBand.Last() + defaultLower := ds.defaultBoll.DownBand.Last() + defaultWidth := defaultUpper - defaultLower + neutralUpper := ds.neutralBoll.UpBand.Last() + neutralLower := ds.neutralBoll.DownBand.Last() + factor := defaultWidth / ds.Sensitivity + var weightedUpper, weightedLower, weightedDivUpper, weightedDivLower float64 + if positiveSigmoid { + weightedUpper = factor * math.Log(math.Exp(neutralUpper/factor)+math.Exp(defaultMid/factor)) + weightedLower = factor * math.Log(math.Exp(neutralLower/factor)+math.Exp(defaultMid/factor)) + weightedDivUpper = factor * math.Log(math.Exp(defaultUpper/factor)+math.Exp(defaultMid/factor)) + weightedDivLower = factor * math.Log(math.Exp(defaultLower/factor)+math.Exp(defaultMid/factor)) + } else { + weightedUpper = neutralUpper - factor*math.Log(math.Exp(neutralUpper/factor)+math.Exp(defaultMid/factor)) + weightedLower = neutralLower - factor*math.Log(math.Exp(neutralLower/factor)+math.Exp(defaultMid/factor)) + weightedDivUpper = defaultUpper - factor*math.Log(math.Exp(defaultUpper/factor)+math.Exp(defaultMid/factor)) + weightedDivLower = defaultLower - factor*math.Log(math.Exp(defaultLower/factor)+math.Exp(defaultMid/factor)) + } + return (weightedUpper - weightedLower) / (weightedDivUpper - weightedDivLower) +} From faee87d2adbd70394cab7f186304cf5b588bf9c6 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 21 Oct 2022 17:20:31 +0800 Subject: [PATCH 03/37] feature/dynamicExposure: dynamicExposure as a common package --- pkg/dynamicmetric/bollsetting.go | 9 +++ pkg/dynamicmetric/dynamic_exposure.go | 83 +++++++++++++++++++++++++++ pkg/dynamicmetric/dynamic_spread.go | 6 -- 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 pkg/dynamicmetric/bollsetting.go create mode 100644 pkg/dynamicmetric/dynamic_exposure.go diff --git a/pkg/dynamicmetric/bollsetting.go b/pkg/dynamicmetric/bollsetting.go new file mode 100644 index 000000000..79bff4ef1 --- /dev/null +++ b/pkg/dynamicmetric/bollsetting.go @@ -0,0 +1,9 @@ +package dynamicmetric + +import "github.com/c9s/bbgo/pkg/types" + +// BollingerSetting is for Bollinger Band settings +type BollingerSetting struct { + types.IntervalWindow + BandWidth float64 `json:"bandWidth"` +} diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go new file mode 100644 index 000000000..aadd1ece3 --- /dev/null +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -0,0 +1,83 @@ +package dynamicmetric + +import ( + "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/pkg/errors" + log "github.com/sirupsen/logrus" + "math" +) + +type DynamicExposure struct { + // BollBandExposure calculates the max exposure with the Bollinger Band + BollBandExposure *DynamicExposureBollBand `json:"bollBandExposure"` +} + +// Initialize dynamic exposure +func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession) { + switch { + case d.BollBandExposure != nil: + d.BollBandExposure.initialize(symbol, session) + } +} + +func (d *DynamicExposure) IsEnabled() bool { + return d.BollBandExposure != nil +} + +// GetMaxExposure returns the max exposure +func (d *DynamicExposure) GetMaxExposure(price float64) (maxExposure fixedpoint.Value, err error) { + switch { + case d.BollBandExposure != nil: + return d.BollBandExposure.getMaxExposure(price) + default: + return fixedpoint.Zero, errors.New("dynamic exposure is not enabled") + } +} + +// DynamicExposureBollBand calculates the max exposure with the Bollinger Band +type DynamicExposureBollBand struct { + // DynamicExposureBollBandScale is used to define the exposure range with the given percentage. + DynamicExposureBollBandScale *bbgo.PercentageScale `json:"dynamicExposurePositionScale"` + + *BollingerSetting + + StandardIndicatorSet *bbgo.StandardIndicatorSet + + dynamicExposureBollBand *indicator.BOLL +} + +// Initialize DynamicExposureBollBand +func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession) { + d.dynamicExposureBollBand = d.StandardIndicatorSet.BOLL(d.IntervalWindow, d.BandWidth) + + // Subscribe kline + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: d.dynamicExposureBollBand.Interval, + }) +} + +// getMaxExposure returns the max exposure +func (d *DynamicExposureBollBand) getMaxExposure(price float64) (fixedpoint.Value, error) { + downBand := d.dynamicExposureBollBand.DownBand.Last() + upBand := d.dynamicExposureBollBand.UpBand.Last() + sma := d.dynamicExposureBollBand.SMA.Last() + log.Infof("dynamicExposureBollBand bollinger band: up %f sma %f down %f", upBand, sma, downBand) + + bandPercentage := 0.0 + if price < sma { + // should be negative percentage + bandPercentage = (price - sma) / math.Abs(sma-downBand) + } else if price > sma { + // should be positive percentage + bandPercentage = (price - sma) / math.Abs(upBand-sma) + } + + v, err := d.DynamicExposureBollBandScale.Scale(bandPercentage) + if err != nil { + return fixedpoint.Zero, err + } + return fixedpoint.NewFromFloat(v), nil +} diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go index 2f4d66360..a1e5e631c 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -160,12 +160,6 @@ func (ds *DynamicSpreadAmp) getBidSpread() (bidSpread float64, err error) { return 0, errors.New("incomplete dynamic spread settings or not enough data yet") } -// BollingerSetting is for Bollinger Band settings -type BollingerSetting struct { - types.IntervalWindow - BandWidth float64 `json:"bandWidth"` -} - type DynamicSpreadBollWidthRatio struct { // AskSpreadScale is used to define the ask spread range with the given percentage. AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` From 48c6326ac12034990e66cfa640e3aeb3aa71c7fa Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 17 Nov 2022 17:59:23 +0800 Subject: [PATCH 04/37] strategy/linregmaker: draft --- pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/linregmaker/doc.go | 6 + pkg/strategy/linregmaker/strategy.go | 493 ++++++++++++++++++++++ pkg/strategy/linregmaker/strategy_test.go | 69 +++ 4 files changed, 569 insertions(+) create mode 100644 pkg/strategy/linregmaker/doc.go create mode 100644 pkg/strategy/linregmaker/strategy.go create mode 100644 pkg/strategy/linregmaker/strategy_test.go diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 1c29ab684..0c2a27b88 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -20,6 +20,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/harmonic" _ "github.com/c9s/bbgo/pkg/strategy/irr" _ "github.com/c9s/bbgo/pkg/strategy/kline" + _ "github.com/c9s/bbgo/pkg/strategy/linregmaker" _ "github.com/c9s/bbgo/pkg/strategy/marketcap" _ "github.com/c9s/bbgo/pkg/strategy/pivotshort" _ "github.com/c9s/bbgo/pkg/strategy/pricealert" diff --git a/pkg/strategy/linregmaker/doc.go b/pkg/strategy/linregmaker/doc.go new file mode 100644 index 000000000..a4bddf911 --- /dev/null +++ b/pkg/strategy/linregmaker/doc.go @@ -0,0 +1,6 @@ +// Linregmaker is a maker strategy depends on the linear regression baseline slopes +// +// Linregmaker uses two linear regression baseline slopes for trading: +// 1) The fast linReg is to determine the short-term trend. It controls whether placing buy/sell orders or not. +// 2) The slow linReg is to determine the mid-term trend. It controls whether the creation of opposite direction position is allowed. +package linregmaker diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go new file mode 100644 index 000000000..6857bd4c9 --- /dev/null +++ b/pkg/strategy/linregmaker/strategy.go @@ -0,0 +1,493 @@ +package linregmaker + +import ( + "context" + "fmt" + "github.com/c9s/bbgo/pkg/dynamicmetric" + "sync" + + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/util" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// TODO: +// - TradeInBand: no buy order above the band, no sell order below the band +// - DynamicQuantity +// - Validate() + +const ID = "linregmaker" + +var notionModifier = fixedpoint.NewFromFloat(1.1) +var two = fixedpoint.NewFromInt(2) + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type BollingerSetting struct { + types.IntervalWindow + BandWidth float64 `json:"bandWidth"` +} + +type Strategy struct { + Environment *bbgo.Environment + StandardIndicatorSet *bbgo.StandardIndicatorSet + Market types.Market + + // Symbol is the market symbol you want to trade + Symbol string `json:"symbol"` + + types.IntervalWindow + + bbgo.QuantityOrAmount + + // ReverseEMA is used to determine the long-term trend. + // Above the ReverseEMA is the long trend and vise versa. + // All the opposite trend position will be closed upon the trend change + ReverseEMA *indicator.EWMA `json:"reverseEMA"` + + // mainTrendCurrent is the current long-term trend + mainTrendCurrent types.Direction + // mainTrendPrevious is the long-term trend of previous kline + mainTrendPrevious types.Direction + + // FastLinReg is to determine the short-term trend. + // Buy/sell orders are placed if the FastLinReg and the ReverseEMA trend are in the same direction, and only orders + // that reduce position are placed if the FastLinReg and the ReverseEMA trend are in different directions. + FastLinReg *indicator.LinReg `json:"fastLinReg,omitempty"` + + // SlowLinReg is to determine the midterm trend. + // When the SlowLinReg and the ReverseEMA trend are in different directions, creation of opposite position is + // allowed. + SlowLinReg *indicator.LinReg `json:"slowLinReg,omitempty"` + + // NeutralBollinger is the smaller range of the bollinger band + // If price is in this band, it usually means the price is oscillating. + // If price goes out of this band, we tend to not place sell orders or buy orders + NeutralBollinger *BollingerSetting `json:"neutralBollinger"` + + // TradeInBand + // When this is on, places orders only when the current price is in the bollinger band. + TradeInBand bool `json:"tradeInBand"` + + // useTickerPrice use the ticker api to get the mid price instead of the closed kline price. + // The back-test engine is kline-based, so the ticker price api is not supported. + // Turn this on if you want to do real trading. + useTickerPrice bool + + // Spread is the price spread from the middle price. + // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) + // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) + // Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 + Spread fixedpoint.Value `json:"spread"` + + // BidSpread overrides the spread setting, this spread will be used for the buy order + BidSpread fixedpoint.Value `json:"bidSpread,omitempty"` + + // AskSpread overrides the spread setting, this spread will be used for the sell order + AskSpread fixedpoint.Value `json:"askSpread,omitempty"` + + // DynamicSpread enables the automatic adjustment to bid and ask spread. + // Overrides Spread, BidSpread, and AskSpread + DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` + + // MaxExposurePosition is the maximum position you can hold + // +10 means you can hold 10 ETH long position by maximum + // -10 means you can hold -10 ETH short position by maximum + MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` + + // DynamicExposure is used to define the exposure position range with the given percentage. + // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically according to the bollinger + // band you set. + DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` + + session *bbgo.ExchangeSession + + // ExitMethods are various TP/SL methods + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + orderExecutor *bbgo.GeneralOrderExecutor + + groupID uint32 + + // defaultBoll is the BOLLINGER indicator we used for predicting the price. + defaultBoll *indicator.BOLL + + // neutralBoll is the neutral price section + neutralBoll *indicator.BOLL + + // StrategyController + bbgo.StrategyController +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // Subscribe for ReverseEMA + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.ReverseEMA.Interval, + }) + // Initialize ReverseEMA + s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow) + + // Subscribe for LinRegs + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.FastLinReg.Interval, + }) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.SlowLinReg.Interval, + }) + // Initialize LinRegs + kLineStore, _ := session.MarketDataStore(s.Symbol) + s.FastLinReg.BindK(session.MarketDataStream, s.Symbol, s.FastLinReg.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.FastLinReg.Interval); ok { + s.FastLinReg.LoadK((*klines)[0:]) + } + s.SlowLinReg.BindK(session.MarketDataStream, s.Symbol, s.SlowLinReg.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.SlowLinReg.Interval); ok { + s.SlowLinReg.LoadK((*klines)[0:]) + } + + // Subscribe for BBs + if s.NeutralBollinger != nil && s.NeutralBollinger.Interval != "" { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.NeutralBollinger.Interval, + }) + } + // Initialize BBs + s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) + + // Setup Exits + s.ExitMethods.SetAndSubscribe(session, s) + + // Setup dynamic spread + if s.DynamicSpread.IsEnabled() { + s.DynamicSpread.Initialize(s.Symbol, session) + } + + // Setup dynamic exposure + if s.DynamicExposure.IsEnabled() { + s.DynamicExposure.Initialize(s.Symbol, session) + } +} + +func (s *Strategy) Validate() error { + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + return nil +} + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + return s.orderExecutor.ClosePosition(ctx, percentage) +} + +// updateSpread for ask and bid price +func (s *Strategy) updateSpread() { + // Update spreads with dynamic spread + if s.DynamicSpread.IsEnabled() { + dynamicBidSpread, err := s.DynamicSpread.GetBidSpread() + if err == nil && dynamicBidSpread > 0 { + s.BidSpread = fixedpoint.NewFromFloat(dynamicBidSpread) + log.Infof("%s dynamic bid spread updated: %s", s.Symbol, s.BidSpread.Percentage()) + } + dynamicAskSpread, err := s.DynamicSpread.GetAskSpread() + if err == nil && dynamicAskSpread > 0 { + s.AskSpread = fixedpoint.NewFromFloat(dynamicAskSpread) + log.Infof("%s dynamic ask spread updated: %s", s.Symbol, s.AskSpread.Percentage()) + } + } + + if s.BidSpread.Sign() <= 0 { + s.BidSpread = s.Spread + } + + if s.BidSpread.Sign() <= 0 { + s.AskSpread = s.Spread + } +} + +// updateMaxExposure with dynamic exposure +func (s *Strategy) updateMaxExposure(midPrice fixedpoint.Value) { + // Calculate max exposure + if s.DynamicExposure.IsEnabled() { + var err error + maxExposurePosition, err := s.DynamicExposure.GetMaxExposure(midPrice.Float64()) + if err != nil { + log.WithError(err).Errorf("can not calculate DynamicExposure of %s, use previous MaxExposurePosition instead", s.Symbol) + } else { + s.MaxExposurePosition = maxExposurePosition + } + log.Infof("calculated %s max exposure position: %v", s.Symbol, s.MaxExposurePosition) + } +} + +// getOrderPrices returns ask and bid prices +func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoint.Value, bidPrice fixedpoint.Value) { + askPrice = midPrice.Mul(fixedpoint.One.Add(s.AskSpread)) + bidPrice = midPrice.Mul(fixedpoint.One.Sub(s.BidSpread)) + log.Infof("mid price:%v ask:%v bid: %v", midPrice, askPrice, bidPrice) + + return askPrice, bidPrice +} + +// getOrderQuantities returns sell and buy qty +func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { + // TODO: dynamic qty to determine qty + sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) + + return sellQuantity, buyQuantity +} + +// getCanBuySell returns the buy sell switches +func (s *Strategy) getCanBuySell(midPrice fixedpoint.Value) (canBuy bool, canSell bool) { + // By default, both buy and sell are on, which means we will place buy and sell orders + canBuy = true + canSell = true + + // Check if current position > maxExposurePosition + if s.Position.GetBase().Abs().Compare(s.MaxExposurePosition) > 0 { + if s.mainTrendCurrent == types.DirectionUp { + canBuy = false + } else if s.mainTrendCurrent == types.DirectionDown { + canSell = false + } + } + + if s.TradeInBand { + // Price too high + if midPrice.Float64() > s.neutralBoll.UpBand.Last() { + canBuy = false + log.Infof("tradeInBand is set, skip buy when the price is higher than the neutralBB") + } + // Price too low in uptrend + if midPrice.Float64() < s.neutralBoll.DownBand.Last() { + canSell = false + log.Infof("tradeInBand is set, skip sell when the price is lower than the neutralBB") + } + } + + return canBuy, canSell +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // initial required information + s.session = session + + // Calculate group id for orders + instanceID := s.InstanceID() + s.groupID = util.FNV32(instanceID) + + // If position is nil, we need to allocate a new position for calculation + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + // Set fee rate + if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: s.session.MakerFeeRate, + TakerFeeRate: s.session.TakerFeeRate, + }) + } + + // If position is nil, we need to allocate a new position for calculation + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + // Always update the position fields + s.Position.Strategy = ID + s.Position.StrategyInstanceID = s.InstanceID() + + // Profit stats + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + 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.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.ExitMethods.Bind(session, s.orderExecutor) + + if bbgo.IsBackTesting { + s.useTickerPrice = false + } else { + s.useTickerPrice = true + } + + // StrategyController + s.Status = types.StrategyStatusRunning + s.OnSuspend(func() { + _ = s.orderExecutor.GracefulCancel(ctx) + bbgo.Sync(ctx, s) + }) + s.OnEmergencyStop(func() { + // Close whole position + _ = s.ClosePosition(ctx, fixedpoint.NewFromFloat(1.0)) + }) + + // Main interval + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + _ = s.orderExecutor.GracefulCancel(ctx) + + // closePrice is the close price of current kline + closePrice := kline.GetClose() + // priceReverseEMA is the current ReverseEMA price + priceReverseEMA := fixedpoint.NewFromFloat(s.ReverseEMA.Last()) + + // Main trend by ReverseEMA + s.mainTrendPrevious = s.mainTrendCurrent + if closePrice.Compare(priceReverseEMA) > 0 { + s.mainTrendCurrent = types.DirectionUp + } else if closePrice.Compare(priceReverseEMA) < 0 { + s.mainTrendCurrent = types.DirectionDown + } + // TODO: everything should works for both direction + + // Trend reversal + if s.mainTrendCurrent != s.mainTrendPrevious { + // Close on-hand position that is not in the same direction as the new trend + if !s.Position.IsDust(closePrice) && + ((s.Position.IsLong() && s.mainTrendCurrent == types.DirectionDown) || + (s.Position.IsShort() && s.mainTrendCurrent == types.DirectionUp)) { + log.Infof("trend reverse to %v. closing on-hand position", s.mainTrendCurrent) + if err := s.ClosePosition(ctx, fixedpoint.One); err != nil { + log.WithError(err).Errorf("cannot close on-hand position of %s", s.Symbol) + // TODO: close position failed. retry? + } + } + } + + // midPrice for ask and bid prices + var midPrice fixedpoint.Value + if s.useTickerPrice { + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + return + } + + midPrice = ticker.Buy.Add(ticker.Sell).Div(two) + log.Infof("using ticker price: bid %v / ask %v, mid price %v", ticker.Buy, ticker.Sell, midPrice) + } else { + midPrice = closePrice + } + + // Update price spread + s.updateSpread() + + // Update max exposure + s.updateMaxExposure(midPrice) + + // Current position status + log.Infof("position: %s", s.Position) + if !s.Position.IsClosed() && !s.Position.IsDust(midPrice) { + log.Infof("current %s unrealized profit: %f %s", s.Symbol, s.Position.UnrealizedProfit(midPrice).Float64(), s.Market.QuoteCurrency) + } + + // Order prices + askPrice, bidPrice := s.getOrderPrices(midPrice) + + // Order qty + sellQuantity, buyQuantity := s.getOrderQuantities(askPrice, bidPrice) + + // TODO: Reduce only in margin and futures + sellOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: sellQuantity, + Price: askPrice, + Market: s.Market, + GroupID: s.groupID, + } + buyOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: buyQuantity, + Price: bidPrice, + Market: s.Market, + GroupID: s.groupID, + } + + canBuy, canSell := s.getCanBuySell(midPrice) + + // TODO: check enough balance? + + // Submit orders + var submitOrders []types.SubmitOrder + if canSell { + submitOrders = append(submitOrders, adjustOrderQuantity(sellOrder, s.Market)) + } + if canBuy { + submitOrders = append(submitOrders, adjustOrderQuantity(buyOrder, s.Market)) + } + + if len(submitOrders) == 0 { + return + } + _, _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...) + })) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + return nil +} + +// TODO +func adjustOrderQuantity(submitOrder types.SubmitOrder, market types.Market) types.SubmitOrder { + if submitOrder.Quantity.Mul(submitOrder.Price).Compare(market.MinNotional) < 0 { + submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, market.MinNotional.Mul(notionModifier)) + } + + if submitOrder.Quantity.Compare(market.MinQuantity) < 0 { + submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, market.MinQuantity) + } + + return submitOrder +} diff --git a/pkg/strategy/linregmaker/strategy_test.go b/pkg/strategy/linregmaker/strategy_test.go new file mode 100644 index 000000000..317464ad0 --- /dev/null +++ b/pkg/strategy/linregmaker/strategy_test.go @@ -0,0 +1,69 @@ +package linregmaker + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +func Test_calculateBandPercentage(t *testing.T) { + type args struct { + up float64 + down float64 + sma float64 + midPrice float64 + } + tests := []struct { + name string + args args + want fixedpoint.Value + }{ + { + name: "positive boundary", + args: args{ + up: 2000.0, + sma: 1500.0, + down: 1000.0, + midPrice: 2000.0, + }, + want: fixedpoint.NewFromFloat(1.0), + }, + { + name: "inside positive boundary", + args: args{ + up: 2000.0, + sma: 1500.0, + down: 1000.0, + midPrice: 1600.0, + }, + want: fixedpoint.NewFromFloat(0.2), // 20% + }, + { + name: "negative boundary", + args: args{ + up: 2000.0, + sma: 1500.0, + down: 1000.0, + midPrice: 1000.0, + }, + want: fixedpoint.NewFromFloat(-1.0), + }, + { + name: "out of negative boundary", + args: args{ + up: 2000.0, + sma: 1500.0, + down: 1000.0, + midPrice: 800.0, + }, + want: fixedpoint.NewFromFloat(-1.4), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := calculateBandPercentage(tt.args.up, tt.args.down, tt.args.sma, tt.args.midPrice); fixedpoint.NewFromFloat(got) != tt.want { + t.Errorf("calculateBandPercentage() = %v, want %v", got, tt.want) + } + }) + } +} From 9be9ea2a471666f30bc563f9fb617ac2cd2bc383 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 18 Nov 2022 15:12:38 +0800 Subject: [PATCH 05/37] strategy/linregmaker: add AllowOppositePosition and FasterDecreaseRatio --- pkg/strategy/linregmaker/strategy.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 6857bd4c9..154336d0f 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -70,6 +70,14 @@ type Strategy struct { // allowed. SlowLinReg *indicator.LinReg `json:"slowLinReg,omitempty"` + // AllowOppositePosition if true, the creation of opposite position is allowed when both fast and slow LinReg are in + // the opposite direction to main trend + AllowOppositePosition bool `json:"allowOppositePosition"` + + // FasterDecreaseRatio the quantity of decreasing position orders are multiplied by this ratio when both fast and + // slow LinReg are in the opposite direction to main trend + FasterDecreaseRatio fixedpoint.Value `json:"FasterDecreaseRatio,omitempty"` + // NeutralBollinger is the smaller range of the bollinger band // If price is in this band, it usually means the price is oscillating. // If price goes out of this band, we tend to not place sell orders or buy orders @@ -259,8 +267,17 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin // getOrderQuantities returns sell and buy qty func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { // TODO: dynamic qty to determine qty + // TODO: spot, margin, and futures sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + + // Faster position decrease + if s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0 { + sellQuantity = sellQuantity * s.FasterDecreaseRatio + } else if s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0 { + buyQuantity = buyQuantity * s.FasterDecreaseRatio + } + log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) return sellQuantity, buyQuantity @@ -294,6 +311,15 @@ func (s *Strategy) getCanBuySell(midPrice fixedpoint.Value) (canBuy bool, canSel } } + // Stop decrease when position closed unless both LinRegs are in the opposite direction to the main trend + if s.Position.IsClosed() || s.Position.IsDust(midPrice) { + if s.mainTrendCurrent == types.DirectionUp && !(s.AllowOppositePosition && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0) { + canSell = false + } else if s.mainTrendCurrent == types.DirectionDown && !(s.AllowOppositePosition && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0) { + canBuy = false + } + } + return canBuy, canSell } From 8a81e68e276ca2955142aaa59c4f3b932bd6a1a5 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 18 Nov 2022 16:42:51 +0800 Subject: [PATCH 06/37] strategy/linregmaker: add dynamic quantity --- pkg/dynamicmetric/dynamic_exposure.go | 2 +- pkg/dynamicmetric/dynamic_quantity.go | 93 +++++++++++++++++++++++++++ pkg/strategy/linregmaker/strategy.go | 62 +++++++++++++++--- 3 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 pkg/dynamicmetric/dynamic_quantity.go diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go index aadd1ece3..c2e1802a1 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -49,7 +49,7 @@ type DynamicExposureBollBand struct { dynamicExposureBollBand *indicator.BOLL } -// Initialize DynamicExposureBollBand +// initialize dynamic exposure with Bollinger Band func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession) { d.dynamicExposureBollBand = d.StandardIndicatorSet.BOLL(d.IntervalWindow, d.BandWidth) diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/dynamicmetric/dynamic_quantity.go new file mode 100644 index 000000000..ac2e85b38 --- /dev/null +++ b/pkg/dynamicmetric/dynamic_quantity.go @@ -0,0 +1,93 @@ +package dynamicmetric + +import ( + "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/pkg/errors" +) + +// DynamicQuantitySet uses multiple dynamic quantity rules to calculate the total quantity +type DynamicQuantitySet []DynamicQuantity + +// Initialize dynamic quantity set +func (d *DynamicQuantitySet) Initialize(symbol string, session *bbgo.ExchangeSession) { + for i := range *d { + (*d)[i].Initialize(symbol, session) + } +} + +// GetQuantity returns the quantity +func (d *DynamicQuantitySet) GetQuantity() (fixedpoint.Value, error) { + quantity := fixedpoint.Zero + for i := range *d { + v, err := (*d)[i].getQuantity() + if err != nil { + return fixedpoint.Zero, err + } + quantity = quantity.Add(v) + } + + return quantity, nil +} + +type DynamicQuantity struct { + // LinRegQty calculates quantity based on LinReg slope + LinRegDynamicQuantity *DynamicQuantityLinReg `json:"linRegDynamicQuantity"` +} + +// Initialize dynamic quantity +func (d *DynamicQuantity) Initialize(symbol string, session *bbgo.ExchangeSession) { + switch { + case d.LinRegDynamicQuantity != nil: + d.LinRegDynamicQuantity.initialize(symbol, session) + } +} + +func (d *DynamicQuantity) IsEnabled() bool { + return d.LinRegDynamicQuantity != nil +} + +// getQuantity returns quantity +func (d *DynamicQuantity) getQuantity() (fixedpoint.Value, error) { + switch { + case d.LinRegDynamicQuantity != nil: + return d.LinRegDynamicQuantity.getQuantity() + default: + return fixedpoint.Zero, errors.New("dynamic quantity is not enabled") + } +} + +// DynamicQuantityLinReg uses LinReg slope to calculate quantity +type DynamicQuantityLinReg struct { + // DynamicQuantityLinRegScale is used to define the quantity range with the given parameters. + DynamicQuantityLinRegScale *bbgo.PercentageScale `json:"dynamicQuantityLinRegScale"` + + // QuantityLinReg to define the interval and window of the LinReg + QuantityLinReg *indicator.LinReg `json:"quantityLinReg"` +} + +// initialize LinReg dynamic quantity +func (d *DynamicQuantityLinReg) initialize(symbol string, session *bbgo.ExchangeSession) { + // Subscribe for LinReg + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: d.QuantityLinReg.Interval, + }) + + // Initialize LinReg + kLineStore, _ := session.MarketDataStore(symbol) + d.QuantityLinReg.BindK(session.MarketDataStream, symbol, d.QuantityLinReg.Interval) + if klines, ok := kLineStore.KLinesOfInterval(d.QuantityLinReg.Interval); ok { + d.QuantityLinReg.LoadK((*klines)[0:]) + } +} + +// getQuantity returns quantity +func (d *DynamicQuantityLinReg) getQuantity() (fixedpoint.Value, error) { + v, err := d.DynamicQuantityLinRegScale.Scale(d.QuantityLinReg.Last()) + if err != nil { + return fixedpoint.Zero, err + } + return fixedpoint.NewFromFloat(v), nil +} diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 154336d0f..1eca593d8 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -48,8 +48,6 @@ type Strategy struct { types.IntervalWindow - bbgo.QuantityOrAmount - // ReverseEMA is used to determine the long-term trend. // Above the ReverseEMA is the long trend and vise versa. // All the opposite trend position will be closed upon the trend change @@ -109,15 +107,21 @@ type Strategy struct { DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` // MaxExposurePosition is the maximum position you can hold - // +10 means you can hold 10 ETH long position by maximum - // -10 means you can hold -10 ETH short position by maximum + // 10 means you can hold 10 ETH long/short position by maximum MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` // DynamicExposure is used to define the exposure position range with the given percentage. - // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically according to the bollinger - // band you set. + // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` + bbgo.QuantityOrAmount + + // DynamicQuantityIncrease calculates the increase position order quantity dynamically + DynamicQuantityIncrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityIncrease"` + + // DynamicQuantityDecrease calculates the decrease position order quantity dynamically + DynamicQuantityDecrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityDecrease"` + session *bbgo.ExchangeSession // ExitMethods are various TP/SL methods @@ -197,6 +201,14 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { if s.DynamicExposure.IsEnabled() { s.DynamicExposure.Initialize(s.Symbol, session) } + + // Setup dynamic quantities + if len(s.DynamicQuantityIncrease) > 0 { + s.DynamicQuantityIncrease.Initialize(s.Symbol, session) + } + if len(s.DynamicQuantityDecrease) > 0 { + s.DynamicQuantityDecrease.Initialize(s.Symbol, session) + } } func (s *Strategy) Validate() error { @@ -266,10 +278,42 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin // getOrderQuantities returns sell and buy qty func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { - // TODO: dynamic qty to determine qty // TODO: spot, margin, and futures - sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) - buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + + // Dynamic qty + switch { + case s.mainTrendCurrent == types.DirectionUp: + var err error + if len(s.DynamicQuantityIncrease) > 0 { + buyQuantity, err = s.DynamicQuantityIncrease.GetQuantity() + if err != nil { + buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + } + } + if len(s.DynamicQuantityDecrease) > 0 { + sellQuantity, err = s.DynamicQuantityDecrease.GetQuantity() + if err != nil { + sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + } + } + case s.mainTrendCurrent == types.DirectionDown: + var err error + if len(s.DynamicQuantityIncrease) > 0 { + sellQuantity, err = s.DynamicQuantityIncrease.GetQuantity() + if err != nil { + sellQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + } + } + if len(s.DynamicQuantityDecrease) > 0 { + buyQuantity, err = s.DynamicQuantityDecrease.GetQuantity() + if err != nil { + buyQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + } + } + default: + sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + } // Faster position decrease if s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0 { From f121218ede3feec095b71342b0b5a96152019184 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 21 Nov 2022 13:46:13 +0800 Subject: [PATCH 07/37] strategy/linregmaker: prototype --- config/linregmaker.yaml | 143 +++++++++++++++++++++++++++ pkg/indicator/linreg.go | 4 +- pkg/strategy/linregmaker/strategy.go | 5 +- 3 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 config/linregmaker.yaml diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml new file mode 100644 index 000000000..5a62ed8ce --- /dev/null +++ b/config/linregmaker.yaml @@ -0,0 +1,143 @@ +--- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: BTCUSDT + +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-01-01" + endTime: "2022-06-30" + symbols: + - BTCUSDT + accounts: + binance: + makerCommission: 10 # 0.15% + takerCommission: 15 # 0.15% + balances: + BTC: 10.0 + USDT: 10000.0 + +exchangeStrategies: +- on: binance + linregmaker: + symbol: BTCUSDT + + # interval is how long do you want to update your order price and quantity + interval: 1m + + # reverseEMA + reverseEMA: + interval: 1d + window: 60 + + # fastLinReg + fastLinReg: + interval: 1m + window: 20 + + # slowLinReg + slowLinReg: + interval: 1m + window: 60 + + # allowOppositePosition + allowOppositePosition: true + + # fasterDecreaseRatio + fasterDecreaseRatio: 2 + + # neutralBollinger + neutralBollinger: + interval: "5m" + window: 21 + bandWidth: 2.0 + + # tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band. + tradeInBand: true + + # spread + spread: 0.1% + + # dynamicSpread + dynamicSpread: + amplitude: # delete other scaling strategy if this is defined + # window is the window of the SMAs of spreads + window: 1 + interval: "1m" + askSpreadScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from down to up + domain: [ 0.0001, 0.005 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 0.001, 0.002 ] + bidSpreadScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from down to up + domain: [ 0.0001, 0.005 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 0.001, 0.002 ] + + maxExposurePosition: 10 + DynamicExposure: + interval: "1h" + window: 21 + bandWidth: 2.0 + dynamicExposurePositionScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ -1, 1 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 10.0, 1.0 ] + + # quantity is the base order quantity for your buy/sell order. + quantity: 0.1 + dynamicQuantityIncrease: + - linRegDynamicQuantity: + quantityLinReg: + interval: 1m + window: 20 + dynamicQuantityLinRegScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ -1, 1 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 0, 0.1 ] + dynamicQuantityDecrease: + - linRegDynamicQuantity: + quantityLinReg: + interval: 1m + window: 20 + dynamicQuantityLinRegScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ 1, -1 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 0, 0.1 ] diff --git a/pkg/indicator/linreg.go b/pkg/indicator/linreg.go index e48c82a2c..d41fe9856 100644 --- a/pkg/indicator/linreg.go +++ b/pkg/indicator/linreg.go @@ -8,7 +8,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -var log = logrus.WithField("indicator", "supertrend") +var logLinReg = logrus.WithField("indicator", "LinReg") // LinReg is Linear Regression baseline //go:generate callbackgen -type LinReg @@ -74,7 +74,7 @@ func (lr *LinReg) Update(kline types.KLine) { startPrice := endPrice + slope*(length-1) lr.Values.Push((endPrice - startPrice) / (length - 1)) - log.Debugf("linear regression baseline slope: %f", lr.Last()) + logLinReg.Debugf("linear regression baseline slope: %f", lr.Last()) } func (lr *LinReg) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 1eca593d8..e38f6613b 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -74,7 +74,7 @@ type Strategy struct { // FasterDecreaseRatio the quantity of decreasing position orders are multiplied by this ratio when both fast and // slow LinReg are in the opposite direction to main trend - FasterDecreaseRatio fixedpoint.Value `json:"FasterDecreaseRatio,omitempty"` + FasterDecreaseRatio fixedpoint.Value `json:"fasterDecreaseRatio,omitempty"` // NeutralBollinger is the smaller range of the bollinger band // If price is in this band, it usually means the price is oscillating. @@ -136,9 +136,6 @@ type Strategy struct { groupID uint32 - // defaultBoll is the BOLLINGER indicator we used for predicting the price. - defaultBoll *indicator.BOLL - // neutralBoll is the neutral price section neutralBoll *indicator.BOLL From dd0f13e742817eae522139767931759707a1375c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 22 Nov 2022 11:35:32 +0800 Subject: [PATCH 08/37] strategy/linregmaker: misc --- config/linregmaker.yaml | 4 ++-- pkg/strategy/linregmaker/strategy.go | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 5a62ed8ce..7cee62504 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -18,8 +18,8 @@ backtest: # 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-01-01" - endTime: "2022-06-30" + startTime: "2022-05-01" + endTime: "2022-10-31" symbols: - BTCUSDT accounts: diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index e38f6613b..17564952d 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -17,11 +17,6 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// TODO: -// - TradeInBand: no buy order above the band, no sell order below the band -// - DynamicQuantity -// - Validate() - const ID = "linregmaker" var notionModifier = fixedpoint.NewFromFloat(1.1) @@ -115,7 +110,7 @@ type Strategy struct { DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount - + // TODO: Should work w/o dynamic qty // DynamicQuantityIncrease calculates the increase position order quantity dynamically DynamicQuantityIncrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityIncrease"` @@ -208,6 +203,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } +// TODO func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -450,7 +446,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } else if closePrice.Compare(priceReverseEMA) < 0 { s.mainTrendCurrent = types.DirectionDown } - // TODO: everything should works for both direction // Trend reversal if s.mainTrendCurrent != s.mainTrendPrevious { From 37a2fedf155e4f484eb4d038e35b25be1edad60e Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 22 Nov 2022 18:24:04 +0800 Subject: [PATCH 09/37] strategy/linregmaker: dynamic qty uses linreg slope ratio --- config/linregmaker.yaml | 25 +++++++++++++------------ pkg/bbgo/standard_indicator_set.go | 1 + pkg/dynamicmetric/dynamic_exposure.go | 9 +++++---- pkg/dynamicmetric/dynamic_quantity.go | 2 +- pkg/indicator/linreg.go | 27 +++++++++++++++++++++++++++ pkg/strategy/linregmaker/strategy.go | 2 +- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 7cee62504..63c1a224e 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -98,18 +98,19 @@ exchangeStrategies: maxExposurePosition: 10 DynamicExposure: - interval: "1h" - window: 21 - bandWidth: 2.0 - dynamicExposurePositionScale: - byPercentage: - # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale - exp: - # from lower band -100% (-1) to upper band 100% (+1) - domain: [ -1, 1 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum - range: [ 10.0, 1.0 ] + bollBandExposure: + interval: "1h" + window: 21 + bandWidth: 2.0 + dynamicExposurePositionScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ -1, 1 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 10.0, 1.0 ] # quantity is the base order quantity for your buy/sell order. quantity: 0.1 diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go index 52ecdddce..922856726 100644 --- a/pkg/bbgo/standard_indicator_set.go +++ b/pkg/bbgo/standard_indicator_set.go @@ -142,6 +142,7 @@ func (s *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) if !ok { inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth} s.initAndBind(inc, iw.Interval) + inc.SMA = &indicator.SMA{IntervalWindow: iw} if debugBOLL { inc.OnUpdate(func(sma float64, upBand float64, downBand float64) { diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go index c2e1802a1..afc442c37 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -16,10 +16,10 @@ type DynamicExposure struct { } // Initialize dynamic exposure -func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession) { +func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession, standardIndicatorSet *bbgo.StandardIndicatorSet) { switch { case d.BollBandExposure != nil: - d.BollBandExposure.initialize(symbol, session) + d.BollBandExposure.initialize(symbol, session, standardIndicatorSet) } } @@ -42,7 +42,7 @@ type DynamicExposureBollBand struct { // DynamicExposureBollBandScale is used to define the exposure range with the given percentage. DynamicExposureBollBandScale *bbgo.PercentageScale `json:"dynamicExposurePositionScale"` - *BollingerSetting + types.IntervalWindowBandWidth StandardIndicatorSet *bbgo.StandardIndicatorSet @@ -50,7 +50,8 @@ type DynamicExposureBollBand struct { } // initialize dynamic exposure with Bollinger Band -func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession) { +func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession, standardIndicatorSet *bbgo.StandardIndicatorSet) { + d.StandardIndicatorSet = standardIndicatorSet d.dynamicExposureBollBand = d.StandardIndicatorSet.BOLL(d.IntervalWindow, d.BandWidth) // Subscribe kline diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/dynamicmetric/dynamic_quantity.go index ac2e85b38..70e44797d 100644 --- a/pkg/dynamicmetric/dynamic_quantity.go +++ b/pkg/dynamicmetric/dynamic_quantity.go @@ -85,7 +85,7 @@ func (d *DynamicQuantityLinReg) initialize(symbol string, session *bbgo.Exchange // getQuantity returns quantity func (d *DynamicQuantityLinReg) getQuantity() (fixedpoint.Value, error) { - v, err := d.DynamicQuantityLinRegScale.Scale(d.QuantityLinReg.Last()) + v, err := d.DynamicQuantityLinRegScale.Scale(d.QuantityLinReg.LastRatio()) if err != nil { return fixedpoint.Zero, err } diff --git a/pkg/indicator/linreg.go b/pkg/indicator/linreg.go index d41fe9856..3bb4606ed 100644 --- a/pkg/indicator/linreg.go +++ b/pkg/indicator/linreg.go @@ -18,6 +18,9 @@ type LinReg struct { // Values are the slopes of linear regression baseline Values floats.Slice + // ValueRatios are the ratio of slope to the price + ValueRatios floats.Slice + klines types.KLineWindow EndTime time.Time @@ -32,6 +35,14 @@ func (lr *LinReg) Last() float64 { return lr.Values.Last() } +// LastRatio of slope to price +func (lr *LinReg) LastRatio() float64 { + if lr.ValueRatios.Length() == 0 { + return 0.0 + } + return lr.ValueRatios.Last() +} + // Index returns the slope of specified index func (lr *LinReg) Index(i int) float64 { if i >= lr.Values.Length() { @@ -41,11 +52,25 @@ func (lr *LinReg) Index(i int) float64 { return lr.Values.Index(i) } +// IndexRatio returns the slope ratio +func (lr *LinReg) IndexRatio(i int) float64 { + if i >= lr.ValueRatios.Length() { + return 0.0 + } + + return lr.ValueRatios.Index(i) +} + // Length of the slope values func (lr *LinReg) Length() int { return lr.Values.Length() } +// LengthRatio of the slope ratio values +func (lr *LinReg) LengthRatio() int { + return lr.ValueRatios.Length() +} + var _ types.SeriesExtend = &LinReg{} // Update Linear Regression baseline slope @@ -54,6 +79,7 @@ func (lr *LinReg) Update(kline types.KLine) { lr.klines.Truncate(lr.Window) if len(lr.klines) < lr.Window { lr.Values.Push(0) + lr.ValueRatios.Push(0) return } @@ -73,6 +99,7 @@ func (lr *LinReg) Update(kline types.KLine) { endPrice := average - slope*sumX/length + slope startPrice := endPrice + slope*(length-1) lr.Values.Push((endPrice - startPrice) / (length - 1)) + lr.ValueRatios.Push(lr.Values.Last() / kline.GetClose().Float64()) logLinReg.Debugf("linear regression baseline slope: %f", lr.Last()) } diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 17564952d..8b4b7584e 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -191,7 +191,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Setup dynamic exposure if s.DynamicExposure.IsEnabled() { - s.DynamicExposure.Initialize(s.Symbol, session) + s.DynamicExposure.Initialize(s.Symbol, session, s.StandardIndicatorSet) } // Setup dynamic quantities From e776c9e5eaca7bdd327b27bffcb74ed2bedd269b Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 12:28:38 +0800 Subject: [PATCH 10/37] strategy/linregmaker: use session standard indicator set --- pkg/dynamicmetric/dynamic_exposure.go | 11 ++++------- pkg/dynamicmetric/dynamic_spread.go | 6 ++---- pkg/strategy/linregmaker/strategy.go | 9 ++++++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go index afc442c37..dc5f51c8d 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -16,10 +16,10 @@ type DynamicExposure struct { } // Initialize dynamic exposure -func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession, standardIndicatorSet *bbgo.StandardIndicatorSet) { +func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession) { switch { case d.BollBandExposure != nil: - d.BollBandExposure.initialize(symbol, session, standardIndicatorSet) + d.BollBandExposure.initialize(symbol, session) } } @@ -44,15 +44,12 @@ type DynamicExposureBollBand struct { types.IntervalWindowBandWidth - StandardIndicatorSet *bbgo.StandardIndicatorSet - dynamicExposureBollBand *indicator.BOLL } // initialize dynamic exposure with Bollinger Band -func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession, standardIndicatorSet *bbgo.StandardIndicatorSet) { - d.StandardIndicatorSet = standardIndicatorSet - d.dynamicExposureBollBand = d.StandardIndicatorSet.BOLL(d.IntervalWindow, d.BandWidth) +func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession) { + d.dynamicExposureBollBand = session.StandardIndicatorSet(symbol).BOLL(d.IntervalWindow, d.BandWidth) // Subscribe kline session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go index a1e5e631c..6c0391c33 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -174,15 +174,13 @@ type DynamicSpreadBollWidthRatio struct { DefaultBollinger *BollingerSetting `json:"defaultBollinger"` NeutralBollinger *BollingerSetting `json:"neutralBollinger"` - StandardIndicatorSet *bbgo.StandardIndicatorSet - neutralBoll *indicator.BOLL defaultBoll *indicator.BOLL } func (ds *DynamicSpreadBollWidthRatio) initialize(symbol string, session *bbgo.ExchangeSession) { - ds.neutralBoll = ds.StandardIndicatorSet.BOLL(ds.NeutralBollinger.IntervalWindow, ds.NeutralBollinger.BandWidth) - ds.defaultBoll = ds.StandardIndicatorSet.BOLL(ds.DefaultBollinger.IntervalWindow, ds.DefaultBollinger.BandWidth) + ds.neutralBoll = session.StandardIndicatorSet(symbol).BOLL(ds.NeutralBollinger.IntervalWindow, ds.NeutralBollinger.BandWidth) + ds.defaultBoll = session.StandardIndicatorSet(symbol).BOLL(ds.DefaultBollinger.IntervalWindow, ds.DefaultBollinger.BandWidth) // Subscribe kline session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 8b4b7584e..0d84ba031 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -24,10 +24,13 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) +//TODO: Logic for backtest + func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +// TODO: Remove BollingerSetting and bollsetting.go type BollingerSetting struct { types.IntervalWindow BandWidth float64 `json:"bandWidth"` @@ -191,7 +194,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Setup dynamic exposure if s.DynamicExposure.IsEnabled() { - s.DynamicExposure.Initialize(s.Symbol, session, s.StandardIndicatorSet) + s.DynamicExposure.Initialize(s.Symbol, session) } // Setup dynamic quantities @@ -203,7 +206,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } -// TODO +// TODO Validate() func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -541,7 +544,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -// TODO +// TODO adjustOrderQuantity() func adjustOrderQuantity(submitOrder types.SubmitOrder, market types.Market) types.SubmitOrder { if submitOrder.Quantity.Mul(submitOrder.Price).Compare(market.MinNotional) < 0 { submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, market.MinNotional.Mul(notionModifier)) From cc124d42649ec68380ecb4090b66411df16371be Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 16:53:08 +0800 Subject: [PATCH 11/37] strategy/linregmaker: works w/o dynamic qty --- config/linregmaker.yaml | 2 +- pkg/dynamicmetric/bollsetting.go | 9 ----- pkg/dynamicmetric/dynamic_spread.go | 26 ++------------ pkg/strategy/linregmaker/strategy.go | 52 ++++++++++++---------------- 4 files changed, 27 insertions(+), 62 deletions(-) delete mode 100644 pkg/dynamicmetric/bollsetting.go diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 63c1a224e..80d5b1f5b 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -110,7 +110,7 @@ exchangeStrategies: domain: [ -1, 1 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum - range: [ 10.0, 1.0 ] + range: [ 1.0, 1.0 ] # quantity is the base order quantity for your buy/sell order. quantity: 0.1 diff --git a/pkg/dynamicmetric/bollsetting.go b/pkg/dynamicmetric/bollsetting.go deleted file mode 100644 index 79bff4ef1..000000000 --- a/pkg/dynamicmetric/bollsetting.go +++ /dev/null @@ -1,9 +0,0 @@ -package dynamicmetric - -import "github.com/c9s/bbgo/pkg/types" - -// BollingerSetting is for Bollinger Band settings -type BollingerSetting struct { - types.IntervalWindow - BandWidth float64 `json:"bandWidth"` -} diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go index 6c0391c33..905f49bc3 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -16,18 +16,6 @@ type DynamicSpread struct { // WeightedBollWidthRatioSpread calculates spreads based on two Bollinger Bands WeightedBollWidthRatioSpread *DynamicSpreadBollWidthRatio `json:"weightedBollWidth"` - - // deprecated - Enabled *bool `json:"enabled"` - - // deprecated - types.IntervalWindow - - // deprecated. AskSpreadScale is used to define the ask spread range with the given percentage. - AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` - - // deprecated. BidSpreadScale is used to define the bid spread range with the given percentage. - BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` } // Initialize dynamic spread @@ -37,14 +25,6 @@ func (ds *DynamicSpread) Initialize(symbol string, session *bbgo.ExchangeSession ds.AmpSpread.initialize(symbol, session) case ds.WeightedBollWidthRatioSpread != nil: ds.WeightedBollWidthRatioSpread.initialize(symbol, session) - case ds.Enabled != nil && *ds.Enabled: - // backward compatibility - ds.AmpSpread = &DynamicSpreadAmp{ - IntervalWindow: ds.IntervalWindow, - AskSpreadScale: ds.AskSpreadScale, - BidSpreadScale: ds.BidSpreadScale, - } - ds.AmpSpread.initialize(symbol, session) } } @@ -171,8 +151,8 @@ type DynamicSpreadBollWidthRatio struct { // A positive number. The greater factor, the sharper weighting function. Default set to 1.0 . Sensitivity float64 `json:"sensitivity"` - DefaultBollinger *BollingerSetting `json:"defaultBollinger"` - NeutralBollinger *BollingerSetting `json:"neutralBollinger"` + DefaultBollinger types.IntervalWindowBandWidth `json:"defaultBollinger"` + NeutralBollinger types.IntervalWindowBandWidth `json:"neutralBollinger"` neutralBoll *indicator.BOLL defaultBoll *indicator.BOLL @@ -232,7 +212,7 @@ func (ds *DynamicSpreadBollWidthRatio) getWeightedBBWidthRatio(positiveSigmoid b // - To bid spread, the weighting density function d_weight(x) is sigmoid((default_BB_mid - x) / (w / alpha)) // - The higher sensitivity factor alpha, the sharper weighting function. // - // Then calculate the weighted band width ratio by taking integral of d_weight(x) from neutral_BB_lower to neutral_BB_upper: + // Then calculate the weighted bandwidth ratio by taking integral of d_weight(x) from neutral_BB_lower to neutral_BB_upper: // infinite integral of ask spread sigmoid weighting density function F(x) = (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) // infinite integral of bid spread sigmoid weighting density function F(x) = x - (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) // Note that we've rescaled the sigmoid function to fit default BB, diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 0d84ba031..5e4ba99e6 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -25,17 +25,12 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) //TODO: Logic for backtest +// TODO: Dynamic exposure should work on both side func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } -// TODO: Remove BollingerSetting and bollsetting.go -type BollingerSetting struct { - types.IntervalWindow - BandWidth float64 `json:"bandWidth"` -} - type Strategy struct { Environment *bbgo.Environment StandardIndicatorSet *bbgo.StandardIndicatorSet @@ -77,7 +72,10 @@ type Strategy struct { // NeutralBollinger is the smaller range of the bollinger band // If price is in this band, it usually means the price is oscillating. // If price goes out of this band, we tend to not place sell orders or buy orders - NeutralBollinger *BollingerSetting `json:"neutralBollinger"` + NeutralBollinger types.IntervalWindowBandWidth `json:"neutralBollinger"` + + // neutralBoll is the neutral price section for TradeInBand + neutralBoll *indicator.BOLL // TradeInBand // When this is on, places orders only when the current price is in the bollinger band. @@ -113,7 +111,7 @@ type Strategy struct { DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount - // TODO: Should work w/o dynamic qty + // DynamicQuantityIncrease calculates the increase position order quantity dynamically DynamicQuantityIncrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityIncrease"` @@ -134,9 +132,6 @@ type Strategy struct { groupID uint32 - // neutralBoll is the neutral price section - neutralBoll *indicator.BOLL - // StrategyController bbgo.StrategyController } @@ -176,7 +171,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } // Subscribe for BBs - if s.NeutralBollinger != nil && s.NeutralBollinger.Interval != "" { + if s.NeutralBollinger.Interval != "" { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: s.NeutralBollinger.Interval, }) @@ -276,39 +271,38 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { // TODO: spot, margin, and futures + // Default + sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + // Dynamic qty switch { case s.mainTrendCurrent == types.DirectionUp: - var err error if len(s.DynamicQuantityIncrease) > 0 { - buyQuantity, err = s.DynamicQuantityIncrease.GetQuantity() - if err != nil { - buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + qty, err := s.DynamicQuantityIncrease.GetQuantity() + if err == nil { + buyQuantity = qty } } if len(s.DynamicQuantityDecrease) > 0 { - sellQuantity, err = s.DynamicQuantityDecrease.GetQuantity() - if err != nil { - sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + qty, err := s.DynamicQuantityDecrease.GetQuantity() + if err == nil { + sellQuantity = qty } } case s.mainTrendCurrent == types.DirectionDown: - var err error if len(s.DynamicQuantityIncrease) > 0 { - sellQuantity, err = s.DynamicQuantityIncrease.GetQuantity() - if err != nil { - sellQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + qty, err := s.DynamicQuantityIncrease.GetQuantity() + if err == nil { + sellQuantity = qty } } if len(s.DynamicQuantityDecrease) > 0 { - buyQuantity, err = s.DynamicQuantityDecrease.GetQuantity() - if err != nil { - buyQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + qty, err := s.DynamicQuantityDecrease.GetQuantity() + if err == nil { + buyQuantity = qty } } - default: - sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) - buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) } // Faster position decrease From fbc949a133f695cdb3fbb241b791bbe612c3e96f Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 16:58:24 +0800 Subject: [PATCH 12/37] strategy/linregmaker: validate basic config parameters --- pkg/strategy/linregmaker/strategy.go | 48 ++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 5e4ba99e6..cca769c77 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -54,12 +54,12 @@ type Strategy struct { // FastLinReg is to determine the short-term trend. // Buy/sell orders are placed if the FastLinReg and the ReverseEMA trend are in the same direction, and only orders // that reduce position are placed if the FastLinReg and the ReverseEMA trend are in different directions. - FastLinReg *indicator.LinReg `json:"fastLinReg,omitempty"` + FastLinReg *indicator.LinReg `json:"fastLinReg"` // SlowLinReg is to determine the midterm trend. // When the SlowLinReg and the ReverseEMA trend are in different directions, creation of opposite position is // allowed. - SlowLinReg *indicator.LinReg `json:"slowLinReg,omitempty"` + SlowLinReg *indicator.LinReg `json:"slowLinReg"` // AllowOppositePosition if true, the creation of opposite position is allowed when both fast and slow LinReg are in // the opposite direction to main trend @@ -90,7 +90,8 @@ type Strategy struct { // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) // Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 - Spread fixedpoint.Value `json:"spread"` + // TODO: if nil? + Spread fixedpoint.Value `json:"spread,omitempty"` // BidSpread overrides the spread setting, this spread will be used for the buy order BidSpread fixedpoint.Value `json:"bidSpread,omitempty"` @@ -104,7 +105,8 @@ type Strategy struct { // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum - MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` + // TODO: if nil? + MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition,omitempty"` // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically @@ -144,6 +146,35 @@ func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s:%s", ID, s.Symbol) } +// Validate basic config parameters. TODO LATER: Validate more +func (s *Strategy) Validate() error { + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + if len(s.Interval) == 0 { + return errors.New("interval is required") + } + + if s.Window <= 0 { + return errors.New("window must be more than 0") + } + + if s.ReverseEMA == nil { + return errors.New("reverseEMA must be set") + } + + if s.FastLinReg == nil { + return errors.New("fastLinReg must be set") + } + + if s.SlowLinReg == nil { + return errors.New("slowLinReg must be set") + } + + return nil +} + func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Subscribe for ReverseEMA session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ @@ -201,15 +232,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } -// TODO Validate() -func (s *Strategy) Validate() error { - if len(s.Symbol) == 0 { - return errors.New("symbol is required") - } - - return nil -} - func (s *Strategy) CurrentPosition() *types.Position { return s.Position } From 0f0549fa42b56b4bcefe7ffc45d0ddd20563cb94 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 17:23:18 +0800 Subject: [PATCH 13/37] strategy/linregmaker: dynamic exposure works on both direction --- config/linregmaker.yaml | 2 +- pkg/dynamicmetric/dynamic_exposure.go | 11 ++++++++--- pkg/strategy/linregmaker/strategy.go | 16 ++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 80d5b1f5b..a550aaee0 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -110,7 +110,7 @@ exchangeStrategies: domain: [ -1, 1 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum - range: [ 1.0, 1.0 ] + range: [ 0.1, 10 ] # quantity is the base order quantity for your buy/sell order. quantity: 0.1 diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go index dc5f51c8d..d150e11d9 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -28,10 +28,10 @@ func (d *DynamicExposure) IsEnabled() bool { } // GetMaxExposure returns the max exposure -func (d *DynamicExposure) GetMaxExposure(price float64) (maxExposure fixedpoint.Value, err error) { +func (d *DynamicExposure) GetMaxExposure(price float64, trend types.Direction) (maxExposure fixedpoint.Value, err error) { switch { case d.BollBandExposure != nil: - return d.BollBandExposure.getMaxExposure(price) + return d.BollBandExposure.getMaxExposure(price, trend) default: return fixedpoint.Zero, errors.New("dynamic exposure is not enabled") } @@ -58,7 +58,7 @@ func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.Exchan } // getMaxExposure returns the max exposure -func (d *DynamicExposureBollBand) getMaxExposure(price float64) (fixedpoint.Value, error) { +func (d *DynamicExposureBollBand) getMaxExposure(price float64, trend types.Direction) (fixedpoint.Value, error) { downBand := d.dynamicExposureBollBand.DownBand.Last() upBand := d.dynamicExposureBollBand.UpBand.Last() sma := d.dynamicExposureBollBand.SMA.Last() @@ -73,6 +73,11 @@ func (d *DynamicExposureBollBand) getMaxExposure(price float64) (fixedpoint.Valu bandPercentage = (price - sma) / math.Abs(upBand-sma) } + // Reverse if downtrend + if trend == types.DirectionDown { + bandPercentage = 0 - bandPercentage + } + v, err := d.DynamicExposureBollBandScale.Scale(bandPercentage) if err != nil { return fixedpoint.Zero, err diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index cca769c77..1ce8481c9 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -24,8 +24,7 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) -//TODO: Logic for backtest -// TODO: Dynamic exposure should work on both side +// TODO: Logic for backtest func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -81,7 +80,7 @@ type Strategy struct { // When this is on, places orders only when the current price is in the bollinger band. TradeInBand bool `json:"tradeInBand"` - // useTickerPrice use the ticker api to get the mid price instead of the closed kline price. + // useTickerPrice use the ticker api to get the mid-price instead of the closed kline price. // The back-test engine is kline-based, so the ticker price api is not supported. // Turn this on if you want to do real trading. useTickerPrice bool @@ -146,7 +145,8 @@ func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s:%s", ID, s.Symbol) } -// Validate basic config parameters. TODO LATER: Validate more +// Validate basic config parameters +// TODO LATER: Validate more func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -156,10 +156,6 @@ func (s *Strategy) Validate() error { return errors.New("interval is required") } - if s.Window <= 0 { - return errors.New("window must be more than 0") - } - if s.ReverseEMA == nil { return errors.New("reverseEMA must be set") } @@ -270,7 +266,7 @@ func (s *Strategy) updateMaxExposure(midPrice fixedpoint.Value) { // Calculate max exposure if s.DynamicExposure.IsEnabled() { var err error - maxExposurePosition, err := s.DynamicExposure.GetMaxExposure(midPrice.Float64()) + maxExposurePosition, err := s.DynamicExposure.GetMaxExposure(midPrice.Float64(), s.mainTrendCurrent) if err != nil { log.WithError(err).Errorf("can not calculate DynamicExposure of %s, use previous MaxExposurePosition instead", s.Symbol) } else { @@ -560,7 +556,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -// TODO adjustOrderQuantity() +// adjustOrderQuantity to meet the min notional and qty requirement func adjustOrderQuantity(submitOrder types.SubmitOrder, market types.Market) types.SubmitOrder { if submitOrder.Quantity.Mul(submitOrder.Price).Compare(market.MinNotional) < 0 { submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, market.MinNotional.Mul(notionModifier)) From 41e27a8e382a6a0c7243e32aca15991772d79618 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 17:44:40 +0800 Subject: [PATCH 14/37] strategy/linregmaker: default value of spread --- config/linregmaker.yaml | 10 +++++++--- pkg/strategy/linregmaker/strategy.go | 11 +++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index a550aaee0..6b7eda488 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -27,7 +27,7 @@ backtest: makerCommission: 10 # 0.15% takerCommission: 15 # 0.15% balances: - BTC: 10.0 + BTC: 2.0 USDT: 10000.0 exchangeStrategies: @@ -70,7 +70,6 @@ exchangeStrategies: # spread spread: 0.1% - # dynamicSpread dynamicSpread: amplitude: # delete other scaling strategy if this is defined @@ -96,7 +95,7 @@ exchangeStrategies: # when in up band, holds 0.05 by maximum range: [ 0.001, 0.002 ] - maxExposurePosition: 10 + #maxExposurePosition: 10 DynamicExposure: bollBandExposure: interval: "1h" @@ -142,3 +141,8 @@ exchangeStrategies: # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] + + exits: + # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) + - roiStopLoss: + percentage: 20% diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 1ce8481c9..c7492034a 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -89,8 +89,7 @@ type Strategy struct { // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) // Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 - // TODO: if nil? - Spread fixedpoint.Value `json:"spread,omitempty"` + Spread fixedpoint.Value `json:"spread"` // BidSpread overrides the spread setting, this spread will be used for the buy order BidSpread fixedpoint.Value `json:"bidSpread,omitempty"` @@ -104,8 +103,7 @@ type Strategy struct { // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum - // TODO: if nil? - MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition,omitempty"` + MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically @@ -429,6 +427,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.useTickerPrice = true } + // Default spread + if s.Spread == fixedpoint.Zero { + s.Spread = fixedpoint.NewFromFloat(0.001) + } + // StrategyController s.Status = types.StrategyStatusRunning s.OnSuspend(func() { From 8c57dec793fa701be9337eefff8168ca3cb2aecd Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 24 Nov 2022 16:51:37 +0800 Subject: [PATCH 15/37] strategy/linregmaker: parameter of check main trend interval --- config/linregmaker.yaml | 19 ++++++++------ pkg/strategy/linregmaker/strategy.go | 38 +++++++++++++++++++++------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 6b7eda488..2a4f97233 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -43,15 +43,18 @@ exchangeStrategies: interval: 1d window: 60 + # reverseInterval + reverseInterval: 4h + # fastLinReg fastLinReg: interval: 1m - window: 20 + window: 30 # slowLinReg slowLinReg: interval: 1m - window: 60 + window: 120 # allowOppositePosition allowOppositePosition: true @@ -61,7 +64,7 @@ exchangeStrategies: # neutralBollinger neutralBollinger: - interval: "5m" + interval: "15m" window: 21 bandWidth: 2.0 @@ -120,8 +123,8 @@ exchangeStrategies: window: 20 dynamicQuantityLinRegScale: byPercentage: - # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale - exp: + # log means we want to use log scale, you can replace "log" with "linear" for linear scale + linear: # from lower band -100% (-1) to upper band 100% (+1) domain: [ -1, 1 ] # when in down band, holds 1.0 by maximum @@ -134,8 +137,8 @@ exchangeStrategies: window: 20 dynamicQuantityLinRegScale: byPercentage: - # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale - exp: + # log means we want to use log scale, you can replace "log" with "linear" for linear scale + linear: # from lower band -100% (-1) to upper band 100% (+1) domain: [ 1, -1 ] # when in down band, holds 1.0 by maximum @@ -145,4 +148,4 @@ exchangeStrategies: exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) - roiStopLoss: - percentage: 20% + percentage: 30% diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index c7492034a..ec11d95b9 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -45,6 +45,10 @@ type Strategy struct { // All the opposite trend position will be closed upon the trend change ReverseEMA *indicator.EWMA `json:"reverseEMA"` + // ReverseInterval is the interval to check trend reverse against ReverseEMA. Close price of this interval crossing + // the ReverseEMA triggers main trend change. + ReverseInterval types.Interval `json:"reverseInterval"` + // mainTrendCurrent is the current long-term trend mainTrendCurrent types.Direction // mainTrendPrevious is the long-term trend of previous kline @@ -158,6 +162,11 @@ func (s *Strategy) Validate() error { return errors.New("reverseEMA must be set") } + // Use interval of ReverseEMA if ReverseInterval is omitted + if s.ReverseInterval == "" { + s.ReverseInterval = s.ReverseEMA.Interval + } + if s.FastLinReg == nil { return errors.New("fastLinReg must be set") } @@ -177,6 +186,11 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Initialize ReverseEMA s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow) + // Subscribe for ReverseInterval + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.ReverseInterval, + }) + // Subscribe for LinRegs session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: s.FastLinReg.Interval, @@ -443,15 +457,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se _ = s.ClosePosition(ctx, fixedpoint.NewFromFloat(1.0)) }) - // Main interval - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - // StrategyController - if s.Status != types.StrategyStatusRunning { - return - } - - _ = s.orderExecutor.GracefulCancel(ctx) - + // Check trend reversal + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.ReverseInterval, func(kline types.KLine) { // closePrice is the close price of current kline closePrice := kline.GetClose() // priceReverseEMA is the current ReverseEMA price @@ -464,6 +471,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } else if closePrice.Compare(priceReverseEMA) < 0 { s.mainTrendCurrent = types.DirectionDown } + })) + + // Main interval + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + _ = s.orderExecutor.GracefulCancel(ctx) + + // closePrice is the close price of current kline + closePrice := kline.GetClose() // Trend reversal if s.mainTrendCurrent != s.mainTrendPrevious { From 66f0f3e1138e6d3c22af5ed665b409dcfca67132 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 24 Nov 2022 17:06:14 +0800 Subject: [PATCH 16/37] strategy/linregmaker: remove useTickerPrice --- pkg/strategy/linregmaker/strategy.go | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index ec11d95b9..59ad728e1 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -84,11 +84,6 @@ type Strategy struct { // When this is on, places orders only when the current price is in the bollinger band. TradeInBand bool `json:"tradeInBand"` - // useTickerPrice use the ticker api to get the mid-price instead of the closed kline price. - // The back-test engine is kline-based, so the ticker price api is not supported. - // Turn this on if you want to do real trading. - useTickerPrice bool - // Spread is the price spread from the middle price. // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) @@ -148,7 +143,6 @@ func (s *Strategy) InstanceID() string { } // Validate basic config parameters -// TODO LATER: Validate more func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -162,11 +156,6 @@ func (s *Strategy) Validate() error { return errors.New("reverseEMA must be set") } - // Use interval of ReverseEMA if ReverseInterval is omitted - if s.ReverseInterval == "" { - s.ReverseInterval = s.ReverseEMA.Interval - } - if s.FastLinReg == nil { return errors.New("fastLinReg must be set") } @@ -186,7 +175,10 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Initialize ReverseEMA s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow) - // Subscribe for ReverseInterval + // Subscribe for ReverseInterval. Use interval of ReverseEMA if ReverseInterval is omitted + if s.ReverseInterval == "" { + s.ReverseInterval = s.ReverseEMA.Interval + } session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: s.ReverseInterval, }) @@ -435,12 +427,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.ExitMethods.Bind(session, s.orderExecutor) - if bbgo.IsBackTesting { - s.useTickerPrice = false - } else { - s.useTickerPrice = true - } - // Default spread if s.Spread == fixedpoint.Zero { s.Spread = fixedpoint.NewFromFloat(0.001) @@ -501,7 +487,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // midPrice for ask and bid prices var midPrice fixedpoint.Value - if s.useTickerPrice { + if !bbgo.IsBackTesting { ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { return From 5c60ad0e416dc0a0009177faec8c8768c1996c2d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 25 Nov 2022 12:27:47 +0800 Subject: [PATCH 17/37] strategy/linregmaker: re-organize strategy logic --- pkg/strategy/linregmaker/strategy.go | 195 ++++++++++++++++++++++----- 1 file changed, 159 insertions(+), 36 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 59ad728e1..141e39932 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -25,6 +25,7 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) // TODO: Logic for backtest +// TODO: initial trend func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -240,6 +241,20 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu return s.orderExecutor.ClosePosition(ctx, percentage) } +// isAllowOppositePosition returns if opening opposite position is allowed +func (s *Strategy) isAllowOppositePosition() bool { + if !s.AllowOppositePosition { + return false + } + + if (s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0) || + (s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0) { + return true + } + + return false +} + // updateSpread for ask and bid price func (s *Strategy) updateSpread() { // Update spreads with dynamic spread @@ -291,8 +306,6 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin // getOrderQuantities returns sell and buy qty func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { - // TODO: spot, margin, and futures - // Default sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) @@ -328,10 +341,21 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } // Faster position decrease - if s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0 { - sellQuantity = sellQuantity * s.FasterDecreaseRatio - } else if s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0 { - buyQuantity = buyQuantity * s.FasterDecreaseRatio + if s.isAllowOppositePosition() { + if s.mainTrendCurrent == types.DirectionUp { + sellQuantity = sellQuantity * s.FasterDecreaseRatio + } else if s.mainTrendCurrent == types.DirectionDown { + buyQuantity = buyQuantity * s.FasterDecreaseRatio + } + } + + // Reduce order qty to fit current position + if !s.isAllowOppositePosition() { + if s.Position.IsLong() && s.Position.Base.Abs().Compare(sellQuantity) < 0 { + sellQuantity = s.Position.Base.Abs() + } else if s.Position.IsShort() && s.Position.Base.Abs().Compare(buyQuantity) < 0 { + buyQuantity = s.Position.Base.Abs() + } } log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) @@ -339,8 +363,42 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp return sellQuantity, buyQuantity } +// getAllowedBalance returns the allowed qty of orders +// TODO LATER: Check max qty of margin and futures +func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { + // Default + baseQty = fixedpoint.PosInf + quoteQty = fixedpoint.PosInf + + balances := s.session.GetAccount().Balances() + baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] + quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] + + isMargin := s.session.Margin || s.session.IsolatedMargin + isFutures := s.session.Futures || s.session.IsolatedFutures + + if isMargin { + + } else if isFutures { + + } else { + if !hasBaseBalance { + baseQty = fixedpoint.Zero + } else { + baseQty = baseBalance.Available + } + if !hasQuoteBalance { + quoteQty = fixedpoint.Zero + } else { + quoteQty = quoteBalance.Available + } + } + + return baseQty, quoteQty +} + // getCanBuySell returns the buy sell switches -func (s *Strategy) getCanBuySell(midPrice fixedpoint.Value) (canBuy bool, canSell bool) { +func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice fixedpoint.Value) (canBuy bool, canSell bool) { // By default, both buy and sell are on, which means we will place buy and sell orders canBuy = true canSell = true @@ -352,33 +410,118 @@ func (s *Strategy) getCanBuySell(midPrice fixedpoint.Value) (canBuy bool, canSel } else if s.mainTrendCurrent == types.DirectionDown { canSell = false } + log.Infof("current position %v larger than max exposure %v, skip increase position", s.Position.GetBase().Abs(), s.MaxExposurePosition) } + // Check TradeInBand if s.TradeInBand { // Price too high - if midPrice.Float64() > s.neutralBoll.UpBand.Last() { + if bidPrice.Float64() > s.neutralBoll.UpBand.Last() { canBuy = false log.Infof("tradeInBand is set, skip buy when the price is higher than the neutralBB") } // Price too low in uptrend - if midPrice.Float64() < s.neutralBoll.DownBand.Last() { + if askPrice.Float64() < s.neutralBoll.DownBand.Last() { canSell = false log.Infof("tradeInBand is set, skip sell when the price is lower than the neutralBB") } } // Stop decrease when position closed unless both LinRegs are in the opposite direction to the main trend - if s.Position.IsClosed() || s.Position.IsDust(midPrice) { - if s.mainTrendCurrent == types.DirectionUp && !(s.AllowOppositePosition && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0) { + if !s.isAllowOppositePosition() { + if s.mainTrendCurrent == types.DirectionUp && (s.Position.IsClosed() || s.Position.IsDust(askPrice)) { canSell = false - } else if s.mainTrendCurrent == types.DirectionDown && !(s.AllowOppositePosition && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0) { + } else if s.mainTrendCurrent == types.DirectionDown && (s.Position.IsClosed() || s.Position.IsDust(bidPrice)) { canBuy = false } } + // Check against account balance + baseQty, quoteQty := s.getAllowedBalance() + if buyQuantity.Compare(quoteQty.Div(bidPrice)) > 0 { + canBuy = false + } + if sellQuantity.Compare(baseQty) > 0 { + canSell = false + } + return canBuy, canSell } +// getOrderForms returns buy and sell order form for submission +// TODO: Simplify +func (s *Strategy) getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice fixedpoint.Value) (buyOrder types.SubmitOrder, sellOrder types.SubmitOrder) { + sellOrder = types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: sellQuantity, + Price: askPrice, + Market: s.Market, + GroupID: s.groupID, + } + buyOrder = types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: buyQuantity, + Price: bidPrice, + Market: s.Market, + GroupID: s.groupID, + } + + isMargin := s.session.Margin || s.session.IsolatedMargin + isFutures := s.session.Futures || s.session.IsolatedFutures + + if s.Position.IsClosed() { + if isMargin { + buyOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + sellOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + } else if isFutures { + buyOrder.ReduceOnly = false + sellOrder.ReduceOnly = false + } + } else if s.Position.IsLong() { + if isMargin { + buyOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + sellOrder.MarginSideEffect = types.SideEffectTypeAutoRepay + } else if isFutures { + buyOrder.ReduceOnly = false + sellOrder.ReduceOnly = true + } + + if s.Position.Base.Abs().Compare(sellOrder.Quantity) < 0 { + if isMargin { + sellOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + } else if isFutures { + sellOrder.ReduceOnly = false + } + } + } else if s.Position.IsShort() { + if isMargin { + buyOrder.MarginSideEffect = types.SideEffectTypeAutoRepay + sellOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + } else if isFutures { + buyOrder.ReduceOnly = true + sellOrder.ReduceOnly = false + } + + if s.Position.Base.Abs().Compare(buyOrder.Quantity) < 0 { + if isMargin { + sellOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + } else if isFutures { + sellOrder.ReduceOnly = false + } + } + } + + // TODO: Move these to qty calculation + sellOrder = adjustOrderQuantity(sellOrder, s.Market) + buyOrder = adjustOrderQuantity(buyOrder, s.Market) + + return buyOrder, sellOrder +} + func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { // initial required information s.session = session @@ -517,37 +660,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Order qty sellQuantity, buyQuantity := s.getOrderQuantities(askPrice, bidPrice) - // TODO: Reduce only in margin and futures - sellOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Quantity: sellQuantity, - Price: askPrice, - Market: s.Market, - GroupID: s.groupID, - } - buyOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimitMaker, - Quantity: buyQuantity, - Price: bidPrice, - Market: s.Market, - GroupID: s.groupID, - } + buyOrder, sellOrder := s.getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice) - canBuy, canSell := s.getCanBuySell(midPrice) - - // TODO: check enough balance? + canBuy, canSell := s.getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice) // Submit orders var submitOrders []types.SubmitOrder if canSell { - submitOrders = append(submitOrders, adjustOrderQuantity(sellOrder, s.Market)) + submitOrders = append(submitOrders, sellOrder) } if canBuy { - submitOrders = append(submitOrders, adjustOrderQuantity(buyOrder, s.Market)) + submitOrders = append(submitOrders, buyOrder) } if len(submitOrders) == 0 { From 02a67a3de86d1cfe5fa913e933df3a4fde0a19d5 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 25 Nov 2022 12:38:28 +0800 Subject: [PATCH 18/37] strategy/linregmaker: initial trend --- pkg/strategy/linregmaker/strategy.go | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 141e39932..a694dc5b3 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -25,7 +25,6 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) // TODO: Logic for backtest -// TODO: initial trend func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -586,6 +585,31 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se _ = s.ClosePosition(ctx, fixedpoint.NewFromFloat(1.0)) }) + // Initial trend + session.UserDataStream.OnStart(func() { + var closePrice fixedpoint.Value + if !bbgo.IsBackTesting { + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + return + } + + closePrice = ticker.Buy.Add(ticker.Sell).Div(two) + } else { + if price, ok := session.LastPrice(s.Symbol); ok { + closePrice = price + } + } + priceReverseEMA := fixedpoint.NewFromFloat(s.ReverseEMA.Last()) + + // Main trend by ReverseEMA + if closePrice.Compare(priceReverseEMA) > 0 { + s.mainTrendCurrent = types.DirectionUp + } else if closePrice.Compare(priceReverseEMA) < 0 { + s.mainTrendCurrent = types.DirectionDown + } + }) + // Check trend reversal session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.ReverseInterval, func(kline types.KLine) { // closePrice is the close price of current kline From 71137620bd4c1299def81cb60dc38f79b0716b04 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 25 Nov 2022 16:39:15 +0800 Subject: [PATCH 19/37] strategy/linregmaker: qty calculation for backtest --- config/linregmaker.yaml | 4 +-- pkg/strategy/linregmaker/strategy.go | 52 ++++++++++++++++------------ 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 2a4f97233..5b5e59db7 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -126,7 +126,7 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [ -1, 1 ] + domain: [ -0.05, 0.05 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] @@ -140,7 +140,7 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [ 1, -1 ] + domain: [0.05, -0.05 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index a694dc5b3..a3dc3fe4c 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -24,7 +24,7 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) -// TODO: Logic for backtest +// TODO: Check logic of dynamic qty func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -303,6 +303,20 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin return askPrice, bidPrice } +// adjustQuantity to meet the min notional and qty requirement +func (s *Strategy) adjustQuantity(quantity, price fixedpoint.Value) fixedpoint.Value { + adjustedQty := quantity + if quantity.Mul(price).Compare(s.Market.MinNotional) < 0 { + adjustedQty = bbgo.AdjustFloatQuantityByMinAmount(quantity, price, s.Market.MinNotional.Mul(notionModifier)) + } + + if adjustedQty.Compare(s.Market.MinQuantity) < 0 { + adjustedQty = fixedpoint.Max(adjustedQty, s.Market.MinQuantity) + } + + return adjustedQty +} + // getOrderQuantities returns sell and buy qty func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { // Default @@ -357,6 +371,9 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } + buyQuantity = s.adjustQuantity(buyQuantity, bidPrice) + sellQuantity = s.adjustQuantity(sellQuantity, askPrice) + log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) return sellQuantity, buyQuantity @@ -373,12 +390,18 @@ func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] - isMargin := s.session.Margin || s.session.IsolatedMargin - isFutures := s.session.Futures || s.session.IsolatedFutures + if bbgo.IsBackTesting { + if !hasQuoteBalance { + baseQty = fixedpoint.Zero + quoteQty = fixedpoint.Zero + } else { + lastPrice, _ := s.session.LastPrice(s.Symbol) + baseQty = quoteBalance.Available.Div(lastPrice) + quoteQty = quoteBalance.Available + } + } else if s.session.Margin || s.session.IsolatedMargin { - if isMargin { - - } else if isFutures { + } else if s.session.Futures || s.session.IsolatedFutures { } else { if !hasBaseBalance { @@ -514,10 +537,6 @@ func (s *Strategy) getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice f } } - // TODO: Move these to qty calculation - sellOrder = adjustOrderQuantity(sellOrder, s.Market) - buyOrder = adjustOrderQuantity(buyOrder, s.Market) - return buyOrder, sellOrder } @@ -711,16 +730,3 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } - -// adjustOrderQuantity to meet the min notional and qty requirement -func adjustOrderQuantity(submitOrder types.SubmitOrder, market types.Market) types.SubmitOrder { - if submitOrder.Quantity.Mul(submitOrder.Price).Compare(market.MinNotional) < 0 { - submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, market.MinNotional.Mul(notionModifier)) - } - - if submitOrder.Quantity.Compare(market.MinQuantity) < 0 { - submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, market.MinQuantity) - } - - return submitOrder -} From 19d4033aaa26816295c9445966dc82390c39b8f6 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 30 Nov 2022 13:00:29 +0800 Subject: [PATCH 20/37] strategy/linregmaker: adj config --- config/linregmaker.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 5b5e59db7..84bbc4950 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -126,7 +126,7 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [ -0.05, 0.05 ] + domain: [ -0.02, 0.02 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] @@ -140,7 +140,7 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [0.05, -0.05 ] + domain: [0.02, -0.02 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] From a6956e50b78a6098c52d987056391aeb39215f3c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 12 Dec 2022 18:23:49 +0800 Subject: [PATCH 21/37] strategy/linregmaker: add more logs --- config/linregmaker.yaml | 10 +++--- pkg/strategy/linregmaker/strategy.go | 54 +++++++++++++++++----------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 84bbc4950..994e5b80e 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -41,7 +41,7 @@ exchangeStrategies: # reverseEMA reverseEMA: interval: 1d - window: 60 + window: 5 # reverseInterval reverseInterval: 4h @@ -126,10 +126,10 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [ -0.02, 0.02 ] + domain: [ -0.000002, 0.000002 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum - range: [ 0, 0.1 ] + range: [ 0, 0.001 ] dynamicQuantityDecrease: - linRegDynamicQuantity: quantityLinReg: @@ -140,10 +140,10 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [0.02, -0.02 ] + domain: [0.000002, -0.000002 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum - range: [ 0, 0.1 ] + range: [ 0, 0.001 ] exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index a3dc3fe4c..7421eb196 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -25,6 +25,7 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) // TODO: Check logic of dynamic qty +// TODO: Add tg logs func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -248,8 +249,10 @@ func (s *Strategy) isAllowOppositePosition() bool { if (s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0) || (s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0) { + log.Infof("%s allow opposite position is enabled: MainTrend %v, FastLinReg: %f, SlowLinReg: %f", s.Symbol, s.mainTrendCurrent, s.FastLinReg.Last(), s.SlowLinReg.Last()) return true } + log.Infof("%s allow opposite position is disabled: MainTrend %v, FastLinReg: %f, SlowLinReg: %f", s.Symbol, s.mainTrendCurrent, s.FastLinReg.Last(), s.SlowLinReg.Last()) return false } @@ -298,7 +301,7 @@ func (s *Strategy) updateMaxExposure(midPrice fixedpoint.Value) { func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoint.Value, bidPrice fixedpoint.Value) { askPrice = midPrice.Mul(fixedpoint.One.Add(s.AskSpread)) bidPrice = midPrice.Mul(fixedpoint.One.Sub(s.BidSpread)) - log.Infof("mid price:%v ask:%v bid: %v", midPrice, askPrice, bidPrice) + log.Infof("%s mid price:%v ask:%v bid: %v", s.Symbol, midPrice, askPrice, bidPrice) return askPrice, bidPrice } @@ -352,14 +355,16 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } } + log.Infof("%s caculated buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) // Faster position decrease if s.isAllowOppositePosition() { if s.mainTrendCurrent == types.DirectionUp { - sellQuantity = sellQuantity * s.FasterDecreaseRatio + sellQuantity = sellQuantity.Mul(s.FasterDecreaseRatio) } else if s.mainTrendCurrent == types.DirectionDown { - buyQuantity = buyQuantity * s.FasterDecreaseRatio + buyQuantity = buyQuantity.Mul(s.FasterDecreaseRatio) } + log.Infof("%s faster position decrease: buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) } // Reduce order qty to fit current position @@ -371,10 +376,14 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } - buyQuantity = s.adjustQuantity(buyQuantity, bidPrice) - sellQuantity = s.adjustQuantity(sellQuantity, askPrice) + if buyQuantity.Compare(fixedpoint.Zero) > 0 { + buyQuantity = s.adjustQuantity(buyQuantity, bidPrice) + } + if sellQuantity.Compare(fixedpoint.Zero) > 0 { + sellQuantity = s.adjustQuantity(sellQuantity, askPrice) + } - log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) + log.Infof("adjusted sell qty:%v buy qty: %v", sellQuantity, buyQuantity) return sellQuantity, buyQuantity } @@ -467,6 +476,7 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f canSell = false } + log.Infof("canBuy %t, canSell %t", canBuy, canSell) return canBuy, canSell } @@ -643,6 +653,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } else if closePrice.Compare(priceReverseEMA) < 0 { s.mainTrendCurrent = types.DirectionDown } + log.Infof("%s current trend is %v", s.Symbol, s.mainTrendCurrent) + + // Trend reversal + if s.mainTrendCurrent != s.mainTrendPrevious { + // Close on-hand position that is not in the same direction as the new trend + if !s.Position.IsDust(closePrice) && + ((s.Position.IsLong() && s.mainTrendCurrent == types.DirectionDown) || + (s.Position.IsShort() && s.mainTrendCurrent == types.DirectionUp)) { + log.Infof("%s trend reverse to %v. closing on-hand position", s.Symbol, s.mainTrendCurrent) + bbgo.Notify("%s trend reverse to %v. closing on-hand position", s.Symbol, s.mainTrendCurrent) + if err := s.ClosePosition(ctx, fixedpoint.One); err != nil { + log.WithError(err).Errorf("cannot close on-hand position of %s", s.Symbol) + bbgo.Notify("cannot close on-hand position of %s", s.Symbol) + // TODO: close position failed. retry? + } + } + } })) // Main interval @@ -657,20 +684,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // closePrice is the close price of current kline closePrice := kline.GetClose() - // Trend reversal - if s.mainTrendCurrent != s.mainTrendPrevious { - // Close on-hand position that is not in the same direction as the new trend - if !s.Position.IsDust(closePrice) && - ((s.Position.IsLong() && s.mainTrendCurrent == types.DirectionDown) || - (s.Position.IsShort() && s.mainTrendCurrent == types.DirectionUp)) { - log.Infof("trend reverse to %v. closing on-hand position", s.mainTrendCurrent) - if err := s.ClosePosition(ctx, fixedpoint.One); err != nil { - log.WithError(err).Errorf("cannot close on-hand position of %s", s.Symbol) - // TODO: close position failed. retry? - } - } - } - // midPrice for ask and bid prices var midPrice fixedpoint.Value if !bbgo.IsBackTesting { @@ -719,6 +732,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if len(submitOrders) == 0 { return } + log.Infof("submitting order(s): %v", submitOrders) _, _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...) })) From 79dcda5f5282e3014276487f5492bbb4b9c4b5cf Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 11:06:18 +0800 Subject: [PATCH 22/37] strategy/linregmaker: add more trend reverse logs --- config/linregmaker.yaml | 6 +++--- pkg/strategy/linregmaker/strategy.go | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 994e5b80e..a88da8b50 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -18,8 +18,8 @@ backtest: # 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-05-01" - endTime: "2022-10-31" + startTime: "2022-03-15" + endTime: "2022-03-23" symbols: - BTCUSDT accounts: @@ -41,7 +41,7 @@ exchangeStrategies: # reverseEMA reverseEMA: interval: 1d - window: 5 + window: 60 # reverseInterval reverseInterval: 4h diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 7421eb196..be5c3e20e 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -657,12 +657,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Trend reversal if s.mainTrendCurrent != s.mainTrendPrevious { + log.Infof("%s trend reverse to %v", s.Symbol, s.mainTrendCurrent) // Close on-hand position that is not in the same direction as the new trend if !s.Position.IsDust(closePrice) && ((s.Position.IsLong() && s.mainTrendCurrent == types.DirectionDown) || (s.Position.IsShort() && s.mainTrendCurrent == types.DirectionUp)) { - log.Infof("%s trend reverse to %v. closing on-hand position", s.Symbol, s.mainTrendCurrent) - bbgo.Notify("%s trend reverse to %v. closing on-hand position", s.Symbol, s.mainTrendCurrent) + log.Infof("%s closing on-hand position due to trend reverse", s.Symbol) + bbgo.Notify("%s closing on-hand position due to trend reverse", s.Symbol) if err := s.ClosePosition(ctx, fixedpoint.One); err != nil { log.WithError(err).Errorf("cannot close on-hand position of %s", s.Symbol) bbgo.Notify("cannot close on-hand position of %s", s.Symbol) From 30f3ef2180c37fc80bf0bbd06a5c80e24198fb07 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 12:12:46 +0800 Subject: [PATCH 23/37] strategy/linregmaker: add more tg notification --- pkg/strategy/linregmaker/strategy.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index be5c3e20e..afd58cec2 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -24,9 +24,6 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) -// TODO: Check logic of dynamic qty -// TODO: Add tg logs - func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } @@ -290,6 +287,7 @@ func (s *Strategy) updateMaxExposure(midPrice fixedpoint.Value) { maxExposurePosition, err := s.DynamicExposure.GetMaxExposure(midPrice.Float64(), s.mainTrendCurrent) if err != nil { log.WithError(err).Errorf("can not calculate DynamicExposure of %s, use previous MaxExposurePosition instead", s.Symbol) + bbgo.Notify("can not calculate DynamicExposure of %s, use previous MaxExposurePosition instead", s.Symbol) } else { s.MaxExposurePosition = maxExposurePosition } @@ -333,12 +331,18 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp qty, err := s.DynamicQuantityIncrease.GetQuantity() if err == nil { buyQuantity = qty + } else { + log.WithError(err).Errorf("cannot get dynamic buy qty of %s, use default qty instead", s.Symbol) + bbgo.Notify("cannot get dynamic buy qty of %s, use default qty instead", s.Symbol) } } if len(s.DynamicQuantityDecrease) > 0 { qty, err := s.DynamicQuantityDecrease.GetQuantity() if err == nil { sellQuantity = qty + } else { + log.WithError(err).Errorf("cannot get dynamic sell qty of %s, use default qty instead", s.Symbol) + bbgo.Notify("cannot get dynamic sell qty of %s, use default qty instead", s.Symbol) } } case s.mainTrendCurrent == types.DirectionDown: @@ -346,12 +350,18 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp qty, err := s.DynamicQuantityIncrease.GetQuantity() if err == nil { sellQuantity = qty + } else { + log.WithError(err).Errorf("cannot get dynamic sell qty of %s, use default qty instead", s.Symbol) + bbgo.Notify("cannot get dynamic sell qty of %s, use default qty instead", s.Symbol) } } if len(s.DynamicQuantityDecrease) > 0 { qty, err := s.DynamicQuantityDecrease.GetQuantity() if err == nil { buyQuantity = qty + } else { + log.WithError(err).Errorf("cannot get dynamic buy qty of %s, use default qty instead", s.Symbol) + bbgo.Notify("cannot get dynamic buy qty of %s, use default qty instead", s.Symbol) } } } @@ -658,6 +668,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Trend reversal if s.mainTrendCurrent != s.mainTrendPrevious { log.Infof("%s trend reverse to %v", s.Symbol, s.mainTrendCurrent) + bbgo.Notify("%s trend reverse to %v", s.Symbol, s.mainTrendCurrent) // Close on-hand position that is not in the same direction as the new trend if !s.Position.IsDust(closePrice) && ((s.Position.IsLong() && s.mainTrendCurrent == types.DirectionDown) || @@ -667,7 +678,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if err := s.ClosePosition(ctx, fixedpoint.One); err != nil { log.WithError(err).Errorf("cannot close on-hand position of %s", s.Symbol) bbgo.Notify("cannot close on-hand position of %s", s.Symbol) - // TODO: close position failed. retry? } } } From ff334ca13df510cc1622d0a6d6494e393eaf9aaf Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 17:16:30 +0800 Subject: [PATCH 24/37] strategy/linregmaker: calculated allowed margin when leveraged --- config/linregmaker.yaml | 6 +++-- pkg/strategy/linregmaker/strategy.go | 39 ++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index a88da8b50..80c2ddf55 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -18,8 +18,8 @@ backtest: # 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-03-23" + startTime: "2022-05-01" + endTime: "2022-10-31" symbols: - BTCUSDT accounts: @@ -38,6 +38,8 @@ exchangeStrategies: # interval is how long do you want to update your order price and quantity interval: 1m + leverage: 1 + # reverseEMA reverseEMA: interval: 1d diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index afd58cec2..89f300f9b 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -32,10 +32,14 @@ type Strategy struct { Environment *bbgo.Environment StandardIndicatorSet *bbgo.StandardIndicatorSet Market types.Market + ctx context.Context // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` + // Leverage uses the account net value to calculate the allowed margin + Leverage fixedpoint.Value `json:"leverage"` + types.IntervalWindow // ReverseEMA is used to determine the long-term trend. @@ -408,20 +412,23 @@ func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { balances := s.session.GetAccount().Balances() baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] + lastPrice, _ := s.session.LastPrice(s.Symbol) if bbgo.IsBackTesting { if !hasQuoteBalance { baseQty = fixedpoint.Zero quoteQty = fixedpoint.Zero } else { - lastPrice, _ := s.session.LastPrice(s.Symbol) baseQty = quoteBalance.Available.Div(lastPrice) quoteQty = quoteBalance.Available } - } else if s.session.Margin || s.session.IsolatedMargin { - - } else if s.session.Futures || s.session.IsolatedFutures { - + } else if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { + quoteQ, err := bbgo.CalculateQuoteQuantity(s.ctx, s.session, s.Market.QuoteCurrency, s.Leverage) + if err != nil { + quoteQ = fixedpoint.Zero + } + quoteQty = quoteQ + baseQty = quoteQ.Div(lastPrice) } else { if !hasBaseBalance { baseQty = fixedpoint.Zero @@ -479,11 +486,21 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f // Check against account balance baseQty, quoteQty := s.getAllowedBalance() - if buyQuantity.Compare(quoteQty.Div(bidPrice)) > 0 { - canBuy = false - } - if sellQuantity.Compare(baseQty) > 0 { - canSell = false + if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { // Leveraged + if quoteQty.Compare(fixedpoint.Zero) <= 0 { + if s.Position.IsLong() { + canBuy = false + } else if s.Position.IsShort() { + canSell = false + } + } + } else { + if buyQuantity.Compare(quoteQty.Div(bidPrice)) > 0 { // Spot + canBuy = false + } + if sellQuantity.Compare(baseQty) > 0 { + canSell = false + } } log.Infof("canBuy %t, canSell %t", canBuy, canSell) @@ -491,7 +508,6 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f } // getOrderForms returns buy and sell order form for submission -// TODO: Simplify func (s *Strategy) getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice fixedpoint.Value) (buyOrder types.SubmitOrder, sellOrder types.SubmitOrder) { sellOrder = types.SubmitOrder{ Symbol: s.Symbol, @@ -563,6 +579,7 @@ func (s *Strategy) getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice f func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { // initial required information s.session = session + s.ctx = ctx // Calculate group id for orders instanceID := s.InstanceID() From c6f9b0feed8792cbc5a8e662f5b6494fa49564dd Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 17:37:47 +0800 Subject: [PATCH 25/37] strategy/linregmaker: update config --- config/linregmaker.yaml | 62 +++++++++++++++++----------- pkg/strategy/linregmaker/strategy.go | 3 +- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 80c2ddf55..1f320f181 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -38,33 +38,45 @@ exchangeStrategies: # interval is how long do you want to update your order price and quantity interval: 1m + # leverage uses the account net value to calculate the allowed margin leverage: 1 - # reverseEMA + # reverseEMA is used to determine the long-term trend. + # Above the ReverseEMA is the long trend and vise versa. + # All the opposite trend position will be closed upon the trend change reverseEMA: interval: 1d window: 60 - # reverseInterval + # reverseInterval is the interval to check trend reverse against ReverseEMA. Close price of this interval crossing + # the ReverseEMA triggers main trend change. reverseInterval: 4h - # fastLinReg + # fastLinReg is to determine the short-term trend. + # Buy/sell orders are placed if the FastLinReg and the ReverseEMA trend are in the same direction, and only orders + # that reduce position are placed if the FastLinReg and the ReverseEMA trend are in different directions. fastLinReg: interval: 1m window: 30 - # slowLinReg + # slowLinReg is to determine the midterm trend. + # When the SlowLinReg and the ReverseEMA trend are in different directions, creation of opposite position is + # allowed. slowLinReg: interval: 1m window: 120 - # allowOppositePosition + # allowOppositePosition if true, the creation of opposite position is allowed when both fast and slow LinReg are in + # the opposite direction to main trend allowOppositePosition: true - # fasterDecreaseRatio + # fasterDecreaseRatio the quantity of decreasing position orders are multiplied by this ratio when both fast and + # slow LinReg are in the opposite direction to main trend fasterDecreaseRatio: 2 - # neutralBollinger + # neutralBollinger is the smaller range of the bollinger band + # If price is in this band, it usually means the price is oscillating. + # If price goes out of this band, we tend to not place sell orders or buy orders neutralBollinger: interval: "15m" window: 21 @@ -73,9 +85,13 @@ exchangeStrategies: # tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band. tradeInBand: true - # spread + # spread is the price spread from the middle price. + # For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) + # For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) + # Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 spread: 0.1% - # dynamicSpread + # dynamicSpread enables the automatic adjustment to bid and ask spread. + # Overrides Spread, BidSpread, and AskSpread dynamicSpread: amplitude: # delete other scaling strategy if this is defined # window is the window of the SMAs of spreads @@ -87,8 +103,7 @@ exchangeStrategies: exp: # from down to up domain: [ 0.0001, 0.005 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum + # the spread range range: [ 0.001, 0.002 ] bidSpreadScale: byPercentage: @@ -96,12 +111,15 @@ exchangeStrategies: exp: # from down to up domain: [ 0.0001, 0.005 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum + # the spread range range: [ 0.001, 0.002 ] + # maxExposurePosition is the maximum position you can hold + # 10 means you can hold 10 ETH long/short position by maximum #maxExposurePosition: 10 - DynamicExposure: + # dynamicExposure is used to define the exposure position range with the given percentage. + # When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically + dynamicExposure: bollBandExposure: interval: "1h" window: 21 @@ -112,12 +130,13 @@ exchangeStrategies: exp: # from lower band -100% (-1) to upper band 100% (+1) domain: [ -1, 1 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum - range: [ 0.1, 10 ] + # when in down band, holds 0.1 by maximum + # when in up band, holds 1 by maximum + range: [ 0.1, 1 ] # quantity is the base order quantity for your buy/sell order. quantity: 0.1 + # dynamicQuantityIncrease calculates the increase position order quantity dynamically dynamicQuantityIncrease: - linRegDynamicQuantity: quantityLinReg: @@ -125,13 +144,10 @@ exchangeStrategies: window: 20 dynamicQuantityLinRegScale: byPercentage: - # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: - # from lower band -100% (-1) to upper band 100% (+1) domain: [ -0.000002, 0.000002 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum range: [ 0, 0.001 ] + # dynamicQuantityDecrease calculates the decrease position order quantity dynamically dynamicQuantityDecrease: - linRegDynamicQuantity: quantityLinReg: @@ -139,12 +155,8 @@ exchangeStrategies: window: 20 dynamicQuantityLinRegScale: byPercentage: - # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: - # from lower band -100% (-1) to upper band 100% (+1) domain: [0.000002, -0.000002 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum range: [ 0, 0.001 ] exits: diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 89f300f9b..57e28dbbb 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -17,6 +17,8 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +// TODO: Docs + const ID = "linregmaker" var notionModifier = fixedpoint.NewFromFloat(1.1) @@ -403,7 +405,6 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } // getAllowedBalance returns the allowed qty of orders -// TODO LATER: Check max qty of margin and futures func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { // Default baseQty = fixedpoint.PosInf From 2ecdae65306cdc59a8571127bf6a58d001a1eea2 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 17:49:31 +0800 Subject: [PATCH 26/37] strategy/linregmaker: remove wrong test file --- pkg/strategy/linregmaker/strategy_test.go | 69 ----------------------- 1 file changed, 69 deletions(-) delete mode 100644 pkg/strategy/linregmaker/strategy_test.go diff --git a/pkg/strategy/linregmaker/strategy_test.go b/pkg/strategy/linregmaker/strategy_test.go deleted file mode 100644 index 317464ad0..000000000 --- a/pkg/strategy/linregmaker/strategy_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package linregmaker - -import ( - "testing" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -func Test_calculateBandPercentage(t *testing.T) { - type args struct { - up float64 - down float64 - sma float64 - midPrice float64 - } - tests := []struct { - name string - args args - want fixedpoint.Value - }{ - { - name: "positive boundary", - args: args{ - up: 2000.0, - sma: 1500.0, - down: 1000.0, - midPrice: 2000.0, - }, - want: fixedpoint.NewFromFloat(1.0), - }, - { - name: "inside positive boundary", - args: args{ - up: 2000.0, - sma: 1500.0, - down: 1000.0, - midPrice: 1600.0, - }, - want: fixedpoint.NewFromFloat(0.2), // 20% - }, - { - name: "negative boundary", - args: args{ - up: 2000.0, - sma: 1500.0, - down: 1000.0, - midPrice: 1000.0, - }, - want: fixedpoint.NewFromFloat(-1.0), - }, - { - name: "out of negative boundary", - args: args{ - up: 2000.0, - sma: 1500.0, - down: 1000.0, - midPrice: 800.0, - }, - want: fixedpoint.NewFromFloat(-1.4), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := calculateBandPercentage(tt.args.up, tt.args.down, tt.args.sma, tt.args.midPrice); fixedpoint.NewFromFloat(got) != tt.want { - t.Errorf("calculateBandPercentage() = %v, want %v", got, tt.want) - } - }) - } -} From 2b8a5fe7552b358ef5c20069f2e0484d11235b6c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 14 Dec 2022 11:52:15 +0800 Subject: [PATCH 27/37] strategy/linregmaker: fix faster decrease logic --- pkg/strategy/linregmaker/strategy.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 57e28dbbb..d7342050e 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -374,14 +374,12 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp log.Infof("%s caculated buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) // Faster position decrease - if s.isAllowOppositePosition() { - if s.mainTrendCurrent == types.DirectionUp { - sellQuantity = sellQuantity.Mul(s.FasterDecreaseRatio) - } else if s.mainTrendCurrent == types.DirectionDown { - buyQuantity = buyQuantity.Mul(s.FasterDecreaseRatio) - } - log.Infof("%s faster position decrease: buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) + if s.mainTrendCurrent == types.DirectionUp && s.SlowLinReg.Last() < 0 { + sellQuantity = sellQuantity.Mul(s.FasterDecreaseRatio) + } else if s.mainTrendCurrent == types.DirectionDown && s.SlowLinReg.Last() > 0 { + buyQuantity = buyQuantity.Mul(s.FasterDecreaseRatio) } + log.Infof("%s faster position decrease: buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) // Reduce order qty to fit current position if !s.isAllowOppositePosition() { From d510c37e911fe7fb50a5b8372ecb24179dac90d3 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 14 Dec 2022 12:28:39 +0800 Subject: [PATCH 28/37] improve/dynamic_quantity: fix dynamic qty logic --- pkg/dynamicmetric/dynamic_quantity.go | 18 ++++++++++++------ pkg/strategy/linregmaker/strategy.go | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/dynamicmetric/dynamic_quantity.go index 70e44797d..fa3182ada 100644 --- a/pkg/dynamicmetric/dynamic_quantity.go +++ b/pkg/dynamicmetric/dynamic_quantity.go @@ -19,10 +19,10 @@ func (d *DynamicQuantitySet) Initialize(symbol string, session *bbgo.ExchangeSes } // GetQuantity returns the quantity -func (d *DynamicQuantitySet) GetQuantity() (fixedpoint.Value, error) { +func (d *DynamicQuantitySet) GetQuantity(reverse bool) (fixedpoint.Value, error) { quantity := fixedpoint.Zero for i := range *d { - v, err := (*d)[i].getQuantity() + v, err := (*d)[i].getQuantity(reverse) if err != nil { return fixedpoint.Zero, err } @@ -50,10 +50,10 @@ func (d *DynamicQuantity) IsEnabled() bool { } // getQuantity returns quantity -func (d *DynamicQuantity) getQuantity() (fixedpoint.Value, error) { +func (d *DynamicQuantity) getQuantity(reverse bool) (fixedpoint.Value, error) { switch { case d.LinRegDynamicQuantity != nil: - return d.LinRegDynamicQuantity.getQuantity() + return d.LinRegDynamicQuantity.getQuantity(reverse) default: return fixedpoint.Zero, errors.New("dynamic quantity is not enabled") } @@ -84,8 +84,14 @@ func (d *DynamicQuantityLinReg) initialize(symbol string, session *bbgo.Exchange } // getQuantity returns quantity -func (d *DynamicQuantityLinReg) getQuantity() (fixedpoint.Value, error) { - v, err := d.DynamicQuantityLinRegScale.Scale(d.QuantityLinReg.LastRatio()) +func (d *DynamicQuantityLinReg) getQuantity(reverse bool) (fixedpoint.Value, error) { + var linregRatio float64 + if reverse { + linregRatio = -d.QuantityLinReg.LastRatio() + } else { + linregRatio = d.QuantityLinReg.LastRatio() + } + v, err := d.DynamicQuantityLinRegScale.Scale(linregRatio) if err != nil { return fixedpoint.Zero, err } diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index d7342050e..478f6e168 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -334,7 +334,7 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp switch { case s.mainTrendCurrent == types.DirectionUp: if len(s.DynamicQuantityIncrease) > 0 { - qty, err := s.DynamicQuantityIncrease.GetQuantity() + qty, err := s.DynamicQuantityIncrease.GetQuantity(false) if err == nil { buyQuantity = qty } else { @@ -343,7 +343,7 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } if len(s.DynamicQuantityDecrease) > 0 { - qty, err := s.DynamicQuantityDecrease.GetQuantity() + qty, err := s.DynamicQuantityDecrease.GetQuantity(false) if err == nil { sellQuantity = qty } else { @@ -353,7 +353,7 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } case s.mainTrendCurrent == types.DirectionDown: if len(s.DynamicQuantityIncrease) > 0 { - qty, err := s.DynamicQuantityIncrease.GetQuantity() + qty, err := s.DynamicQuantityIncrease.GetQuantity(true) if err == nil { sellQuantity = qty } else { @@ -362,7 +362,7 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } if len(s.DynamicQuantityDecrease) > 0 { - qty, err := s.DynamicQuantityDecrease.GetQuantity() + qty, err := s.DynamicQuantityDecrease.GetQuantity(true) if err == nil { buyQuantity = qty } else { From 8b1d19124f72cde0ca39e6707c4f11e7a0e875ce Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 14 Dec 2022 14:42:56 +0800 Subject: [PATCH 29/37] strategy/linregmaker: allow using amount for order qty calculation --- config/linregmaker.yaml | 14 +++++++++----- pkg/strategy/linregmaker/strategy.go | 21 +++++++++++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 1f320f181..57b89ec8a 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -135,7 +135,11 @@ exchangeStrategies: range: [ 0.1, 1 ] # quantity is the base order quantity for your buy/sell order. - quantity: 0.1 + quantity: 0.001 + # amount: fixed amount instead of qty + #amount: 10 + # useDynamicQuantityAsAmount calculates amount instead of quantity + useDynamicQuantityAsAmount: false # dynamicQuantityIncrease calculates the increase position order quantity dynamically dynamicQuantityIncrease: - linRegDynamicQuantity: @@ -145,8 +149,8 @@ exchangeStrategies: dynamicQuantityLinRegScale: byPercentage: linear: - domain: [ -0.000002, 0.000002 ] - range: [ 0, 0.001 ] + domain: [ -0.0001, 0.00005 ] + range: [ 0, 0.02 ] # dynamicQuantityDecrease calculates the decrease position order quantity dynamically dynamicQuantityDecrease: - linRegDynamicQuantity: @@ -156,8 +160,8 @@ exchangeStrategies: dynamicQuantityLinRegScale: byPercentage: linear: - domain: [0.000002, -0.000002 ] - range: [ 0, 0.001 ] + domain: [ -0.00005, 0.0001 ] + range: [ 0.02, 0 ] exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 478f6e168..ea6d7e9a3 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -120,6 +120,9 @@ type Strategy struct { // DynamicQuantityDecrease calculates the decrease position order quantity dynamically DynamicQuantityDecrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityDecrease"` + // UseDynamicQuantityAsAmount calculates amount instead of quantity + UseDynamicQuantityAsAmount bool `json:"useDynamicQuantityAsAmount"` + session *bbgo.ExchangeSession // ExitMethods are various TP/SL methods @@ -371,15 +374,25 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } } - log.Infof("%s caculated buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) + if s.UseDynamicQuantityAsAmount { + log.Infof("caculated %s buy amount %v, sell amount %v", s.Symbol, buyQuantity, sellQuantity) + qtyAmount := bbgo.QuantityOrAmount{Amount: buyQuantity} + buyQuantity = qtyAmount.CalculateQuantity(bidPrice) + qtyAmount.Amount = sellQuantity + sellQuantity = qtyAmount.CalculateQuantity(askPrice) + log.Infof("convert %s amount to buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) + } else { + log.Infof("caculated %s buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) + } // Faster position decrease if s.mainTrendCurrent == types.DirectionUp && s.SlowLinReg.Last() < 0 { sellQuantity = sellQuantity.Mul(s.FasterDecreaseRatio) + log.Infof("faster %s position decrease: sell qty %v", s.Symbol, sellQuantity) } else if s.mainTrendCurrent == types.DirectionDown && s.SlowLinReg.Last() > 0 { buyQuantity = buyQuantity.Mul(s.FasterDecreaseRatio) + log.Infof("faster %s position decrease: buy qty %v", s.Symbol, buyQuantity) } - log.Infof("%s faster position decrease: buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) // Reduce order qty to fit current position if !s.isAllowOppositePosition() { @@ -749,10 +762,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Submit orders var submitOrders []types.SubmitOrder - if canSell { + if canSell && sellOrder.Quantity.Compare(fixedpoint.Zero) > 0 { submitOrders = append(submitOrders, sellOrder) } - if canBuy { + if canBuy && buyOrder.Quantity.Compare(fixedpoint.Zero) > 0 { submitOrders = append(submitOrders, buyOrder) } From 1002c13062251e829cf18f6a823801de7a6577a9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:04:43 +0800 Subject: [PATCH 30/37] feature/dynamic_exposure: move to risk package --- pkg/{dynamicmetric => risk}/dynamic_exposure.go | 2 +- pkg/strategy/linregmaker/strategy.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename pkg/{dynamicmetric => risk}/dynamic_exposure.go (99%) diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/risk/dynamic_exposure.go similarity index 99% rename from pkg/dynamicmetric/dynamic_exposure.go rename to pkg/risk/dynamic_exposure.go index d150e11d9..d3eb899e2 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/risk/dynamic_exposure.go @@ -1,4 +1,4 @@ -package dynamicmetric +package risk import ( "github.com/c9s/bbgo/pkg/bbgo" diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index ea6d7e9a3..669133a3d 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/c9s/bbgo/pkg/dynamicmetric" + "github.com/c9s/bbgo/pkg/risk" "sync" "github.com/c9s/bbgo/pkg/indicator" @@ -110,7 +111,7 @@ type Strategy struct { // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically - DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` + DynamicExposure risk.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount From 4c98bed76fe726fa75cd5f99caf25140821a35a7 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:08:14 +0800 Subject: [PATCH 31/37] feature/dynamic_quantity: add comment for getQuantity() --- pkg/dynamicmetric/dynamic_quantity.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/dynamicmetric/dynamic_quantity.go index fa3182ada..4fc222494 100644 --- a/pkg/dynamicmetric/dynamic_quantity.go +++ b/pkg/dynamicmetric/dynamic_quantity.go @@ -84,6 +84,7 @@ func (d *DynamicQuantityLinReg) initialize(symbol string, session *bbgo.Exchange } // getQuantity returns quantity +// If reverse is true, the LinReg slope ratio is reversed, ie -0.01 becomes 0.01. This is for short orders. func (d *DynamicQuantityLinReg) getQuantity(reverse bool) (fixedpoint.Value, error) { var linregRatio float64 if reverse { From 5c7c125c99524978e83704def3849ac031b3bd12 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:09:47 +0800 Subject: [PATCH 32/37] feature/dynamic_spread: move to risk package --- pkg/{dynamicmetric => risk}/dynamic_spread.go | 2 +- pkg/strategy/linregmaker/strategy.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename pkg/{dynamicmetric => risk}/dynamic_spread.go (99%) diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/risk/dynamic_spread.go similarity index 99% rename from pkg/dynamicmetric/dynamic_spread.go rename to pkg/risk/dynamic_spread.go index 905f49bc3..9714d1baf 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/risk/dynamic_spread.go @@ -1,4 +1,4 @@ -package dynamicmetric +package risk import ( "github.com/pkg/errors" diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 669133a3d..bcc537ca6 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -103,7 +103,7 @@ type Strategy struct { // DynamicSpread enables the automatic adjustment to bid and ask spread. // Overrides Spread, BidSpread, and AskSpread - DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` + DynamicSpread risk.DynamicSpread `json:"dynamicSpread,omitempty"` // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum From 5b017cd361d88ae4c0844cea6cc3b9c080cba43c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:12:04 +0800 Subject: [PATCH 33/37] feature/dynamic_spread: rename DynamicSpreadAmp to DynamicAmpSpread --- pkg/risk/dynamic_spread.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/risk/dynamic_spread.go b/pkg/risk/dynamic_spread.go index 9714d1baf..3776d8351 100644 --- a/pkg/risk/dynamic_spread.go +++ b/pkg/risk/dynamic_spread.go @@ -12,7 +12,7 @@ import ( type DynamicSpread struct { // AmpSpread calculates spreads based on kline amplitude - AmpSpread *DynamicSpreadAmp `json:"amplitude"` + AmpSpread *DynamicAmpSpread `json:"amplitude"` // WeightedBollWidthRatioSpread calculates spreads based on two Bollinger Bands WeightedBollWidthRatioSpread *DynamicSpreadBollWidthRatio `json:"weightedBollWidth"` @@ -57,7 +57,7 @@ func (ds *DynamicSpread) GetBidSpread() (bidSpread float64, err error) { } // DynamicSpreadAmp uses kline amplitude to calculate spreads -type DynamicSpreadAmp struct { +type DynamicAmpSpread struct { types.IntervalWindow // AskSpreadScale is used to define the ask spread range with the given percentage. @@ -71,7 +71,7 @@ type DynamicSpreadAmp struct { } // initialize amplitude dynamic spread and preload SMAs -func (ds *DynamicSpreadAmp) initialize(symbol string, session *bbgo.ExchangeSession) { +func (ds *DynamicAmpSpread) initialize(symbol string, session *bbgo.ExchangeSession) { ds.dynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} ds.dynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} @@ -95,7 +95,7 @@ func (ds *DynamicSpreadAmp) initialize(symbol string, session *bbgo.ExchangeSess } // update amplitude dynamic spread with kline -func (ds *DynamicSpreadAmp) update(kline types.KLine) { +func (ds *DynamicAmpSpread) update(kline types.KLine) { // ampl is the amplitude of kline ampl := (kline.GetHigh().Float64() - kline.GetLow().Float64()) / kline.GetOpen().Float64() @@ -112,7 +112,7 @@ func (ds *DynamicSpreadAmp) update(kline types.KLine) { } } -func (ds *DynamicSpreadAmp) getAskSpread() (askSpread float64, err error) { +func (ds *DynamicAmpSpread) getAskSpread() (askSpread float64, err error) { if ds.AskSpreadScale != nil && ds.dynamicAskSpread.Length() >= ds.Window { askSpread, err = ds.AskSpreadScale.Scale(ds.dynamicAskSpread.Last()) if err != nil { @@ -126,7 +126,7 @@ func (ds *DynamicSpreadAmp) getAskSpread() (askSpread float64, err error) { return 0, errors.New("incomplete dynamic spread settings or not enough data yet") } -func (ds *DynamicSpreadAmp) getBidSpread() (bidSpread float64, err error) { +func (ds *DynamicAmpSpread) getBidSpread() (bidSpread float64, err error) { if ds.BidSpreadScale != nil && ds.dynamicBidSpread.Length() >= ds.Window { bidSpread, err = ds.BidSpreadScale.Scale(ds.dynamicBidSpread.Last()) if err != nil { From 754f8da5d43900f3e1624293bf0c93f323714e71 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:23:43 +0800 Subject: [PATCH 34/37] strategy/linregmaker: move private fields to the end of the struct --- pkg/strategy/linregmaker/strategy.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index bcc537ca6..54f8c3a95 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -32,11 +32,6 @@ func init() { } type Strategy struct { - Environment *bbgo.Environment - StandardIndicatorSet *bbgo.StandardIndicatorSet - Market types.Market - ctx context.Context - // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -124,8 +119,6 @@ type Strategy struct { // UseDynamicQuantityAsAmount calculates amount instead of quantity UseDynamicQuantityAsAmount bool `json:"useDynamicQuantityAsAmount"` - session *bbgo.ExchangeSession - // ExitMethods are various TP/SL methods ExitMethods bbgo.ExitMethodSet `json:"exits"` @@ -134,6 +127,13 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + Environment *bbgo.Environment + StandardIndicatorSet *bbgo.StandardIndicatorSet + Market types.Market + ctx context.Context + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor groupID uint32 From 095eb9c134fbf54acb846eaf326bac5fe90026f9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:25:56 +0800 Subject: [PATCH 35/37] feature/dynamic_exposure: undo move dynamic_exposure and dynamic_spread --- pkg/{risk => dynamicmetric}/dynamic_exposure.go | 2 +- pkg/{risk => dynamicmetric}/dynamic_spread.go | 2 +- pkg/strategy/linregmaker/strategy.go | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) rename pkg/{risk => dynamicmetric}/dynamic_exposure.go (99%) rename pkg/{risk => dynamicmetric}/dynamic_spread.go (99%) diff --git a/pkg/risk/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go similarity index 99% rename from pkg/risk/dynamic_exposure.go rename to pkg/dynamicmetric/dynamic_exposure.go index d3eb899e2..d150e11d9 100644 --- a/pkg/risk/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -1,4 +1,4 @@ -package risk +package dynamicmetric import ( "github.com/c9s/bbgo/pkg/bbgo" diff --git a/pkg/risk/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go similarity index 99% rename from pkg/risk/dynamic_spread.go rename to pkg/dynamicmetric/dynamic_spread.go index 3776d8351..2b2e3d149 100644 --- a/pkg/risk/dynamic_spread.go +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -1,4 +1,4 @@ -package risk +package dynamicmetric import ( "github.com/pkg/errors" diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 54f8c3a95..8c6f031e0 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "github.com/c9s/bbgo/pkg/dynamicmetric" - "github.com/c9s/bbgo/pkg/risk" "sync" "github.com/c9s/bbgo/pkg/indicator" @@ -98,7 +97,7 @@ type Strategy struct { // DynamicSpread enables the automatic adjustment to bid and ask spread. // Overrides Spread, BidSpread, and AskSpread - DynamicSpread risk.DynamicSpread `json:"dynamicSpread,omitempty"` + DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum @@ -106,7 +105,7 @@ type Strategy struct { // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically - DynamicExposure risk.DynamicExposure `json:"dynamicExposure"` + DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount From e39b94cf51868830a849384accfd590bddb60297 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:50:05 +0800 Subject: [PATCH 36/37] bbgo/standard_indicator_set: embed BOLL's SMA initialization into the constructor literal --- pkg/bbgo/standard_indicator_set.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go index 922856726..e291cccc6 100644 --- a/pkg/bbgo/standard_indicator_set.go +++ b/pkg/bbgo/standard_indicator_set.go @@ -140,9 +140,8 @@ func (s *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth} inc, ok := s.iwbIndicators[iwb] if !ok { - inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth} + inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth, SMA: &indicator.SMA{IntervalWindow: iw}} s.initAndBind(inc, iw.Interval) - inc.SMA = &indicator.SMA{IntervalWindow: iw} if debugBOLL { inc.OnUpdate(func(sma float64, upBand float64, downBand float64) { From d5e37f03e29a915a20e4d5f610ca91abeff640eb Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Dec 2022 11:51:52 +0800 Subject: [PATCH 37/37] feature/dynamic_*: move dynamic_* to risk/dynamicrisk package --- .../dynamicrisk}/dynamic_exposure.go | 2 +- .../dynamicrisk}/dynamic_quantity.go | 2 +- .../dynamicrisk}/dynamic_spread.go | 2 +- pkg/strategy/linregmaker/strategy.go | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) rename pkg/{dynamicmetric => risk/dynamicrisk}/dynamic_exposure.go (99%) rename pkg/{dynamicmetric => risk/dynamicrisk}/dynamic_quantity.go (99%) rename pkg/{dynamicmetric => risk/dynamicrisk}/dynamic_spread.go (99%) diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/risk/dynamicrisk/dynamic_exposure.go similarity index 99% rename from pkg/dynamicmetric/dynamic_exposure.go rename to pkg/risk/dynamicrisk/dynamic_exposure.go index d150e11d9..71eb12155 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/risk/dynamicrisk/dynamic_exposure.go @@ -1,4 +1,4 @@ -package dynamicmetric +package dynamicrisk import ( "github.com/c9s/bbgo/pkg/bbgo" diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/risk/dynamicrisk/dynamic_quantity.go similarity index 99% rename from pkg/dynamicmetric/dynamic_quantity.go rename to pkg/risk/dynamicrisk/dynamic_quantity.go index 4fc222494..d2d1e8c0e 100644 --- a/pkg/dynamicmetric/dynamic_quantity.go +++ b/pkg/risk/dynamicrisk/dynamic_quantity.go @@ -1,4 +1,4 @@ -package dynamicmetric +package dynamicrisk import ( "github.com/c9s/bbgo/pkg/bbgo" diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/risk/dynamicrisk/dynamic_spread.go similarity index 99% rename from pkg/dynamicmetric/dynamic_spread.go rename to pkg/risk/dynamicrisk/dynamic_spread.go index 2b2e3d149..2b5cbbeb8 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/risk/dynamicrisk/dynamic_spread.go @@ -1,4 +1,4 @@ -package dynamicmetric +package dynamicrisk import ( "github.com/pkg/errors" diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 8c6f031e0..a0d2f2730 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -3,7 +3,7 @@ package linregmaker import ( "context" "fmt" - "github.com/c9s/bbgo/pkg/dynamicmetric" + "github.com/c9s/bbgo/pkg/risk/dynamicrisk" "sync" "github.com/c9s/bbgo/pkg/indicator" @@ -97,7 +97,7 @@ type Strategy struct { // DynamicSpread enables the automatic adjustment to bid and ask spread. // Overrides Spread, BidSpread, and AskSpread - DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` + DynamicSpread dynamicrisk.DynamicSpread `json:"dynamicSpread,omitempty"` // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum @@ -105,15 +105,15 @@ type Strategy struct { // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically - DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` + DynamicExposure dynamicrisk.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount // DynamicQuantityIncrease calculates the increase position order quantity dynamically - DynamicQuantityIncrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityIncrease"` + DynamicQuantityIncrease dynamicrisk.DynamicQuantitySet `json:"dynamicQuantityIncrease"` // DynamicQuantityDecrease calculates the decrease position order quantity dynamically - DynamicQuantityDecrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityDecrease"` + DynamicQuantityDecrease dynamicrisk.DynamicQuantitySet `json:"dynamicQuantityDecrease"` // UseDynamicQuantityAsAmount calculates amount instead of quantity UseDynamicQuantityAsAmount bool `json:"useDynamicQuantityAsAmount"`