mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #1027 from andycheng123/strategy/linregmaker
Strategy: LinReg Maker
This commit is contained in:
commit
2b20ff4da9
169
config/linregmaker.yaml
Normal file
169
config/linregmaker.yaml
Normal file
|
@ -0,0 +1,169 @@
|
|||
---
|
||||
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-05-01"
|
||||
endTime: "2022-10-31"
|
||||
symbols:
|
||||
- BTCUSDT
|
||||
accounts:
|
||||
binance:
|
||||
makerCommission: 10 # 0.15%
|
||||
takerCommission: 15 # 0.15%
|
||||
balances:
|
||||
BTC: 2.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
|
||||
|
||||
# leverage uses the account net value to calculate the allowed margin
|
||||
leverage: 1
|
||||
|
||||
# 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 is the interval to check trend reverse against ReverseEMA. Close price of this interval crossing
|
||||
# the ReverseEMA triggers main trend change.
|
||||
reverseInterval: 4h
|
||||
|
||||
# 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 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 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 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 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
|
||||
bandWidth: 2.0
|
||||
|
||||
# tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band.
|
||||
tradeInBand: true
|
||||
|
||||
# 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 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
|
||||
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 ]
|
||||
# the spread range
|
||||
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 ]
|
||||
# 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 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
|
||||
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 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.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:
|
||||
quantityLinReg:
|
||||
interval: 1m
|
||||
window: 20
|
||||
dynamicQuantityLinRegScale:
|
||||
byPercentage:
|
||||
linear:
|
||||
domain: [ -0.0001, 0.00005 ]
|
||||
range: [ 0, 0.02 ]
|
||||
# dynamicQuantityDecrease calculates the decrease position order quantity dynamically
|
||||
dynamicQuantityDecrease:
|
||||
- linRegDynamicQuantity:
|
||||
quantityLinReg:
|
||||
interval: 1m
|
||||
window: 20
|
||||
dynamicQuantityLinRegScale:
|
||||
byPercentage:
|
||||
linear:
|
||||
domain: [ -0.00005, 0.0001 ]
|
||||
range: [ 0.02, 0 ]
|
||||
|
||||
exits:
|
||||
# roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
|
||||
- roiStopLoss:
|
||||
percentage: 30%
|
|
@ -140,7 +140,7 @@ 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)
|
||||
|
||||
if debugBOLL {
|
||||
|
|
|
@ -21,6 +21,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"
|
||||
|
|
125
pkg/indicator/linreg.go
Normal file
125
pkg/indicator/linreg.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package indicator
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/datatype/floats"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
var logLinReg = logrus.WithField("indicator", "LinReg")
|
||||
|
||||
// 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
|
||||
// ValueRatios are the ratio of slope to the price
|
||||
ValueRatios 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()
|
||||
}
|
||||
|
||||
// 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() {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
lr.ValueRatios.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))
|
||||
lr.ValueRatios.Push(lr.Values.Last() / kline.GetClose().Float64())
|
||||
|
||||
logLinReg.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)
|
||||
}
|
||||
}
|
15
pkg/indicator/linreg_callbacks.go
Normal file
15
pkg/indicator/linreg_callbacks.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
86
pkg/risk/dynamicrisk/dynamic_exposure.go
Normal file
86
pkg/risk/dynamicrisk/dynamic_exposure.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package dynamicrisk
|
||||
|
||||
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, trend types.Direction) (maxExposure fixedpoint.Value, err error) {
|
||||
switch {
|
||||
case d.BollBandExposure != nil:
|
||||
return d.BollBandExposure.getMaxExposure(price, trend)
|
||||
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"`
|
||||
|
||||
types.IntervalWindowBandWidth
|
||||
|
||||
dynamicExposureBollBand *indicator.BOLL
|
||||
}
|
||||
|
||||
// initialize dynamic exposure with Bollinger Band
|
||||
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{
|
||||
Interval: d.dynamicExposureBollBand.Interval,
|
||||
})
|
||||
}
|
||||
|
||||
// getMaxExposure returns the max exposure
|
||||
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()
|
||||
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)
|
||||
}
|
||||
|
||||
// Reverse if downtrend
|
||||
if trend == types.DirectionDown {
|
||||
bandPercentage = 0 - bandPercentage
|
||||
}
|
||||
|
||||
v, err := d.DynamicExposureBollBandScale.Scale(bandPercentage)
|
||||
if err != nil {
|
||||
return fixedpoint.Zero, err
|
||||
}
|
||||
return fixedpoint.NewFromFloat(v), nil
|
||||
}
|
100
pkg/risk/dynamicrisk/dynamic_quantity.go
Normal file
100
pkg/risk/dynamicrisk/dynamic_quantity.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package dynamicrisk
|
||||
|
||||
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(reverse bool) (fixedpoint.Value, error) {
|
||||
quantity := fixedpoint.Zero
|
||||
for i := range *d {
|
||||
v, err := (*d)[i].getQuantity(reverse)
|
||||
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(reverse bool) (fixedpoint.Value, error) {
|
||||
switch {
|
||||
case d.LinRegDynamicQuantity != nil:
|
||||
return d.LinRegDynamicQuantity.getQuantity(reverse)
|
||||
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
|
||||
// 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 {
|
||||
linregRatio = -d.QuantityLinReg.LastRatio()
|
||||
} else {
|
||||
linregRatio = d.QuantityLinReg.LastRatio()
|
||||
}
|
||||
v, err := d.DynamicQuantityLinRegScale.Scale(linregRatio)
|
||||
if err != nil {
|
||||
return fixedpoint.Zero, err
|
||||
}
|
||||
return fixedpoint.NewFromFloat(v), nil
|
||||
}
|
247
pkg/risk/dynamicrisk/dynamic_spread.go
Normal file
247
pkg/risk/dynamicrisk/dynamic_spread.go
Normal file
|
@ -0,0 +1,247 @@
|
|||
package dynamicrisk
|
||||
|
||||
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 *DynamicAmpSpread `json:"amplitude"`
|
||||
|
||||
// WeightedBollWidthRatioSpread calculates spreads based on two Bollinger Bands
|
||||
WeightedBollWidthRatioSpread *DynamicSpreadBollWidthRatio `json:"weightedBollWidth"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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 DynamicAmpSpread 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 *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}}
|
||||
|
||||
// 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 *DynamicAmpSpread) 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 *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 {
|
||||
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 *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 {
|
||||
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")
|
||||
}
|
||||
|
||||
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 types.IntervalWindowBandWidth `json:"defaultBollinger"`
|
||||
NeutralBollinger types.IntervalWindowBandWidth `json:"neutralBollinger"`
|
||||
|
||||
neutralBoll *indicator.BOLL
|
||||
defaultBoll *indicator.BOLL
|
||||
}
|
||||
|
||||
func (ds *DynamicSpreadBollWidthRatio) initialize(symbol string, session *bbgo.ExchangeSession) {
|
||||
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{
|
||||
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 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,
|
||||
// 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)
|
||||
}
|
6
pkg/strategy/linregmaker/doc.go
Normal file
6
pkg/strategy/linregmaker/doc.go
Normal file
|
@ -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
|
786
pkg/strategy/linregmaker/strategy.go
Normal file
786
pkg/strategy/linregmaker/strategy.go
Normal file
|
@ -0,0 +1,786 @@
|
|||
package linregmaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/c9s/bbgo/pkg/risk/dynamicrisk"
|
||||
"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: Docs
|
||||
|
||||
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 Strategy struct {
|
||||
// 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.
|
||||
// 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"`
|
||||
|
||||
// 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
|
||||
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"`
|
||||
|
||||
// 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"`
|
||||
|
||||
// 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
|
||||
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.
|
||||
TradeInBand bool `json:"tradeInBand"`
|
||||
|
||||
// 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 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
|
||||
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
|
||||
DynamicExposure dynamicrisk.DynamicExposure `json:"dynamicExposure"`
|
||||
|
||||
bbgo.QuantityOrAmount
|
||||
|
||||
// DynamicQuantityIncrease calculates the increase position order quantity dynamically
|
||||
DynamicQuantityIncrease dynamicrisk.DynamicQuantitySet `json:"dynamicQuantityIncrease"`
|
||||
|
||||
// DynamicQuantityDecrease calculates the decrease position order quantity dynamically
|
||||
DynamicQuantityDecrease dynamicrisk.DynamicQuantitySet `json:"dynamicQuantityDecrease"`
|
||||
|
||||
// UseDynamicQuantityAsAmount calculates amount instead of quantity
|
||||
UseDynamicQuantityAsAmount bool `json:"useDynamicQuantityAsAmount"`
|
||||
|
||||
// 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"`
|
||||
|
||||
Environment *bbgo.Environment
|
||||
StandardIndicatorSet *bbgo.StandardIndicatorSet
|
||||
Market types.Market
|
||||
ctx context.Context
|
||||
|
||||
session *bbgo.ExchangeSession
|
||||
|
||||
orderExecutor *bbgo.GeneralOrderExecutor
|
||||
|
||||
groupID uint32
|
||||
|
||||
// StrategyController
|
||||
bbgo.StrategyController
|
||||
}
|
||||
|
||||
func (s *Strategy) ID() string {
|
||||
return ID
|
||||
}
|
||||
|
||||
func (s *Strategy) InstanceID() string {
|
||||
return fmt.Sprintf("%s:%s", ID, s.Symbol)
|
||||
}
|
||||
|
||||
// Validate basic config parameters
|
||||
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.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{
|
||||
Interval: s.ReverseEMA.Interval,
|
||||
})
|
||||
// Initialize ReverseEMA
|
||||
s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow)
|
||||
|
||||
// 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,
|
||||
})
|
||||
|
||||
// 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.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)
|
||||
}
|
||||
|
||||
// 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) CurrentPosition() *types.Position {
|
||||
return s.Position
|
||||
}
|
||||
|
||||
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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(), 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
|
||||
}
|
||||
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("%s mid price:%v ask:%v bid: %v", s.Symbol, midPrice, askPrice, bidPrice)
|
||||
|
||||
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
|
||||
sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice)
|
||||
buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice)
|
||||
|
||||
// Dynamic qty
|
||||
switch {
|
||||
case s.mainTrendCurrent == types.DirectionUp:
|
||||
if len(s.DynamicQuantityIncrease) > 0 {
|
||||
qty, err := s.DynamicQuantityIncrease.GetQuantity(false)
|
||||
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(false)
|
||||
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:
|
||||
if len(s.DynamicQuantityIncrease) > 0 {
|
||||
qty, err := s.DynamicQuantityIncrease.GetQuantity(true)
|
||||
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(true)
|
||||
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 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)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
if buyQuantity.Compare(fixedpoint.Zero) > 0 {
|
||||
buyQuantity = s.adjustQuantity(buyQuantity, bidPrice)
|
||||
}
|
||||
if sellQuantity.Compare(fixedpoint.Zero) > 0 {
|
||||
sellQuantity = s.adjustQuantity(sellQuantity, askPrice)
|
||||
}
|
||||
|
||||
log.Infof("adjusted sell qty:%v buy qty: %v", sellQuantity, buyQuantity)
|
||||
|
||||
return sellQuantity, buyQuantity
|
||||
}
|
||||
|
||||
// getAllowedBalance returns the allowed qty of orders
|
||||
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]
|
||||
lastPrice, _ := s.session.LastPrice(s.Symbol)
|
||||
|
||||
if bbgo.IsBackTesting {
|
||||
if !hasQuoteBalance {
|
||||
baseQty = fixedpoint.Zero
|
||||
quoteQty = fixedpoint.Zero
|
||||
} else {
|
||||
baseQty = quoteBalance.Available.Div(lastPrice)
|
||||
quoteQty = quoteBalance.Available
|
||||
}
|
||||
} 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
|
||||
} 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(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
|
||||
|
||||
// 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
|
||||
}
|
||||
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 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 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.isAllowOppositePosition() {
|
||||
if s.mainTrendCurrent == types.DirectionUp && (s.Position.IsClosed() || s.Position.IsDust(askPrice)) {
|
||||
canSell = false
|
||||
} else if s.mainTrendCurrent == types.DirectionDown && (s.Position.IsClosed() || s.Position.IsDust(bidPrice)) {
|
||||
canBuy = false
|
||||
}
|
||||
}
|
||||
|
||||
// Check against account balance
|
||||
baseQty, quoteQty := s.getAllowedBalance()
|
||||
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)
|
||||
return canBuy, canSell
|
||||
}
|
||||
|
||||
// getOrderForms returns buy and sell order form for submission
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buyOrder, sellOrder
|
||||
}
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
// Default spread
|
||||
if s.Spread == fixedpoint.Zero {
|
||||
s.Spread = fixedpoint.NewFromFloat(0.001)
|
||||
}
|
||||
|
||||
// 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))
|
||||
})
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
log.Infof("%s current trend is %v", s.Symbol, s.mainTrendCurrent)
|
||||
|
||||
// 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) ||
|
||||
(s.Position.IsShort() && s.mainTrendCurrent == types.DirectionUp)) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
// 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()
|
||||
|
||||
// midPrice for ask and bid prices
|
||||
var midPrice fixedpoint.Value
|
||||
if !bbgo.IsBackTesting {
|
||||
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)
|
||||
|
||||
buyOrder, sellOrder := s.getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice)
|
||||
|
||||
canBuy, canSell := s.getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice)
|
||||
|
||||
// Submit orders
|
||||
var submitOrders []types.SubmitOrder
|
||||
if canSell && sellOrder.Quantity.Compare(fixedpoint.Zero) > 0 {
|
||||
submitOrders = append(submitOrders, sellOrder)
|
||||
}
|
||||
if canBuy && buyOrder.Quantity.Compare(fixedpoint.Zero) > 0 {
|
||||
submitOrders = append(submitOrders, buyOrder)
|
||||
}
|
||||
|
||||
if len(submitOrders) == 0 {
|
||||
return
|
||||
}
|
||||
log.Infof("submitting order(s): %v", submitOrders)
|
||||
_, _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...)
|
||||
}))
|
||||
|
||||
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user