2022-07-01 10:38:25 +00:00
package drift
import (
2022-08-03 06:10:56 +00:00
"bytes"
2022-07-01 10:38:25 +00:00
"context"
2022-07-15 03:30:04 +00:00
"errors"
2022-07-01 10:38:25 +00:00
"fmt"
2022-08-03 11:41:16 +00:00
"io"
2022-07-14 03:46:19 +00:00
"math"
2022-07-01 10:38:25 +00:00
"os"
2022-08-08 05:55:01 +00:00
"reflect"
"sort"
2022-07-14 03:46:19 +00:00
"strings"
2022-07-01 10:38:25 +00:00
"sync"
2022-07-14 03:46:19 +00:00
"github.com/fatih/color"
2022-07-01 10:38:25 +00:00
"github.com/sirupsen/logrus"
2022-07-12 10:14:57 +00:00
"github.com/wcharczuk/go-chart/v2"
2022-07-01 10:38:25 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
2022-08-03 08:34:51 +00:00
"github.com/c9s/bbgo/pkg/interact"
2022-07-01 10:38:25 +00:00
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
2022-08-08 05:55:01 +00:00
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
2022-07-01 10:38:25 +00:00
)
const ID = "drift"
2022-07-29 06:55:23 +00:00
const DDriftFilterNeg = - 0.7
const DDriftFilterPos = 0.7
2022-08-08 05:55:01 +00:00
const DriftFilterNeg = - 1.85
const DriftFilterPos = 1.85
2022-07-29 06:55:23 +00:00
2022-07-01 10:38:25 +00:00
var log = logrus . WithField ( "strategy" , ID )
2022-07-14 03:46:19 +00:00
var Four fixedpoint . Value = fixedpoint . NewFromInt ( 4 )
var Three fixedpoint . Value = fixedpoint . NewFromInt ( 3 )
var Two fixedpoint . Value = fixedpoint . NewFromInt ( 2 )
var Delta fixedpoint . Value = fixedpoint . NewFromFloat ( 0.01 )
2022-07-28 10:34:12 +00:00
var Fee = fixedpoint . NewFromFloat ( 0.0008 ) // taker fee % * 2, for upper bound
2022-07-01 10:38:25 +00:00
func init ( ) {
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
2022-07-14 03:46:19 +00:00
type SourceFunc func ( * types . KLine ) fixedpoint . Value
2022-07-01 10:38:25 +00:00
type Strategy struct {
Symbol string ` json:"symbol" `
bbgo . StrategyController
types . Market
types . IntervalWindow
* bbgo . Environment
2022-07-14 03:51:55 +00:00
* types . Position ` persistence:"position" `
* types . ProfitStats ` persistence:"profit_stats" `
* types . TradeStats ` persistence:"trade_stats" `
2022-07-01 10:38:25 +00:00
2022-08-02 11:21:44 +00:00
p * types . Position
trendLine types . UpdatableSeriesExtend
2022-07-29 06:55:23 +00:00
ma types . UpdatableSeriesExtend
stdevHigh * indicator . StdDev
stdevLow * indicator . StdDev
drift * DriftMA
2022-08-08 05:55:01 +00:00
drift1m * DriftMA
2022-07-29 06:55:23 +00:00
atr * indicator . ATR
midPrice fixedpoint . Value
lock sync . RWMutex
minutesCounter int
orderPendingCounter map [ uint64 ] int
2022-07-19 09:38:42 +00:00
2022-08-02 11:21:44 +00:00
beta float64
2022-07-28 10:34:12 +00:00
// This stores the maximum TP coefficient of ATR multiplier of each entry point
takeProfitFactor types . UpdatableSeriesExtend
2022-07-27 03:17:33 +00:00
Source string ` json:"source,omitempty" `
2022-07-20 10:49:56 +00:00
TakeProfitFactor float64 ` json:"takeProfitFactor" `
2022-07-28 10:34:12 +00:00
ProfitFactorWindow int ` json:"profitFactorWindow" `
2022-07-19 09:38:42 +00:00
StopLoss fixedpoint . Value ` json:"stoploss" `
CanvasPath string ` json:"canvasPath" `
PredictOffset int ` json:"predictOffset" `
HighLowVarianceMultiplier float64 ` json:"hlVarianceMultiplier" `
NoTrailingStopLoss bool ` json:"noTrailingStopLoss" `
2022-08-02 11:21:44 +00:00
TrailingStopLossType string ` json:"trailingStopLossType" ` // trailing stop sources. Possible options are `kline` for 1m kline and `realtime` from order updates
2022-07-28 10:34:12 +00:00
HLRangeWindow int ` json:"hlRangeWindow" `
SmootherWindow int ` json:"smootherWindow" `
FisherTransformWindow int ` json:"fisherTransformWindow" `
ATRWindow int ` json:"atrWindow" `
2022-08-02 11:21:44 +00:00
PendingMinutes int ` json:"pendingMinutes" ` // if order not be traded for pendingMinutes of time, cancel it.
NoRebalance bool ` json:"noRebalance" ` // disable rebalance
TrendWindow int ` json:"trendWindow" ` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote
RebalanceFilter float64 ` json:"rebalanceFilter" ` // beta filter on the Linear Regression of trendLine
TrailingCallbackRate [ ] float64 ` json:"trailingCallbackRate" `
2022-08-08 05:55:01 +00:00
TrailingActivationRatio [ ] float64 ` json:"trailingActivationRatio" `
2022-08-02 11:21:44 +00:00
buyPrice float64 ` persistence:"buy_price" `
sellPrice float64 ` persistence:"sell_price" `
highestPrice float64 ` persistence:"highest_price" `
lowestPrice float64 ` persistence:"lowest_price" `
2022-07-22 04:32:37 +00:00
2022-07-15 10:59:37 +00:00
// This is not related to trade but for statistics graph generation
// Will deduct fee in percentage from every trade
GraphPNLDeductFee bool ` json:"graphPNLDeductFee" `
GraphPNLPath string ` json:"graphPNLPath" `
GraphCumPNLPath string ` json:"graphCumPNLPath" `
// Whether to generate graph when shutdown
GenerateGraph bool ` json:"generateGraph" `
2022-07-05 11:43:05 +00:00
ExitMethods bbgo . ExitMethodSet ` json:"exits" `
Session * bbgo . ExchangeSession
2022-07-01 10:38:25 +00:00
* bbgo . GeneralOrderExecutor
2022-07-14 03:46:19 +00:00
getLastPrice func ( ) fixedpoint . Value
2022-07-15 03:30:04 +00:00
getSource SourceFunc
2022-07-14 03:46:19 +00:00
}
2022-08-08 05:55:01 +00:00
type jsonStruct struct {
key string
json string
tp string
value interface { }
}
type jsonArr [ ] jsonStruct
func ( a jsonArr ) Len ( ) int { return len ( a ) }
func ( a jsonArr ) Less ( i , j int ) bool { return a [ i ] . key < a [ j ] . key }
func ( a jsonArr ) Swap ( i , j int ) { a [ i ] , a [ j ] = a [ j ] , a [ i ] }
func ( s * Strategy ) Print ( f io . Writer , pretty bool , withColor ... bool ) {
//b, _ := json.MarshalIndent(s.ExitMethods, " ", " ")
t := table . NewWriter ( )
style := table . Style {
Name : "StyleRounded" ,
Box : table . StyleBoxRounded ,
Color : table . ColorOptionsDefault ,
Format : table . FormatOptionsDefault ,
HTML : table . DefaultHTMLOptions ,
Options : table . OptionsDefault ,
Title : table . TitleOptionsDefault ,
}
2022-08-03 11:41:16 +00:00
var hiyellow func ( io . Writer , string , ... interface { } )
if len ( withColor ) > 0 && withColor [ 0 ] {
2022-08-08 05:55:01 +00:00
if pretty {
style . Color = table . ColorOptionsYellowWhiteOnBlack
style . Color . Row = text . Colors { text . FgHiYellow , text . BgHiBlack }
style . Color . RowAlternate = text . Colors { text . FgYellow , text . BgBlack }
}
2022-08-03 11:41:16 +00:00
hiyellow = color . New ( color . FgHiYellow ) . FprintfFunc ( )
} else {
hiyellow = func ( a io . Writer , format string , args ... interface { } ) {
fmt . Fprintf ( a , format , args ... )
}
}
2022-08-08 05:55:01 +00:00
if pretty {
t . SetOutputMirror ( f )
t . SetStyle ( style )
t . AppendHeader ( table . Row { "json" , "struct field name" , "type" , "value" } )
}
2022-07-15 08:00:38 +00:00
hiyellow ( f , "------ %s Settings ------\n" , s . InstanceID ( ) )
2022-08-08 05:55:01 +00:00
embeddedWhiteSet := map [ string ] struct { } { "Window" : { } , "Interval" : { } , "Symbol" : { } }
redundantSet := map [ string ] struct { } { }
var rows [ ] table . Row
val := reflect . ValueOf ( * s )
var values jsonArr
for i := 0 ; i < val . Type ( ) . NumField ( ) ; i ++ {
t := val . Type ( ) . Field ( i )
if ! t . IsExported ( ) {
continue
}
fieldName := t . Name
switch jsonTag := t . Tag . Get ( "json" ) ; jsonTag {
case "-" :
case "" :
if t . Anonymous {
var target reflect . Type
if t . Type . Kind ( ) == reflect . Pointer {
target = t . Type . Elem ( )
} else {
target = t . Type
}
for j := 0 ; j < target . NumField ( ) ; j ++ {
tt := target . Field ( j )
if ! tt . IsExported ( ) {
continue
}
fieldName := tt . Name
if _ , ok := embeddedWhiteSet [ fieldName ] ; ! ok {
continue
}
if jtag := tt . Tag . Get ( "json" ) ; jtag != "" && jtag != "-" {
name := strings . Split ( jtag , "," ) [ 0 ]
if _ , ok := redundantSet [ name ] ; ok {
continue
}
redundantSet [ name ] = struct { } { }
var value interface { }
if t . Type . Kind ( ) == reflect . Pointer {
value = val . Field ( i ) . Elem ( ) . Field ( j ) . Interface ( )
} else {
value = val . Field ( i ) . Field ( j ) . Interface ( )
}
values = append ( values , jsonStruct { key : fieldName , json : name , tp : tt . Type . String ( ) , value : value } )
}
}
}
default :
name := strings . Split ( jsonTag , "," ) [ 0 ]
if _ , ok := redundantSet [ name ] ; ok {
continue
}
redundantSet [ name ] = struct { } { }
values = append ( values , jsonStruct { key : fieldName , json : name , tp : t . Type . String ( ) , value : val . Field ( i ) . Interface ( ) } )
}
}
sort . Sort ( values )
for _ , value := range values {
if pretty {
rows = append ( rows , table . Row { value . json , value . key , value . tp , value . value } )
} else {
hiyellow ( f , "%s: %v\n" , value . json , value . value )
}
}
if pretty {
rows = append ( rows , table . Row { "takeProfitFactor(last)" , "takeProfitFactor" , "float64" , s . takeProfitFactor . Last ( ) } )
t . AppendRows ( rows )
t . Render ( )
} else {
hiyellow ( f , "takeProfitFactor(last): %f\n" , s . takeProfitFactor . Last ( ) )
}
2022-07-01 10:38:25 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) InstanceID ( ) string {
2022-08-08 05:55:01 +00:00
return fmt . Sprintf ( "%s:%s:%v" , ID , "" /*s.Symbol*/ , bbgo . IsBackTesting )
2022-07-01 10:38:25 +00:00
}
func ( s * Strategy ) Subscribe ( session * bbgo . ExchangeSession ) {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions {
Interval : s . Interval ,
} )
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions {
Interval : types . Interval1m ,
} )
if ! bbgo . IsBackTesting {
2022-07-14 04:44:05 +00:00
session . Subscribe ( types . BookTickerChannel , s . Symbol , types . SubscribeOptions { } )
2022-07-01 10:38:25 +00:00
}
2022-07-05 11:43:05 +00:00
s . ExitMethods . SetAndSubscribe ( session , s )
2022-07-01 10:38:25 +00:00
}
2022-07-20 10:49:56 +00:00
func ( s * Strategy ) CurrentPosition ( ) * types . Position {
return s . Position
}
func ( s * Strategy ) ClosePosition ( ctx context . Context , percentage fixedpoint . Value ) error {
2022-08-02 11:21:44 +00:00
order := s . p . NewMarketCloseOrder ( percentage )
2022-07-12 10:14:57 +00:00
if order == nil {
2022-07-20 10:49:56 +00:00
return nil
}
2022-07-15 10:59:37 +00:00
order . Tag = "close"
2022-07-04 12:13:54 +00:00
order . TimeInForce = ""
2022-08-02 11:21:44 +00:00
balances := s . GeneralOrderExecutor . Session ( ) . GetAccount ( ) . Balances ( )
2022-07-04 12:13:54 +00:00
baseBalance := balances [ s . Market . BaseCurrency ] . Available
2022-07-14 03:46:19 +00:00
price := s . getLastPrice ( )
2022-07-04 12:13:54 +00:00
if order . Side == types . SideTypeBuy {
quoteAmount := balances [ s . Market . QuoteCurrency ] . Available . Div ( price )
if order . Quantity . Compare ( quoteAmount ) > 0 {
order . Quantity = quoteAmount
}
} else if order . Side == types . SideTypeSell && order . Quantity . Compare ( baseBalance ) > 0 {
order . Quantity = baseBalance
}
for {
if s . Market . IsDustQuantity ( order . Quantity , price ) {
2022-07-20 10:49:56 +00:00
return nil
2022-07-04 12:13:54 +00:00
}
2022-07-20 10:49:56 +00:00
_ , err := s . GeneralOrderExecutor . SubmitOrders ( ctx , * order )
2022-07-04 12:13:54 +00:00
if err != nil {
order . Quantity = order . Quantity . Mul ( fixedpoint . One . Sub ( Delta ) )
continue
}
2022-07-20 10:49:56 +00:00
return nil
2022-07-04 12:13:54 +00:00
}
}
2022-07-14 03:46:19 +00:00
func ( s * Strategy ) SourceFuncGenerator ( ) SourceFunc {
switch strings . ToLower ( s . Source ) {
case "close" :
return func ( kline * types . KLine ) fixedpoint . Value { return kline . Close }
case "high" :
return func ( kline * types . KLine ) fixedpoint . Value { return kline . High }
case "low" :
return func ( kline * types . KLine ) fixedpoint . Value { return kline . Low }
case "hl2" :
return func ( kline * types . KLine ) fixedpoint . Value {
return kline . High . Add ( kline . Low ) . Div ( Two )
}
case "hlc3" :
return func ( kline * types . KLine ) fixedpoint . Value {
return kline . High . Add ( kline . Low ) . Add ( kline . Close ) . Div ( Three )
}
case "ohlc4" :
return func ( kline * types . KLine ) fixedpoint . Value {
return kline . Open . Add ( kline . High ) . Add ( kline . Low ) . Add ( kline . Close ) . Div ( Four )
}
case "open" :
return func ( kline * types . KLine ) fixedpoint . Value { return kline . Open }
case "" :
2022-07-27 03:17:33 +00:00
log . Infof ( "source not set, use hl2 by default" )
2022-07-14 03:46:19 +00:00
return func ( kline * types . KLine ) fixedpoint . Value {
return kline . High . Add ( kline . Low ) . Div ( Two )
}
default :
panic ( fmt . Sprintf ( "Unable to parse: %s" , s . Source ) )
}
}
2022-07-25 10:36:22 +00:00
type DriftMA struct {
types . SeriesBase
2022-07-28 10:34:12 +00:00
ma1 types . UpdatableSeriesExtend
2022-07-25 10:36:22 +00:00
drift * indicator . Drift
2022-07-28 10:34:12 +00:00
ma2 types . UpdatableSeriesExtend
2022-07-25 10:36:22 +00:00
}
func ( s * DriftMA ) Update ( value float64 ) {
s . ma1 . Update ( value )
s . drift . Update ( s . ma1 . Last ( ) )
s . ma2 . Update ( s . drift . Last ( ) )
}
func ( s * DriftMA ) Last ( ) float64 {
return s . ma2 . Last ( )
}
func ( s * DriftMA ) Index ( i int ) float64 {
return s . ma2 . Index ( i )
}
func ( s * DriftMA ) Length ( ) int {
return s . ma2 . Length ( )
}
func ( s * DriftMA ) ZeroPoint ( ) float64 {
return s . drift . ZeroPoint ( )
2022-07-15 03:30:04 +00:00
}
2022-07-12 10:14:57 +00:00
2022-07-28 10:34:12 +00:00
func ( s * DriftMA ) Clone ( ) * DriftMA {
out := DriftMA {
ma1 : types . Clone ( s . ma1 ) ,
drift : s . drift . Clone ( ) ,
ma2 : types . Clone ( s . ma2 ) ,
}
out . SeriesBase . Series = & out
return & out
}
func ( s * DriftMA ) TestUpdate ( v float64 ) * DriftMA {
out := s . Clone ( )
out . Update ( v )
return out
}
2022-08-03 06:10:56 +00:00
func ( s * Strategy ) initIndicators ( kline * types . KLine , priceLines * types . Queue ) error {
2022-07-28 10:34:12 +00:00
s . ma = & indicator . SMA { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . HLRangeWindow } }
s . stdevHigh = & indicator . StdDev { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . HLRangeWindow } }
s . stdevLow = & indicator . StdDev { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . HLRangeWindow } }
2022-07-25 10:36:22 +00:00
s . drift = & DriftMA {
drift : & indicator . Drift {
MA : & indicator . SMA { IntervalWindow : s . IntervalWindow } ,
IntervalWindow : s . IntervalWindow ,
} ,
ma1 : & indicator . EWMA {
2022-07-28 10:34:12 +00:00
IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . SmootherWindow } ,
2022-07-25 10:36:22 +00:00
} ,
ma2 : & indicator . FisherTransform {
2022-07-28 10:34:12 +00:00
IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . FisherTransformWindow } ,
2022-07-25 10:36:22 +00:00
} ,
2022-07-14 03:46:19 +00:00
}
2022-07-25 10:36:22 +00:00
s . drift . SeriesBase . Series = s . drift
2022-08-08 05:55:01 +00:00
s . drift1m = & DriftMA {
drift : & indicator . Drift {
MA : & indicator . SMA { IntervalWindow : types . IntervalWindow { Interval : types . Interval1m , Window : 2 } } ,
IntervalWindow : types . IntervalWindow { Interval : types . Interval1m , Window : 2 } ,
} ,
ma1 : & indicator . EWMA {
IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : 24 } ,
} ,
ma2 : & indicator . FisherTransform {
IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . FisherTransformWindow * 15 } ,
} ,
2022-08-02 11:21:44 +00:00
}
2022-08-08 05:55:01 +00:00
s . drift1m . SeriesBase . Series = s . drift1m
2022-07-28 10:34:12 +00:00
s . atr = & indicator . ATR { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . ATRWindow } }
s . takeProfitFactor = & indicator . SMA { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . ProfitFactorWindow } }
2022-08-02 11:21:44 +00:00
s . trendLine = & indicator . EWMA { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . TrendWindow } }
2022-07-28 10:34:12 +00:00
for i := 0 ; i < s . ProfitFactorWindow ; i ++ {
s . takeProfitFactor . Update ( s . TakeProfitFactor )
}
2022-07-15 03:30:04 +00:00
store , _ := s . Session . MarketDataStore ( s . Symbol )
2022-07-01 10:38:25 +00:00
klines , ok := store . KLinesOfInterval ( s . Interval )
if ! ok {
2022-07-15 03:30:04 +00:00
return errors . New ( "klines not exists" )
2022-07-01 10:38:25 +00:00
}
2022-07-14 03:46:19 +00:00
2022-07-01 10:38:25 +00:00
for _ , kline := range * klines {
2022-07-15 03:30:04 +00:00
source := s . getSource ( & kline ) . Float64 ( )
2022-07-19 09:38:42 +00:00
high := kline . High . Float64 ( )
low := kline . Low . Float64 ( )
s . ma . Update ( source )
s . stdevHigh . Update ( high - s . ma . Last ( ) )
s . stdevLow . Update ( s . ma . Last ( ) - low )
2022-07-12 10:14:57 +00:00
s . drift . Update ( source )
2022-08-02 11:21:44 +00:00
s . trendLine . Update ( source )
2022-07-14 05:50:37 +00:00
s . atr . PushK ( kline )
2022-08-03 06:10:56 +00:00
priceLines . Update ( source )
}
if kline != nil && klines != nil {
kline . Set ( & ( * klines ) [ len ( * klines ) - 1 ] )
2022-07-01 10:38:25 +00:00
}
2022-08-02 11:21:44 +00:00
klines , ok = store . KLinesOfInterval ( types . Interval1m )
if ! ok {
return errors . New ( "klines not exists" )
}
for _ , kline := range * klines {
source := s . getSource ( & kline ) . Float64 ( )
s . drift1m . Update ( source )
}
2022-07-15 03:30:04 +00:00
return nil
}
2022-07-01 10:38:25 +00:00
2022-07-29 06:55:23 +00:00
func ( s * Strategy ) smartCancel ( ctx context . Context , pricef , atr , takeProfitFactor float64 ) ( int , error ) {
nonTraded := s . GeneralOrderExecutor . ActiveMakerOrders ( ) . Orders ( )
if len ( nonTraded ) > 0 {
if len ( nonTraded ) > 1 {
log . Errorf ( "should only have one order to cancel, got %d" , len ( nonTraded ) )
}
toCancel := false
2022-08-08 05:55:01 +00:00
drift := s . drift1m . Array ( 2 )
2022-07-29 06:55:23 +00:00
for _ , order := range nonTraded {
if s . minutesCounter - s . orderPendingCounter [ order . OrderID ] > s . PendingMinutes {
2022-08-08 05:55:01 +00:00
if order . Side == types . SideTypeBuy && drift [ 1 ] > drift [ 0 ] {
continue
} else if order . Side == types . SideTypeSell && drift [ 1 ] < drift [ 0 ] {
continue
}
2022-07-29 06:55:23 +00:00
toCancel = true
} else if order . Side == types . SideTypeBuy {
2022-08-08 05:55:01 +00:00
// 75% of the probability
if order . Price . Float64 ( ) + s . stdevHigh . Last ( ) * 2 <= pricef {
2022-07-29 06:55:23 +00:00
toCancel = true
}
} else if order . Side == types . SideTypeSell {
2022-08-08 05:55:01 +00:00
// 75% of the probability
if order . Price . Float64 ( ) - s . stdevLow . Last ( ) * 2 >= pricef {
2022-07-29 06:55:23 +00:00
toCancel = true
}
} else {
panic ( "not supported side for the order" )
}
}
if toCancel {
err := s . GeneralOrderExecutor . GracefulCancel ( ctx )
// TODO: clean orderPendingCounter on cancel/trade
if err == nil {
for _ , order := range nonTraded {
delete ( s . orderPendingCounter , order . OrderID )
}
}
return 0 , err
}
}
return len ( nonTraded ) , nil
}
2022-08-02 11:21:44 +00:00
func ( s * Strategy ) trailingCheck ( price float64 , direction string ) bool {
avg := s . buyPrice + s . sellPrice
if s . highestPrice > 0 && s . highestPrice < price {
s . highestPrice = price
}
if s . lowestPrice > 0 && s . lowestPrice > price {
s . lowestPrice = price
}
isShort := direction == "short"
for i := len ( s . TrailingCallbackRate ) - 1 ; i >= 0 ; i -- {
trailingCallbackRate := s . TrailingCallbackRate [ i ]
trailingActivationRatio := s . TrailingActivationRatio [ i ]
if isShort {
if ( avg - s . lowestPrice ) / s . lowestPrice > trailingActivationRatio {
return ( price - s . lowestPrice ) / s . lowestPrice > trailingCallbackRate
}
} else {
if ( s . highestPrice - avg ) / avg > trailingActivationRatio {
return ( s . highestPrice - price ) / price > trailingCallbackRate
}
}
}
return false
}
2022-07-27 03:17:33 +00:00
func ( s * Strategy ) initTickerFunctions ( ctx context . Context ) {
2022-07-14 10:33:47 +00:00
if s . IsBackTesting ( ) {
2022-07-14 03:46:19 +00:00
s . getLastPrice = func ( ) fixedpoint . Value {
lastPrice , ok := s . Session . LastPrice ( s . Symbol )
if ! ok {
log . Error ( "cannot get lastprice" )
}
return lastPrice
2022-07-01 10:38:25 +00:00
}
2022-07-14 03:46:19 +00:00
} else {
2022-07-15 03:30:04 +00:00
s . Session . MarketDataStream . OnBookTickerUpdate ( func ( ticker types . BookTicker ) {
2022-07-14 03:46:19 +00:00
bestBid := ticker . Buy
bestAsk := ticker . Sell
2022-07-28 10:34:12 +00:00
var pricef , atr , avg float64
2022-07-15 03:30:04 +00:00
var price fixedpoint . Value
2022-07-14 03:46:19 +00:00
if util . TryLock ( & s . lock ) {
if ! bestAsk . IsZero ( ) && ! bestBid . IsZero ( ) {
s . midPrice = bestAsk . Add ( bestBid ) . Div ( Two )
} else if ! bestAsk . IsZero ( ) {
s . midPrice = bestAsk
} else {
s . midPrice = bestBid
}
2022-07-15 03:30:04 +00:00
price = s . midPrice
pricef = s . midPrice . Float64 ( )
} else {
return
2022-07-14 03:46:19 +00:00
}
2022-07-29 06:55:23 +00:00
defer s . lock . Unlock ( )
// for trailing stoploss during the realtime
2022-08-02 11:21:44 +00:00
if s . NoTrailingStopLoss || s . TrailingStopLossType == "kline" {
2022-07-29 06:55:23 +00:00
return
}
atr = s . atr . Last ( )
takeProfitFactor := s . takeProfitFactor . Predict ( 2 )
numPending := 0
var err error
if numPending , err = s . smartCancel ( ctx , pricef , atr , takeProfitFactor ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot cancel orders" )
return
}
if numPending > 0 {
return
}
2022-07-25 10:36:22 +00:00
if s . highestPrice > 0 && s . highestPrice < pricef {
s . highestPrice = pricef
}
if s . lowestPrice > 0 && s . lowestPrice > pricef {
s . lowestPrice = pricef
}
2022-07-22 04:32:37 +00:00
avg = s . buyPrice + s . sellPrice
2022-07-29 06:55:23 +00:00
2022-08-08 05:55:01 +00:00
exitShortCondition := ( /*avg*(1.+stoploss) <= pricef || (ddrift > 0 && drift > DDriftFilterPos) ||*/ avg - atr * takeProfitFactor >= pricef ||
2022-08-02 11:21:44 +00:00
s . trailingCheck ( pricef , "short" ) ) &&
( s . p . IsShort ( ) && ! s . p . IsDust ( price ) )
2022-08-08 05:55:01 +00:00
exitLongCondition := ( /*avg*(1.-stoploss) >= pricef || (ddrift < 0 && drift < DDriftFilterNeg) ||*/ avg + atr * takeProfitFactor <= pricef ||
2022-08-02 11:21:44 +00:00
s . trailingCheck ( pricef , "long" ) ) &&
( ! s . p . IsLong ( ) && ! s . p . IsDust ( price ) )
2022-07-15 03:30:04 +00:00
if exitShortCondition || exitLongCondition {
2022-07-28 10:34:12 +00:00
if exitLongCondition && s . highestPrice > avg {
2022-08-08 05:55:01 +00:00
s . takeProfitFactor . Update ( ( s . highestPrice - avg ) / atr * 1.5 )
2022-07-28 10:34:12 +00:00
} else if exitShortCondition && avg > s . lowestPrice {
2022-08-08 05:55:01 +00:00
s . takeProfitFactor . Update ( ( avg - s . lowestPrice ) / atr * 1.5 )
2022-07-15 03:30:04 +00:00
}
2022-08-08 05:55:01 +00:00
log . Infof ( "Close position by orderbook changes" )
2022-07-20 10:49:56 +00:00
_ = s . ClosePosition ( ctx , fixedpoint . One )
2022-07-15 03:30:04 +00:00
}
2022-07-14 03:46:19 +00:00
} )
s . getLastPrice = func ( ) ( lastPrice fixedpoint . Value ) {
var ok bool
s . lock . RLock ( )
if s . midPrice . IsZero ( ) {
lastPrice , ok = s . Session . LastPrice ( s . Symbol )
if ! ok {
log . Error ( "cannot get lastprice" )
2022-08-08 05:55:01 +00:00
s . lock . RUnlock ( )
2022-07-14 03:46:19 +00:00
return lastPrice
}
2022-07-01 10:38:25 +00:00
} else {
2022-07-14 03:46:19 +00:00
lastPrice = s . midPrice
2022-07-01 10:38:25 +00:00
}
2022-07-14 03:46:19 +00:00
s . lock . RUnlock ( )
return lastPrice
2022-07-01 10:38:25 +00:00
}
2022-07-14 03:46:19 +00:00
}
2022-07-01 10:38:25 +00:00
2022-07-15 03:30:04 +00:00
}
2022-08-03 06:10:56 +00:00
func ( s * Strategy ) DrawIndicators ( time types . Time , priceLine types . SeriesExtend , zeroPoints types . Series ) * types . Canvas {
2022-07-15 08:00:38 +00:00
canvas := types . NewCanvas ( s . InstanceID ( ) , s . Interval )
Length := priceLine . Length ( )
2022-07-25 10:36:22 +00:00
if Length > 300 {
Length = 300
2022-07-15 08:00:38 +00:00
}
2022-08-03 06:10:56 +00:00
log . Infof ( "draw indicators with %d data" , Length )
2022-07-15 08:00:38 +00:00
mean := priceLine . Mean ( Length )
highestPrice := priceLine . Minus ( mean ) . Abs ( ) . Highest ( Length )
highestDrift := s . drift . Abs ( ) . Highest ( Length )
2022-07-25 10:36:22 +00:00
hi := s . drift . drift . Abs ( ) . Highest ( Length )
ratio := highestPrice / highestDrift
canvas . Plot ( "upband" , s . ma . Add ( s . stdevHigh ) , time , Length )
canvas . Plot ( "ma" , s . ma , time , Length )
canvas . Plot ( "downband" , s . ma . Minus ( s . stdevLow ) , time , Length )
canvas . Plot ( "drift" , s . drift . Mul ( ratio ) . Add ( mean ) , time , Length )
canvas . Plot ( "driftOrig" , s . drift . drift . Mul ( highestPrice / hi ) . Add ( mean ) , time , Length )
canvas . Plot ( "zero" , types . NumberSeries ( mean ) , time , Length )
canvas . Plot ( "price" , priceLine , time , Length )
canvas . Plot ( "zeroPoint" , zeroPoints , time , Length )
2022-08-03 06:10:56 +00:00
return canvas
}
func ( s * Strategy ) DrawPNL ( profit types . Series ) * types . Canvas {
canvas := types . NewCanvas ( s . InstanceID ( ) )
2022-08-08 05:55:01 +00:00
log . Errorf ( "pnl Highest: %f, Lowest: %f" , types . Highest ( profit , profit . Length ( ) ) , types . Lowest ( profit , profit . Length ( ) ) )
length := profit . Length ( )
2022-08-03 06:10:56 +00:00
if s . GraphPNLDeductFee {
2022-08-08 05:55:01 +00:00
canvas . PlotRaw ( "pnl % (with Fee Deducted)" , profit , length )
2022-08-03 06:10:56 +00:00
} else {
2022-08-08 05:55:01 +00:00
canvas . PlotRaw ( "pnl %" , profit , length )
2022-08-03 06:10:56 +00:00
}
2022-08-08 05:55:01 +00:00
canvas . YAxis = chart . YAxis {
ValueFormatter : func ( v interface { } ) string {
if vf , isFloat := v . ( float64 ) ; isFloat {
return fmt . Sprintf ( "%.4f" , vf )
}
return ""
} ,
}
canvas . PlotRaw ( "1" , types . NumberSeries ( 1 ) , length )
2022-08-03 06:10:56 +00:00
return canvas
}
func ( s * Strategy ) DrawCumPNL ( cumProfit types . Series ) * types . Canvas {
canvas := types . NewCanvas ( s . InstanceID ( ) )
if s . GraphPNLDeductFee {
canvas . PlotRaw ( "cummulative pnl % (with Fee Deducted)" , cumProfit , cumProfit . Length ( ) )
} else {
canvas . PlotRaw ( "cummulative pnl %" , cumProfit , cumProfit . Length ( ) )
}
2022-08-08 05:55:01 +00:00
canvas . YAxis = chart . YAxis {
ValueFormatter : func ( v interface { } ) string {
if vf , isFloat := v . ( float64 ) ; isFloat {
return fmt . Sprintf ( "%.4f" , vf )
}
return ""
} ,
}
2022-08-03 06:10:56 +00:00
return canvas
}
func ( s * Strategy ) Draw ( time types . Time , priceLine types . SeriesExtend , profit types . Series , cumProfit types . Series , zeroPoints types . Series ) {
canvas := s . DrawIndicators ( time , priceLine , zeroPoints )
2022-07-15 08:00:38 +00:00
f , err := os . Create ( s . CanvasPath )
if err != nil {
log . WithError ( err ) . Errorf ( "cannot create on %s" , s . CanvasPath )
return
}
defer f . Close ( )
if err := canvas . Render ( chart . PNG , f ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot render in drift" )
}
2022-07-15 10:59:37 +00:00
2022-08-03 06:10:56 +00:00
canvas = s . DrawPNL ( profit )
2022-07-15 10:59:37 +00:00
f , err = os . Create ( s . GraphPNLPath )
if err != nil {
2022-07-20 10:49:56 +00:00
log . WithError ( err ) . Errorf ( "open pnl" )
return
2022-07-15 10:59:37 +00:00
}
defer f . Close ( )
if err := canvas . Render ( chart . PNG , f ) ; err != nil {
2022-07-20 10:49:56 +00:00
log . WithError ( err ) . Errorf ( "render pnl" )
2022-07-15 10:59:37 +00:00
}
2022-08-03 06:10:56 +00:00
canvas = s . DrawCumPNL ( cumProfit )
2022-07-15 10:59:37 +00:00
f , err = os . Create ( s . GraphCumPNLPath )
if err != nil {
2022-07-20 10:49:56 +00:00
log . WithError ( err ) . Errorf ( "open cumpnl" )
return
2022-07-15 10:59:37 +00:00
}
defer f . Close ( )
if err := canvas . Render ( chart . PNG , f ) ; err != nil {
2022-07-20 10:49:56 +00:00
log . WithError ( err ) . Errorf ( "render cumpnl" )
2022-07-15 10:59:37 +00:00
}
2022-07-15 08:00:38 +00:00
}
2022-08-02 11:21:44 +00:00
// Sending new rebalance orders cost too much.
// Modify the position instead to expect the strategy itself rebalance on Close
func ( s * Strategy ) Rebalance ( ctx context . Context , orderTagHistory map [ uint64 ] string ) {
price := s . getLastPrice ( )
_ , beta := types . LinearRegression ( s . trendLine , 3 )
if math . Abs ( beta ) > s . RebalanceFilter && math . Abs ( s . beta ) > s . RebalanceFilter || math . Abs ( s . beta ) < s . RebalanceFilter && math . Abs ( beta ) < s . RebalanceFilter {
return
}
balances := s . GeneralOrderExecutor . Session ( ) . GetAccount ( ) . Balances ( )
baseBalance := balances [ s . Market . BaseCurrency ] . Total ( )
quoteBalance := balances [ s . Market . QuoteCurrency ] . Total ( )
total := baseBalance . Add ( quoteBalance . Div ( price ) )
percentage := fixedpoint . One . Sub ( Delta )
log . Infof ( "rebalance beta %f %v" , beta , s . p )
if beta > s . RebalanceFilter {
if total . Mul ( percentage ) . Compare ( baseBalance ) > 0 {
q := total . Mul ( percentage ) . Sub ( baseBalance )
s . p . Lock ( )
defer s . p . Unlock ( )
s . p . Base = q . Neg ( )
s . p . Quote = q . Mul ( price )
s . p . AverageCost = price
}
/ * if total . Mul ( percentage ) . Compare ( baseBalance ) > 0 {
q := total . Mul ( percentage ) . Sub ( baseBalance )
if s . Market . IsDustQuantity ( q , price ) {
return
}
err := s . GeneralOrderExecutor . GracefulCancel ( ctx )
if err != nil {
panic ( fmt . Sprintf ( "%s" , err ) )
}
orders , err := s . GeneralOrderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : s . Symbol ,
Side : types . SideTypeBuy ,
Type : types . OrderTypeMarket ,
Price : price ,
Quantity : q ,
Tag : "rebalance" ,
} )
if err == nil {
orderTagHistory [ orders [ 0 ] . OrderID ] = "rebalance"
} else {
log . WithError ( err ) . Errorf ( "rebalance %v %v %v %v" , total , q , balances [ s . Market . QuoteCurrency ] . Available , quoteBalance )
}
} * /
} else if beta <= - s . RebalanceFilter {
if total . Mul ( percentage ) . Compare ( quoteBalance . Div ( price ) ) > 0 {
q := total . Mul ( percentage ) . Sub ( quoteBalance . Div ( price ) )
s . p . Lock ( )
defer s . p . Unlock ( )
s . p . Base = q
s . p . Quote = q . Mul ( price ) . Neg ( )
s . p . AverageCost = price
}
/ * if total . Mul ( percentage ) . Compare ( quoteBalance . Div ( price ) ) > 0 {
q := total . Mul ( percentage ) . Sub ( quoteBalance . Div ( price ) )
if s . Market . IsDustQuantity ( q , price ) {
return
}
err := s . GeneralOrderExecutor . GracefulCancel ( ctx )
if err != nil {
panic ( fmt . Sprintf ( "%s" , err ) )
}
orders , err := s . GeneralOrderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : s . Symbol ,
Side : types . SideTypeSell ,
Type : types . OrderTypeMarket ,
Price : price ,
Quantity : q ,
Tag : "rebalance" ,
} )
if err == nil {
orderTagHistory [ orders [ 0 ] . OrderID ] = "rebalance"
} else {
log . WithError ( err ) . Errorf ( "rebalance %v %v %v %v" , total , q , balances [ s . Market . BaseCurrency ] . Available , baseBalance )
}
} * /
} else {
if total . Div ( Two ) . Compare ( quoteBalance . Div ( price ) ) > 0 {
q := total . Div ( Two ) . Sub ( quoteBalance . Div ( price ) )
s . p . Lock ( )
defer s . p . Unlock ( )
s . p . Base = q
s . p . Quote = q . Mul ( price ) . Neg ( )
s . p . AverageCost = price
} else if total . Div ( Two ) . Compare ( baseBalance ) > 0 {
q := total . Div ( Two ) . Sub ( baseBalance )
s . p . Lock ( )
defer s . p . Unlock ( )
s . p . Base = q . Neg ( )
s . p . Quote = q . Mul ( price )
s . p . AverageCost = price
} else {
s . p . Lock ( )
defer s . p . Unlock ( )
s . p . Reset ( )
}
}
log . Infof ( "rebalanceafter %v %v %v" , baseBalance , quoteBalance , s . p )
s . beta = beta
}
2022-07-15 03:30:04 +00:00
func ( s * Strategy ) Run ( ctx context . Context , orderExecutor bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
instanceID := s . InstanceID ( )
// Will be set by persistence if there's any from DB
if s . Position == nil {
s . Position = types . NewPositionFromMarket ( s . Market )
2022-08-02 11:21:44 +00:00
s . p = types . NewPositionFromMarket ( s . Market )
2022-08-03 06:10:56 +00:00
} else {
s . p = types . NewPositionFromMarket ( s . Market )
s . p . Base = s . Position . Base
s . p . Quote = s . Position . Quote
s . p . AverageCost = s . Position . AverageCost
2022-07-15 03:30:04 +00:00
}
if s . ProfitStats == nil {
s . ProfitStats = types . NewProfitStats ( s . Market )
}
if s . TradeStats == nil {
s . TradeStats = types . NewTradeStats ( s . Symbol )
}
2022-07-19 09:38:42 +00:00
startTime := s . Environment . StartTime ( )
s . TradeStats . SetIntervalProfitCollector ( types . NewIntervalProfitCollector ( types . Interval1d , startTime ) )
s . TradeStats . SetIntervalProfitCollector ( types . NewIntervalProfitCollector ( types . Interval1w , startTime ) )
2022-07-15 03:30:04 +00:00
// StrategyController
s . Status = types . StrategyStatusRunning
// Get source function from config input
s . getSource = s . SourceFuncGenerator ( )
s . OnSuspend ( func ( ) {
_ = s . GeneralOrderExecutor . GracefulCancel ( ctx )
} )
s . OnEmergencyStop ( func ( ) {
_ = s . GeneralOrderExecutor . GracefulCancel ( ctx )
2022-07-20 10:49:56 +00:00
_ = s . ClosePosition ( ctx , fixedpoint . One )
2022-07-15 03:30:04 +00:00
} )
s . GeneralOrderExecutor = bbgo . NewGeneralOrderExecutor ( session , s . Symbol , ID , instanceID , s . Position )
s . GeneralOrderExecutor . BindEnvironment ( s . Environment )
s . GeneralOrderExecutor . BindProfitStats ( s . ProfitStats )
s . GeneralOrderExecutor . BindTradeStats ( s . TradeStats )
s . GeneralOrderExecutor . TradeCollector ( ) . OnPositionUpdate ( func ( position * types . Position ) {
bbgo . Sync ( s )
} )
s . GeneralOrderExecutor . Bind ( )
2022-07-29 06:55:23 +00:00
s . orderPendingCounter = make ( map [ uint64 ] int )
s . minutesCounter = 0
2022-07-15 03:30:04 +00:00
// Exit methods from config
for _ , method := range s . ExitMethods {
method . Bind ( session , s . GeneralOrderExecutor )
}
2022-07-15 10:59:37 +00:00
buyPrice := fixedpoint . Zero
sellPrice := fixedpoint . Zero
2022-07-22 04:32:37 +00:00
Volume := fixedpoint . Zero
2022-07-15 10:59:37 +00:00
profit := types . Float64Slice { }
cumProfit := types . Float64Slice { 1. }
orderTagHistory := make ( map [ uint64 ] string )
2022-07-22 04:32:37 +00:00
s . Session . UserDataStream . OnOrderUpdate ( func ( order types . Order ) {
orderTagHistory [ order . OrderID ] = order . Tag
} )
modify := func ( p fixedpoint . Value ) fixedpoint . Value {
return p
}
if s . GraphPNLDeductFee {
modify = func ( p fixedpoint . Value ) fixedpoint . Value {
2022-07-28 10:34:12 +00:00
return p . Mul ( fixedpoint . One . Sub ( Fee ) )
2022-07-15 10:59:37 +00:00
}
2022-07-22 04:32:37 +00:00
}
s . Session . UserDataStream . OnTradeUpdate ( func ( trade types . Trade ) {
2022-08-02 11:21:44 +00:00
s . p . AddTrade ( trade )
2022-07-22 04:32:37 +00:00
tag , ok := orderTagHistory [ trade . OrderID ]
if ! ok {
panic ( fmt . Sprintf ( "cannot find order: %v" , trade ) )
2022-07-15 10:59:37 +00:00
}
2022-08-08 05:55:01 +00:00
bp := buyPrice
vol := Volume
sp := sellPrice
2022-07-22 04:32:37 +00:00
if tag == "close" {
if ! buyPrice . IsZero ( ) {
2022-08-08 05:55:01 +00:00
if trade . Side == types . SideTypeSell {
if trade . Quantity . Compare ( Volume ) > 0 {
profit . Update ( modify ( trade . Price . Div ( buyPrice ) ) . Float64 ( ) )
} else {
profit . Update ( modify ( trade . Price . Div ( buyPrice ) ) .
Sub ( fixedpoint . One ) .
Mul ( trade . Quantity ) .
Div ( Volume ) .
Add ( fixedpoint . One ) .
Float64 ( ) )
}
cumProfit . Update ( cumProfit . Last ( ) * profit . Last ( ) )
Volume = Volume . Sub ( trade . Quantity )
if Volume . Sign ( ) < 0 {
sellPrice = trade . Price
buyPrice = fixedpoint . Zero
} else if Volume . Sign ( ) == 0 {
buyPrice = fixedpoint . Zero
}
} else {
buyPrice = buyPrice . Mul ( Volume ) . Add ( trade . Price . Mul ( trade . Quantity ) ) . Div ( Volume . Add ( trade . Quantity ) )
Volume = Volume . Add ( trade . Quantity )
2022-07-22 04:32:37 +00:00
}
} else if ! sellPrice . IsZero ( ) {
2022-08-08 05:55:01 +00:00
if trade . Side == types . SideTypeBuy {
if trade . Quantity . Compare ( Volume . Neg ( ) ) > 0 {
profit . Update ( modify ( sellPrice . Div ( trade . Price ) ) . Float64 ( ) )
} else {
profit . Update ( modify ( sellPrice . Div ( trade . Price ) ) .
Sub ( fixedpoint . One ) .
Mul ( trade . Quantity ) .
Div ( Volume ) .
Neg ( ) .
Add ( fixedpoint . One ) .
Float64 ( ) )
}
cumProfit . Update ( cumProfit . Last ( ) * profit . Last ( ) )
Volume = Volume . Add ( trade . Quantity )
if Volume . Sign ( ) > 0 {
buyPrice = trade . Price
sellPrice = fixedpoint . Zero
} else if Volume . Sign ( ) == 0 {
sellPrice = fixedpoint . Zero
}
} else {
sellPrice = sellPrice . Mul ( Volume ) . Sub ( trade . Price . Mul ( trade . Quantity ) ) . Div ( Volume . Sub ( trade . Quantity ) )
Volume = Volume . Sub ( trade . Quantity )
2022-07-15 10:59:37 +00:00
}
2022-07-22 04:32:37 +00:00
} else {
2022-08-02 11:21:44 +00:00
// position changed by strategy
if trade . Side == types . SideTypeBuy {
Volume = Volume . Add ( trade . Quantity )
2022-08-08 05:55:01 +00:00
if Volume . Sign ( ) > 0 {
buyPrice = trade . Price
sellPrice = fixedpoint . Zero
} else if Volume . Sign ( ) < 0 {
sellPrice = trade . Price
buyPrice = fixedpoint . Zero
} else {
buyPrice = fixedpoint . Zero
sellPrice = fixedpoint . Zero
}
2022-08-02 11:21:44 +00:00
} else if trade . Side == types . SideTypeSell {
sellPrice = trade . Price
Volume = Volume . Sub ( trade . Quantity )
2022-08-08 05:55:01 +00:00
if Volume . Sign ( ) > 0 {
buyPrice = trade . Price
sellPrice = fixedpoint . Zero
} else if Volume . Sign ( ) < 0 {
sellPrice = trade . Price
buyPrice = fixedpoint . Zero
} else {
buyPrice = fixedpoint . Zero
sellPrice = fixedpoint . Zero
}
2022-08-02 11:21:44 +00:00
}
2022-07-22 04:32:37 +00:00
}
} else if tag == "short" {
if buyPrice . IsZero ( ) {
if ! sellPrice . IsZero ( ) {
sellPrice = sellPrice . Mul ( Volume ) . Sub ( trade . Price . Mul ( trade . Quantity ) ) . Div ( Volume . Sub ( trade . Quantity ) )
2022-07-15 10:59:37 +00:00
} else {
2022-07-22 04:32:37 +00:00
sellPrice = trade . Price
2022-07-15 10:59:37 +00:00
}
2022-07-22 04:32:37 +00:00
} else {
profit . Update ( modify ( trade . Price . Div ( buyPrice ) ) . Float64 ( ) )
cumProfit . Update ( cumProfit . Last ( ) * profit . Last ( ) )
buyPrice = fixedpoint . Zero
Volume = fixedpoint . Zero
sellPrice = trade . Price
}
Volume = Volume . Sub ( trade . Quantity )
} else if tag == "long" {
if sellPrice . IsZero ( ) {
2022-07-15 10:59:37 +00:00
if ! buyPrice . IsZero ( ) {
2022-07-22 04:32:37 +00:00
buyPrice = buyPrice . Mul ( Volume ) . Add ( trade . Price . Mul ( trade . Quantity ) ) . Div ( Volume . Add ( trade . Quantity ) )
2022-07-15 10:59:37 +00:00
} else {
2022-07-22 04:32:37 +00:00
buyPrice = trade . Price
2022-07-15 10:59:37 +00:00
}
2022-07-22 04:32:37 +00:00
} else {
profit . Update ( modify ( sellPrice . Div ( trade . Price ) ) . Float64 ( ) )
cumProfit . Update ( cumProfit . Last ( ) * profit . Last ( ) )
sellPrice = fixedpoint . Zero
buyPrice = trade . Price
Volume = fixedpoint . Zero
2022-07-15 10:59:37 +00:00
}
2022-07-22 04:32:37 +00:00
Volume = Volume . Add ( trade . Quantity )
2022-08-02 11:21:44 +00:00
} else if tag == "rebalance" {
if sellPrice . IsZero ( ) {
profit . Update ( modify ( sellPrice . Div ( trade . Price ) ) . Float64 ( ) )
} else {
profit . Update ( modify ( trade . Price . Div ( buyPrice ) ) . Float64 ( ) )
}
cumProfit . Update ( cumProfit . Last ( ) * profit . Last ( ) )
sellPrice = fixedpoint . Zero
buyPrice = fixedpoint . Zero
Volume = fixedpoint . Zero
s . p . Lock ( )
defer s . p . Unlock ( )
s . p . Reset ( )
2022-07-22 04:32:37 +00:00
}
s . buyPrice = buyPrice . Float64 ( )
2022-07-25 10:36:22 +00:00
s . highestPrice = s . buyPrice
2022-07-22 04:32:37 +00:00
s . sellPrice = sellPrice . Float64 ( )
2022-07-25 10:36:22 +00:00
s . lowestPrice = s . sellPrice
2022-08-08 05:55:01 +00:00
bbgo . Notify ( "tag %s %v %s volafter: %v, quantity: %v, bp: %v, sp: %v, volbefore: %v, bpafter: %v, spafter: %v" , tag , trade . Price , trade . Side , Volume , trade . Quantity , bp , sp , vol , s . buyPrice , s . sellPrice )
2022-07-22 04:32:37 +00:00
} )
2022-07-15 03:30:04 +00:00
2022-08-03 06:10:56 +00:00
dynamicKLine := & types . KLine { }
priceLine := types . NewQueue ( 300 )
if err := s . initIndicators ( dynamicKLine , priceLine ) ; err != nil {
2022-07-27 03:17:33 +00:00
log . WithError ( err ) . Errorf ( "initIndicator failed" )
2022-07-15 03:30:04 +00:00
return nil
}
2022-07-27 03:17:33 +00:00
s . initTickerFunctions ( ctx )
2022-07-15 03:30:04 +00:00
2022-07-25 10:36:22 +00:00
zeroPoints := types . NewQueue ( 300 )
2022-07-15 03:30:04 +00:00
stoploss := s . StopLoss . Float64 ( )
2022-08-02 11:21:44 +00:00
// default value: use 1m kline
if ! s . NoTrailingStopLoss && s . IsBackTesting ( ) || s . TrailingStopLossType == "" {
s . TrailingStopLossType = "kline"
}
2022-07-12 10:14:57 +00:00
2022-08-03 08:34:51 +00:00
bbgo . RegisterCommand ( "/draw" , "Draw Indicators" , func ( reply interact . Reply ) {
2022-08-03 06:10:56 +00:00
canvas := s . DrawIndicators ( dynamicKLine . StartTime , priceLine , zeroPoints )
var buffer bytes . Buffer
if err := canvas . Render ( chart . PNG , & buffer ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot render indicators in drift" )
2022-08-03 08:34:51 +00:00
reply . Message ( fmt . Sprintf ( "[error] cannot render indicators in drift: %v" , err ) )
2022-08-03 06:10:56 +00:00
return
}
bbgo . SendPhoto ( & buffer )
} )
2022-08-03 08:34:51 +00:00
bbgo . RegisterCommand ( "/pnl" , "Draw PNL per trade" , func ( reply interact . Reply ) {
2022-08-03 06:10:56 +00:00
canvas := s . DrawPNL ( & profit )
var buffer bytes . Buffer
if err := canvas . Render ( chart . PNG , & buffer ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot render pnl in drift" )
2022-08-03 08:34:51 +00:00
reply . Message ( fmt . Sprintf ( "[error] cannot render pnl in drift: %v" , err ) )
2022-08-03 06:10:56 +00:00
return
}
bbgo . SendPhoto ( & buffer )
} )
2022-08-03 08:34:51 +00:00
bbgo . RegisterCommand ( "/cumpnl" , "Draw Cummulative PNL" , func ( reply interact . Reply ) {
2022-08-03 06:10:56 +00:00
canvas := s . DrawCumPNL ( & cumProfit )
var buffer bytes . Buffer
if err := canvas . Render ( chart . PNG , & buffer ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot render cumpnl in drift" )
2022-08-03 08:34:51 +00:00
reply . Message ( fmt . Sprintf ( "[error] canot render cumpnl in drift: %v" , err ) )
2022-08-03 06:10:56 +00:00
return
}
bbgo . SendPhoto ( & buffer )
} )
2022-08-03 11:41:16 +00:00
bbgo . RegisterCommand ( "/config" , "Show latest config" , func ( reply interact . Reply ) {
var buffer bytes . Buffer
2022-08-08 05:55:01 +00:00
s . Print ( & buffer , false )
2022-08-03 11:41:16 +00:00
reply . Message ( buffer . String ( ) )
} )
2022-07-01 10:38:25 +00:00
session . MarketDataStream . OnKLineClosed ( func ( kline types . KLine ) {
if s . Status != types . StrategyStatusRunning {
return
}
2022-07-12 10:14:57 +00:00
if kline . Symbol != s . Symbol {
return
}
var driftPred , atr float64
var drift [ ] float64
if ! kline . Closed {
return
}
if kline . Interval == types . Interval1m {
2022-08-02 11:21:44 +00:00
s . drift1m . Update ( s . getSource ( & kline ) . Float64 ( ) )
2022-07-29 06:55:23 +00:00
s . minutesCounter += 1
2022-08-02 11:21:44 +00:00
if s . NoTrailingStopLoss || s . TrailingStopLossType == "realtime" {
2022-07-15 03:30:04 +00:00
return
}
// for doing the trailing stoploss during backtesting
atr = s . atr . Last ( )
price := s . getLastPrice ( )
pricef := price . Float64 ( )
2022-07-29 06:55:23 +00:00
takeProfitFactor := s . takeProfitFactor . Predict ( 2 )
var err error
numPending := 0
if numPending , err = s . smartCancel ( ctx , pricef , atr , takeProfitFactor ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot cancel orders" )
return
}
if numPending > 0 {
return
}
2022-07-15 03:30:04 +00:00
lowf := math . Min ( kline . Low . Float64 ( ) , pricef )
highf := math . Max ( kline . High . Float64 ( ) , pricef )
2022-07-25 10:36:22 +00:00
if s . lowestPrice > 0 && lowf < s . lowestPrice {
s . lowestPrice = lowf
}
if s . highestPrice > 0 && highf > s . highestPrice {
s . highestPrice = highf
}
2022-07-22 04:32:37 +00:00
avg := s . buyPrice + s . sellPrice
2022-08-08 05:55:01 +00:00
exitShortCondition := ( /*avg*(1.+stoploss) <= pricef || (drift > 0 || ddrift > DDriftFilterPos) ||*/ avg - atr * takeProfitFactor >= pricef ||
2022-08-02 11:21:44 +00:00
s . trailingCheck ( highf , "short" ) ) &&
2022-07-20 10:49:56 +00:00
( s . Position . IsShort ( ) && ! s . Position . IsDust ( price ) )
2022-08-08 05:55:01 +00:00
exitLongCondition := ( /*avg*(1.-stoploss) >= pricef || (drift < 0 || ddrift < DDriftFilterNeg) ||*/ avg + atr * takeProfitFactor <= pricef ||
2022-08-02 11:21:44 +00:00
s . trailingCheck ( lowf , "long" ) ) &&
2022-07-20 10:49:56 +00:00
( s . Position . IsLong ( ) && ! s . Position . IsDust ( price ) )
2022-07-15 03:30:04 +00:00
if exitShortCondition || exitLongCondition {
2022-07-28 10:34:12 +00:00
if exitLongCondition && s . highestPrice > avg {
2022-08-08 05:55:01 +00:00
s . takeProfitFactor . Update ( ( s . highestPrice - avg ) / atr * 1.5 )
2022-07-28 10:34:12 +00:00
} else if exitShortCondition && avg > s . lowestPrice {
2022-08-08 05:55:01 +00:00
s . takeProfitFactor . Update ( ( avg - s . lowestPrice ) / atr * 1.5 )
2022-07-15 03:30:04 +00:00
}
2022-07-20 10:49:56 +00:00
_ = s . ClosePosition ( ctx , fixedpoint . One )
2022-07-15 03:30:04 +00:00
}
2022-07-01 10:38:25 +00:00
return
}
2022-08-08 05:55:01 +00:00
if kline . Interval != s . Interval {
return
}
2022-07-27 03:17:33 +00:00
dynamicKLine . Set ( & kline )
2022-07-12 10:14:57 +00:00
2022-07-15 03:30:04 +00:00
source := s . getSource ( dynamicKLine )
2022-07-12 10:14:57 +00:00
sourcef := source . Float64 ( )
priceLine . Update ( sourcef )
2022-07-19 09:38:42 +00:00
s . ma . Update ( sourcef )
2022-08-02 11:21:44 +00:00
s . trendLine . Update ( sourcef )
2022-07-12 10:14:57 +00:00
s . drift . Update ( sourcef )
2022-08-02 11:21:44 +00:00
2022-07-25 10:36:22 +00:00
zeroPoint := s . drift . ZeroPoint ( )
zeroPoints . Update ( zeroPoint )
2022-07-14 05:50:37 +00:00
s . atr . PushK ( kline )
2022-07-12 10:14:57 +00:00
drift = s . drift . Array ( 2 )
2022-07-25 10:36:22 +00:00
ddrift := s . drift . drift . Array ( 2 )
2022-07-14 03:46:19 +00:00
driftPred = s . drift . Predict ( s . PredictOffset )
2022-07-28 10:34:12 +00:00
ddriftPred := s . drift . drift . Predict ( s . PredictOffset )
2022-07-12 10:14:57 +00:00
atr = s . atr . Last ( )
2022-07-14 03:46:19 +00:00
price := s . getLastPrice ( )
2022-07-13 08:08:57 +00:00
pricef := price . Float64 ( )
2022-07-15 03:30:04 +00:00
lowf := math . Min ( kline . Low . Float64 ( ) , pricef )
highf := math . Max ( kline . High . Float64 ( ) , pricef )
2022-07-19 09:38:42 +00:00
lowdiff := s . ma . Last ( ) - lowf
s . stdevLow . Update ( lowdiff )
highdiff := highf - s . ma . Last ( )
s . stdevHigh . Update ( highdiff )
2022-08-08 05:55:01 +00:00
//log.Errorf("highdiff: %3.2f ma: %.2f, close: %8v, high: %8v, low: %8v, time: %v", s.stdevHigh.Last(), s.ma.Last(), kline.Close, kline.High, kline.Low, kline.StartTime)
2022-07-28 10:34:12 +00:00
if s . lowestPrice > 0 && lowf < s . lowestPrice {
s . lowestPrice = lowf
}
if s . highestPrice > 0 && highf > s . highestPrice {
s . highestPrice = highf
}
2022-07-22 04:32:37 +00:00
avg := s . buyPrice + s . sellPrice
2022-07-28 10:34:12 +00:00
takeProfitFactor := s . takeProfitFactor . Predict ( 2 )
2022-07-12 10:14:57 +00:00
2022-08-02 11:21:44 +00:00
if ! s . NoRebalance {
s . Rebalance ( ctx , orderTagHistory )
}
2022-08-08 05:55:01 +00:00
//if !s.IsBackTesting() {
balances := s . GeneralOrderExecutor . Session ( ) . GetAccount ( ) . Balances ( )
bbgo . Notify ( "source: %.4f, price: %.4f, driftPred: %.4f, ddriftPred: %.4f, drift[1]: %.4f, ddrift[1]: %.4f, atr: %.4f, avg: %.4f, takeProfitFact: %.4f, lowf %.4f, highf: %.4f" ,
sourcef , pricef , driftPred , ddriftPred , drift [ 1 ] , ddrift [ 1 ] , atr , avg , takeProfitFactor , lowf , highf )
// Notify will parse args to strings and process separately
bbgo . Notify ( "balances: [Base] %s(%vU) [Quote] %s" ,
balances [ s . Market . BaseCurrency ] . String ( ) ,
balances [ s . Market . BaseCurrency ] . Total ( ) . Mul ( price ) ,
balances [ s . Market . QuoteCurrency ] . String ( ) )
//}
shortCondition := ( drift [ 1 ] >= DriftFilterNeg || ddrift [ 1 ] >= 0 ) && ( driftPred <= DDriftFilterNeg || ddriftPred <= 0 ) || drift [ 1 ] < 0 && drift [ 0 ] < 0
longCondition := ( drift [ 1 ] <= DriftFilterPos || ddrift [ 1 ] <= 0 ) && ( driftPred >= DDriftFilterPos || ddriftPred >= 0 ) || drift [ 1 ] > 0 && drift [ 0 ] > 0
exitShortCondition := ( ( drift [ 0 ] >= DDriftFilterPos || ddrift [ 0 ] >= 0 ) ||
2022-07-28 10:34:12 +00:00
avg * ( 1. + stoploss ) <= pricef ||
avg - atr * takeProfitFactor >= pricef ) &&
2022-08-02 11:21:44 +00:00
s . Position . IsShort ( )
2022-08-08 05:55:01 +00:00
exitLongCondition := ( ( drift [ 0 ] <= DDriftFilterNeg || ddrift [ 0 ] <= 0 ) ||
2022-07-28 10:34:12 +00:00
avg * ( 1. - stoploss ) >= pricef ||
avg + atr * takeProfitFactor <= pricef ) &&
2022-08-02 11:21:44 +00:00
s . Position . IsLong ( )
2022-07-28 10:34:12 +00:00
2022-08-02 11:21:44 +00:00
if ( exitShortCondition || exitLongCondition ) && s . Position . IsOpened ( price ) && ! shortCondition && ! longCondition {
2022-07-14 03:46:19 +00:00
if err := s . GeneralOrderExecutor . GracefulCancel ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot cancel orders" )
return
2022-07-13 08:08:57 +00:00
}
2022-07-28 10:34:12 +00:00
if exitShortCondition && avg > s . lowestPrice {
2022-08-08 05:55:01 +00:00
s . takeProfitFactor . Update ( ( avg - s . lowestPrice ) / atr * 1.5 )
2022-07-28 10:34:12 +00:00
} else if exitLongCondition && avg < s . highestPrice {
2022-08-08 05:55:01 +00:00
s . takeProfitFactor . Update ( ( s . highestPrice - avg ) / atr * 1.5 )
2022-07-28 10:34:12 +00:00
}
if s . takeProfitFactor . Last ( ) == 0 {
log . Errorf ( "exit %f %f %f %v" , s . highestPrice , s . lowestPrice , avg , s . takeProfitFactor . Array ( 10 ) )
}
2022-07-20 10:49:56 +00:00
_ = s . ClosePosition ( ctx , fixedpoint . One )
2022-07-25 10:36:22 +00:00
return
2022-07-13 08:08:57 +00:00
}
2022-07-12 10:14:57 +00:00
if shortCondition {
2022-07-14 03:46:19 +00:00
if err := s . GeneralOrderExecutor . GracefulCancel ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot cancel orders" )
return
2022-07-01 10:38:25 +00:00
}
2022-07-04 12:13:54 +00:00
baseBalance , ok := s . Session . GetAccount ( ) . Balance ( s . Market . BaseCurrency )
if ! ok {
log . Errorf ( "unable to get baseBalance" )
return
}
2022-07-19 09:38:42 +00:00
source = source . Add ( fixedpoint . NewFromFloat ( s . stdevHigh . Last ( ) * s . HighLowVarianceMultiplier ) )
2022-07-12 10:14:57 +00:00
if source . Compare ( price ) < 0 {
source = price
2022-07-04 12:13:54 +00:00
}
2022-07-22 04:32:37 +00:00
sourcef = source . Float64 ( )
2022-07-04 12:13:54 +00:00
2022-07-12 10:14:57 +00:00
if s . Market . IsDustQuantity ( baseBalance . Available , source ) {
2022-07-05 11:43:05 +00:00
return
}
2022-07-28 10:34:12 +00:00
if avg < s . highestPrice && avg > 0 && s . Position . IsLong ( ) {
2022-08-08 05:55:01 +00:00
s . takeProfitFactor . Update ( ( s . highestPrice - avg ) / atr * 1.5 )
2022-07-28 10:34:12 +00:00
}
2022-07-15 03:30:04 +00:00
// Cleanup pending StopOrders
2022-07-14 10:33:47 +00:00
quantity := baseBalance . Available
createdOrders , err := s . GeneralOrderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : s . Symbol ,
Side : types . SideTypeSell ,
Type : types . OrderTypeLimit ,
Price : source ,
Quantity : quantity ,
2022-07-15 10:59:37 +00:00
Tag : "short" ,
2022-07-04 12:13:54 +00:00
} )
if err != nil {
log . WithError ( err ) . Errorf ( "cannot place sell order" )
return
}
2022-07-22 04:32:37 +00:00
orderTagHistory [ createdOrders [ 0 ] . OrderID ] = "short"
2022-07-29 06:55:23 +00:00
s . orderPendingCounter [ createdOrders [ 0 ] . OrderID ] = s . minutesCounter
2022-07-04 12:13:54 +00:00
}
2022-07-12 10:14:57 +00:00
if longCondition {
2022-07-14 03:46:19 +00:00
if err := s . GeneralOrderExecutor . GracefulCancel ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot cancel orders" )
return
2022-07-12 10:14:57 +00:00
}
2022-07-19 09:38:42 +00:00
source = source . Sub ( fixedpoint . NewFromFloat ( s . stdevLow . Last ( ) * s . HighLowVarianceMultiplier ) )
2022-07-12 10:14:57 +00:00
if source . Compare ( price ) > 0 {
source = price
2022-07-04 12:13:54 +00:00
}
2022-07-22 04:32:37 +00:00
sourcef = source . Float64 ( )
2022-07-25 10:36:22 +00:00
2022-07-04 12:13:54 +00:00
quoteBalance , ok := s . Session . GetAccount ( ) . Balance ( s . Market . QuoteCurrency )
if ! ok {
log . Errorf ( "unable to get quoteCurrency" )
return
}
if s . Market . IsDustQuantity (
2022-07-12 10:14:57 +00:00
quoteBalance . Available . Div ( source ) , source ) {
2022-07-04 12:13:54 +00:00
return
}
2022-07-28 10:34:12 +00:00
if avg > s . lowestPrice && s . Position . IsShort ( ) {
2022-08-08 05:55:01 +00:00
s . takeProfitFactor . Update ( ( avg - s . lowestPrice ) / atr * 1.5 )
2022-07-28 10:34:12 +00:00
}
2022-07-14 10:33:47 +00:00
quantity := quoteBalance . Available . Div ( source )
createdOrders , err := s . GeneralOrderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : s . Symbol ,
Side : types . SideTypeBuy ,
Type : types . OrderTypeLimit ,
Price : source ,
Quantity : quantity ,
2022-07-15 10:59:37 +00:00
Tag : "long" ,
2022-07-04 12:13:54 +00:00
} )
if err != nil {
log . WithError ( err ) . Errorf ( "cannot place buy order" )
return
2022-07-01 10:38:25 +00:00
}
2022-07-22 04:32:37 +00:00
orderTagHistory [ createdOrders [ 0 ] . OrderID ] = "long"
2022-07-29 06:55:23 +00:00
s . orderPendingCounter [ createdOrders [ 0 ] . OrderID ] = s . minutesCounter
2022-07-01 10:38:25 +00:00
}
} )
2022-07-04 12:13:54 +00:00
bbgo . OnShutdown ( func ( ctx context . Context , wg * sync . WaitGroup ) {
2022-07-15 08:00:38 +00:00
2022-08-08 05:55:01 +00:00
var buffer bytes . Buffer
s . Print ( & buffer , true , true )
fmt . Fprintln ( & buffer , s . TradeStats . BriefString ( ) )
2022-07-15 08:00:38 +00:00
2022-08-08 05:55:01 +00:00
os . Stdout . Write ( buffer . Bytes ( ) )
2022-07-15 08:00:38 +00:00
2022-07-15 10:59:37 +00:00
if s . GenerateGraph {
2022-07-25 10:36:22 +00:00
s . Draw ( dynamicKLine . StartTime , priceLine , & profit , & cumProfit , zeroPoints )
2022-07-15 10:59:37 +00:00
}
2022-07-15 08:00:38 +00:00
2022-07-01 10:38:25 +00:00
wg . Done ( )
} )
return nil
}