Merge pull request #862 from andycheng123/improve/supertrend-strategy

Improve: supertrend strategy
This commit is contained in:
Andy Cheng 2022-08-08 14:11:49 +08:00 committed by GitHub
commit 1cd48177ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 286 additions and 104 deletions

View File

@ -43,8 +43,10 @@ exchangeStrategies:
# ATR Multiplier for calculating super trend prices, the higher, the stronger the trends are
supertrendMultiplier: 4.1
# leverage is the leverage of the orders
# leverage uses the account net value to calculate the order qty
leverage: 1.0
# quantity sets the fixed order qty, takes precedence over Leverage
#quantity: 0.5
# fastDEMAWindow and slowDEMAWindow are for filtering super trend noise
fastDEMAWindow: 144

View File

@ -130,7 +130,24 @@ func (inc *Supertrend) GetSignal() types.Direction {
var _ types.SeriesExtend = &Supertrend{}
func (inc *Supertrend) PushK(k types.KLine) {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
return
}
inc.Update(k.GetHigh().Float64(), k.GetLow().Float64(), k.GetClose().Float64())
inc.EndTime = k.EndTime.Time()
inc.EmitUpdate(inc.Last())
}
func (inc *Supertrend) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) {
target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK))
}
func (inc *Supertrend) LoadK(allKLines []types.KLine) {
for _, k := range allKLines {
inc.PushK(k)
}
}
func (inc *Supertrend) CalculateAndUpdate(kLines []types.KLine) {

View File

@ -12,6 +12,8 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
var log = logrus.WithField("risk", "AccountValueCalculator")
var one = fixedpoint.One
var maxLeverage = fixedpoint.NewFromInt(10)
@ -140,6 +142,34 @@ func (c *AccountValueCalculator) NetValue(ctx context.Context) (fixedpoint.Value
return accountValue, nil
}
func (c *AccountValueCalculator) AvailableQuote(ctx context.Context) (fixedpoint.Value, error) {
accountValue := fixedpoint.Zero
if len(c.prices) == 0 {
if err := c.UpdatePrices(ctx); err != nil {
return accountValue, err
}
}
balances := c.session.Account.Balances()
for _, b := range balances {
if b.Currency == c.quoteCurrency {
accountValue = accountValue.Add(b.Available)
continue
}
symbol := b.Currency + c.quoteCurrency
price, ok := c.prices[symbol]
if !ok {
continue
}
accountValue = accountValue.Add(b.Available.Mul(price))
}
return accountValue, nil
}
// MarginLevel calculates the margin level from the asset market value and the debt value
// See https://www.binance.com/en/support/faq/360030493931
func (c *AccountValueCalculator) MarginLevel(ctx context.Context) (fixedpoint.Value, error) {
@ -164,7 +194,6 @@ func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, p
leverage = fixedpoint.NewFromInt(3)
}
baseBalance, _ := session.Account.Balance(market.BaseCurrency)
quoteBalance, _ := session.Account.Balance(market.QuoteCurrency)
@ -241,3 +270,29 @@ func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, p
return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your settings")
}
func CalculateQuoteQuantity(session *bbgo.ExchangeSession, ctx context.Context, quoteCurrency string, leverage fixedpoint.Value) (fixedpoint.Value, error) {
// default leverage guard
if leverage.IsZero() {
leverage = fixedpoint.NewFromInt(3)
}
quoteBalance, _ := session.Account.Balance(quoteCurrency)
accountValue := NewAccountValueCalculator(session, quoteCurrency)
usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
if !usingLeverage {
// For spot, we simply return the quote balance
return quoteBalance.Available.Mul(fixedpoint.Min(leverage, fixedpoint.One)), nil
}
// using leverage -- starts from here
availableQuote, err := accountValue.AvailableQuote(ctx)
if err != nil {
log.WithError(err).Errorf("can not update available quote")
return fixedpoint.Zero, err
}
logrus.Infof("calculating available leveraged quote quantity: account available quote = %+v", availableQuote)
return availableQuote.Mul(leverage), nil
}

View File

