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}
|
iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth}
|
||||||
inc, ok := s.iwbIndicators[iwb]
|
inc, ok := s.iwbIndicators[iwb]
|
||||||
if !ok {
|
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)
|
s.initAndBind(inc, iw.Interval)
|
||||||
|
|
||||||
if debugBOLL {
|
if debugBOLL {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/harmonic"
|
_ "github.com/c9s/bbgo/pkg/strategy/harmonic"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/irr"
|
_ "github.com/c9s/bbgo/pkg/strategy/irr"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/kline"
|
_ "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/marketcap"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/pivotshort"
|
_ "github.com/c9s/bbgo/pkg/strategy/pivotshort"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
|
_ "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