@ -1,73 +0,0 @@
package supertrend
import (
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
// LinGre is Linear Regression baseline
type LinGre struct {
types.IntervalWindow
baseLineSlope float64
}
// Update Linear Regression baseline slope
func (lg *LinGre) Update(klines []types.KLine) {
if len(klines) < lg.Window {
lg.baseLineSlope = 0
return
}
var sumX, sumY, sumXSqr, sumXY float64 = 0, 0, 0, 0
end := len(klines) - 1 // The last kline
for i := end; i >= end-lg.Window+1; i-- {
val := klines[i].GetClose().Float64()
per := float64(end - i + 1)
sumX += per
sumY += val
sumXSqr += per * per
sumXY += val * per
}
length := float64(lg.Window)
slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX)
average := sumY / length
endPrice := average - slope*sumX/length + slope
startPrice := endPrice + slope*(length-1)
lg.baseLineSlope = (length - 1) / (endPrice - startPrice)
log.Debugf("linear regression baseline slope: %f", lg.baseLineSlope)
}
func (lg *LinGre) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if lg.Interval != interval {
return
}
lg.Update(window)
}
func (lg *LinGre) Bind(updater indicator.KLineWindowUpdater) {
updater.OnKLineWindowUpdate(lg.handleKLineWindowUpdate)
}
// GetSignal get linear regression signal
func (lg *LinGre) GetSignal() types.Direction {
var lgSignal types.Direction = types.DirectionNone
switch {
case lg.baseLineSlope > 0:
lgSignal = types.DirectionUp
case lg.baseLineSlope < 0:
lgSignal = types.DirectionDown
}
return lgSignal
}
// preloadLinGre preloads linear regression indicator
func (lg *LinGre) preload(kLineStore *bbgo.MarketDataStore) {
if klines, ok := kLineStore.KLinesOfInterval(lg.Interval); ok {
lg.Update((*klines)[0:])
}
}

View File

@ -0,0 +1,104 @@
package supertrend
import (
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
"time"
)
// LinReg is Linear Regression baseline
type LinReg struct {
types.SeriesBase
types.IntervalWindow
// Values are the slopes of linear regression baseline
Values types.Float64Slice
klines types.KLineWindow
EndTime time.Time
}
// Last slope of linear regression baseline
func (lr *LinReg) Last() float64 {
if lr.Values.Length() == 0 {
return 0.0
}
return lr.Values.Last()
}
// Index returns the slope of specified index
func (lr *LinReg) Index(i int) float64 {
if i >= lr.Values.Length() {
return 0.0
}
return lr.Values.Index(i)
}
// Length of the slope values
func (lr *LinReg) Length() int {
return lr.Values.Length()
}
var _ types.SeriesExtend = &LinReg{}
// Update Linear Regression baseline slope
func (lr *LinReg) Update(kline types.KLine) {
lr.klines.Add(kline)
lr.klines.Truncate(lr.Window)
if len(lr.klines) < lr.Window {
lr.Values.Push(0)
return
}
var sumX, sumY, sumXSqr, sumXY float64 = 0, 0, 0, 0
end := len(lr.klines) - 1 // The last kline
for i := end; i >= end-lr.Window+1; i-- {
val := lr.klines[i].GetClose().Float64()
per := float64(end - i + 1)
sumX += per
sumY += val
sumXSqr += per * per
sumXY += val * per
}
length := float64(lr.Window)
slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX)
average := sumY / length
endPrice := average - slope*sumX/length + slope
startPrice := endPrice + slope*(length-1)
lr.Values.Push((length - 1) / (endPrice - startPrice))
log.Debugf("linear regression baseline slope: %f", lr.Last())
}
func (lr *LinReg) BindK(target indicator.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)
}
}
// GetSignal get linear regression signal
func (lr *LinReg) GetSignal() types.Direction {
var lrSignal types.Direction = types.DirectionNone
switch {
case lr.Last() > 0:
lrSignal = types.DirectionUp
case lr.Last() < 0:
lrSignal = types.DirectionDown
}
return lrSignal
}

View File

@ -1,8 +1,11 @@
package supertrend
import (
"bufio"
"context"
"fmt"
"github.com/c9s/bbgo/pkg/risk"
"github.com/fatih/color"
"os"
"sync"
@ -54,10 +57,13 @@ type Strategy struct {
SupertrendMultiplier float64 `json:"supertrendMultiplier"`
// LinearRegression Use linear regression as trend confirmation
LinearRegression *LinGre `json:"linearRegression,omitempty"`
LinearRegression *LinReg `json:"linearRegression,omitempty"`
// Leverage
Leverage float64 `json:"leverage"`
// Leverage uses the account net value to calculate the order qty
Leverage fixedpoint.Value `json:"leverage"`
// Quantity sets the fixed order qty, takes precedence over Leverage
Quantity fixedpoint.Value `json:"quantity"`
AccountValueCalculator *risk.AccountValueCalculator
// TakeProfitAtrMultiplier TP according to ATR multiple, 0 to disable this
TakeProfitAtrMultiplier float64 `json:"takeProfitAtrMultiplier"`
@ -84,6 +90,16 @@ type Strategy struct {
// StrategyController
bbgo.StrategyController
// Accumulated profit report
accumulatedProfit fixedpoint.Value
accumulatedProfitMA *indicator.SMA
// AccumulatedProfitMAWindow Accumulated profit SMA window
AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"`
dailyAccumulatedProfits types.Float64Slice
lastDayAccumulatedProfit fixedpoint.Value
// AccumulatedProfitLastPeriodWindow Last period window of accumulated profit
AccumulatedProfitLastPeriodWindow int `json:"accumulatedProfitLastPeriodWindow"`
}
func (s *Strategy) ID() string {
@ -103,10 +119,6 @@ func (s *Strategy) Validate() error {
return errors.New("interval is required")
}
if s.Leverage <= 0.0 {
return errors.New("leverage is required")
}
return nil
}
@ -115,6 +127,9 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LinearRegression.Interval})
s.ExitMethods.SetAndSubscribe(session, s)
// Accumulated profit report
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1d})
}
// Position control
@ -153,15 +168,6 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
return err
}
// preloadSupertrend preloads supertrend indicator
func preloadSupertrend(supertrend *indicator.Supertrend, kLineStore *bbgo.MarketDataStore) {
if klines, ok := kLineStore.KLinesOfInterval(supertrend.Interval); ok {
for i := 0; i < len(*klines); i++ {
supertrend.Update((*klines)[i].GetHigh().Float64(), (*klines)[i].GetLow().Float64(), (*klines)[i].GetClose().Float64())
}
}
}
// setupIndicators initializes indicators
func (s *Strategy) setupIndicators() {
// K-line store for indicators
@ -179,8 +185,10 @@ func (s *Strategy) setupIndicators() {
}
s.Supertrend = &indicator.Supertrend{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}, ATRMultiplier: s.SupertrendMultiplier}
s.Supertrend.AverageTrueRange = &indicator.ATR{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}}
s.Supertrend.Bind(kLineStore)
preloadSupertrend(s.Supertrend, kLineStore)
s.Supertrend.BindK(s.session.MarketDataStream, s.Symbol, s.Supertrend.Interval)
if klines, ok := kLineStore.KLinesOfInterval(s.Supertrend.Interval); ok {
s.Supertrend.LoadK((*klines)[0:])
}
// Linear Regression
if s.LinearRegression != nil {
@ -189,8 +197,10 @@ func (s *Strategy) setupIndicators() {
} else if s.LinearRegression.Interval == "" {
s.LinearRegression = nil
} else {
s.LinearRegression.Bind(kLineStore)
s.LinearRegression.preload(kLineStore)
s.LinearRegression.BindK(s.session.MarketDataStream, s.Symbol, s.LinearRegression.Interval)
if klines, ok := kLineStore.KLinesOfInterval(s.LinearRegression.Interval); ok {
s.LinearRegression.LoadK((*klines)[0:])
}
}
}
}
@ -251,17 +261,52 @@ func (s *Strategy) generateOrderForm(side types.SideType, quantity fixedpoint.Va
}
// calculateQuantity returns leveraged quantity
func (s *Strategy) calculateQuantity(currentPrice fixedpoint.Value) fixedpoint.Value {
balance, ok := s.session.GetAccount().Balance(s.Market.QuoteCurrency)
if !ok {
log.Errorf("can not update %s balance from exchange", s.Symbol)
return fixedpoint.Zero
func (s *Strategy) calculateQuantity(ctx context.Context, currentPrice fixedpoint.Value, side types.SideType) fixedpoint.Value {
// Quantity takes precedence
if !s.Quantity.IsZero() {
return s.Quantity
}
amountAvailable := balance.Available.Mul(fixedpoint.NewFromFloat(s.Leverage))
quantity := amountAvailable.Div(currentPrice)
usingLeverage := s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures
return quantity
if bbgo.IsBackTesting { // Backtesting
balance, ok := s.session.GetAccount().Balance(s.Market.QuoteCurrency)
if !ok {
log.Errorf("can not update %s quote balance from exchange", s.Symbol)
return fixedpoint.Zero
}
return balance.Available.Mul(fixedpoint.Min(s.Leverage, fixedpoint.One)).Div(currentPrice)
} else if !usingLeverage && side == types.SideTypeSell { // Spot sell
balance, ok := s.session.GetAccount().Balance(s.Market.BaseCurrency)
if !ok {
log.Errorf("can not update %s base balance from exchange", s.Symbol)
return fixedpoint.Zero
}
return balance.Available.Mul(fixedpoint.Min(s.Leverage, fixedpoint.One))
} else { // Using leverage or spot buy
quoteQty, err := risk.CalculateQuoteQuantity(s.session, ctx, s.Market.QuoteCurrency, s.Leverage)
if err != nil {
log.WithError(err).Errorf("can not update %s quote balance from exchange", s.Symbol)
return fixedpoint.Zero
}
return quoteQty.Div(currentPrice)
}
}
// PrintResult prints accumulated profit status
func (s *Strategy) PrintResult(o *os.File) {
f := bufio.NewWriter(o)
defer f.Flush()
hiyellow := color.New(color.FgHiYellow).FprintfFunc()
hiyellow(f, "------ %s Accumulated Profit Results ------\n", s.InstanceID())
hiyellow(f, "Symbol: %v\n", s.Symbol)
hiyellow(f, "Accumulated Profit: %v\n", s.accumulatedProfit)
hiyellow(f, "Accumulated Profit %dMA: %f\n", s.AccumulatedProfitMAWindow, s.accumulatedProfitMA.Last())
hiyellow(f, "Last %d day(s) Accumulated Profit: %f\n", s.AccumulatedProfitLastPeriodWindow, s.dailyAccumulatedProfits.Sum())
hiyellow(f, "\n")
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
@ -289,6 +334,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
if s.TradeStats == nil {
s.TradeStats = types.NewTradeStats(s.Symbol)
}
startTime := s.Environment.StartTime()
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime))
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime))
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1mo, startTime))
// Set fee rate
if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 {
@ -305,6 +354,31 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.orderExecutor.BindTradeStats(s.TradeStats)
s.orderExecutor.Bind()
// AccountValueCalculator
s.AccountValueCalculator = risk.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency)
// Accumulated profit report
if s.AccumulatedProfitMAWindow <= 0 {
s.AccumulatedProfitMAWindow = 60
}
s.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.AccumulatedProfitMAWindow}}
s.orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) {
if profit == nil {
return
}
s.accumulatedProfit = s.accumulatedProfit.Add(profit.Profit)
s.accumulatedProfitMA.Update(s.accumulatedProfit.Float64())
})
if s.AccumulatedProfitLastPeriodWindow <= 0 {
s.AccumulatedProfitLastPeriodWindow = 7
}
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1d, func(kline types.KLine) {
s.dailyAccumulatedProfits.Update(s.accumulatedProfit.Sub(s.lastDayAccumulatedProfit).Float64())
s.dailyAccumulatedProfits = s.dailyAccumulatedProfits.Tail(s.AccumulatedProfitLastPeriodWindow)
s.lastDayAccumulatedProfit = s.accumulatedProfit
}))
// Sync position to redis on trade
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(s)
@ -395,7 +469,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
}
orderForm := s.generateOrderForm(side, s.calculateQuantity(closePrice), types.SideEffectTypeMarginBuy)
orderForm := s.generateOrderForm(side, s.calculateQuantity(ctx, closePrice, side), types.SideEffectTypeMarginBuy)
log.Infof("submit open position order %v", orderForm)
_, err := s.orderExecutor.SubmitOrders(ctx, orderForm)
if err != nil {
@ -409,6 +483,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
// Print accumulated profit report
defer s.PrintResult(os.Stdout)
_ = s.orderExecutor.GracefulCancel(ctx)
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
})