2022-07-01 10:38:25 +00:00
package drift
import (
2022-08-09 08:25:36 +00:00
"bufio"
2022-07-01 10:38:25 +00:00
"context"
2022-08-09 08:25:36 +00:00
"encoding/json"
2022-07-15 03:30:04 +00:00
"errors"
2022-07-01 10:38:25 +00:00
"fmt"
2022-07-14 03:46:19 +00:00
"math"
2022-07-01 10:38:25 +00:00
"os"
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"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)
const ID = "drift"
2022-07-29 06:55:23 +00:00
const DDriftFilterNeg = - 0.7
const DDriftFilterPos = 0.7
const DriftFilterNeg = - 1.8
const DriftFilterPos = 1.8
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-02 11:21:44 +00:00
drift1m * indicator . Drift
2022-07-29 06:55:23 +00:00
atr * indicator . ATR
midPrice fixedpoint . Value
lock sync . RWMutex
minutesCounter int
orderPendingCounter map [ uint64 ] int
2022-08-02 11:21:44 +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" `
TrailingActivationRatio [ ] float64 ` josn:"trailingActivationRatio" `
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-09 08:25:36 +00:00
func ( s * Strategy ) Print ( o * os . File ) {
f := bufio . NewWriter ( o )
defer f . Flush ( )
b , _ := json . MarshalIndent ( s . ExitMethods , " " , " " )
hiyellow := color . New ( color . FgHiYellow ) . FprintfFunc ( )
2022-07-15 08:00:38 +00:00
hiyellow ( f , "------ %s Settings ------\n" , s . InstanceID ( ) )
2022-08-09 08:25:36 +00:00
hiyellow ( f , "generateGraph: %v\n" , s . GenerateGraph )
hiyellow ( f , "canvasPath: %s\n" , s . CanvasPath )
hiyellow ( f , "graphPNLPath: %s\n" , s . GraphPNLPath )
hiyellow ( f , "graphCumPNLPath: %s\n" , s . GraphCumPNLPath )
hiyellow ( f , "source: %s\n" , s . Source )
hiyellow ( f , "stoploss: %v\n" , s . StopLoss )
hiyellow ( f , "takeProfitFactor(last): %f, (init): %f\n" , s . takeProfitFactor . Last ( ) , s . TakeProfitFactor )
hiyellow ( f , "profitFactorWindow: %d\n" , s . ProfitFactorWindow )
hiyellow ( f , "predictOffset: %d\n" , s . PredictOffset )
hiyellow ( f , "exits:\n %s\n" , string ( b ) )
hiyellow ( f , "symbol: %s\n" , s . Symbol )
hiyellow ( f , "interval: %s\n" , s . Interval )
hiyellow ( f , "window: %d\n" , s . Window )
hiyellow ( f , "noTrailingStopLoss: %v\n" , s . NoTrailingStopLoss )
2022-08-02 11:21:44 +00:00
hiyellow ( f , "trailingStopLossType: %s\n" , s . TrailingStopLossType )
2022-08-09 08:25:36 +00:00
hiyellow ( f , "hlVarianceMutiplier: %f\n" , s . HighLowVarianceMultiplier )
hiyellow ( f , "hlRangeWindow: %d\n" , s . HLRangeWindow )
hiyellow ( f , "smootherWindow: %d\n" , s . SmootherWindow )
hiyellow ( f , "fisherTransformWindow: %d\n" , s . FisherTransformWindow )
hiyellow ( f , "atrWindow: %d\n" , s . ATRWindow )
2022-07-29 06:55:23 +00:00
hiyellow ( f , "pendingMinutes: %d\n" , s . PendingMinutes )
2022-08-02 11:21:44 +00:00
hiyellow ( f , "noRebalance: %v\n" , s . NoRebalance )
hiyellow ( f , "\ttrendWindow: %d\n" , s . TrendWindow )
hiyellow ( f , "\trebalanceFilter: %f\n" , s . RebalanceFilter )
hiyellow ( f , "trailingActivationRatio: %v\n" , s . TrailingActivationRatio )
hiyellow ( f , "trailingCallbackRate: %v\n" , s . TrailingCallbackRate )
2022-08-09 08:25:36 +00:00
hiyellow ( f , "\n" )
2022-07-01 10:38:25 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) InstanceID ( ) string {
2022-08-02 11:21:44 +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-09 08:25:36 +00:00
func ( s * Strategy ) initIndicators ( ) 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-02 11:21:44 +00:00
s . drift1m = & indicator . Drift {
MA : & indicator . SMA { IntervalWindow : types . IntervalWindow { Interval : types . Interval1m , Window : 2 } } ,
IntervalWindow : types . IntervalWindow { Interval : types . Interval1m , Window : 2 } ,
}
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-08-09 08:25:36 +00:00
for i := 0 ; i < s . ProfitFactorWindow ; i ++ {
2022-07-28 10:34:12 +00:00
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-02 11:21:44 +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
for _ , order := range nonTraded {
if s . minutesCounter - s . orderPendingCounter [ order . OrderID ] > s . PendingMinutes {
toCancel = true
} else if order . Side == types . SideTypeBuy {
if order . Price . Float64 ( ) + atr * takeProfitFactor <= pricef {
toCancel = true
}
} else if order . Side == types . SideTypeSell {
if order . Price . Float64 ( ) - atr * takeProfitFactor >= pricef {
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-08-09 08:25:36 +00:00
avg = s . buyPrice + s . sellPrice
2022-07-29 06:55:23 +00:00
exitShortCondition := ( /*avg+atr/2 <= pricef || 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-07-29 06:55:23 +00:00
exitLongCondition := ( /*avg-atr/2 >= pricef || 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-09 08:25:36 +00:00
s . takeProfitFactor . Update ( ( s . highestPrice - avg ) / atr * 4 )
2022-07-28 10:34:12 +00:00
} else if exitShortCondition && avg > s . lowestPrice {
2022-08-09 08:25:36 +00:00
s . takeProfitFactor . Update ( ( avg - s . lowestPrice ) / atr * 4 )
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-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" )
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-09 08:25:36 +00:00
func ( s * Strategy ) Draw ( time types . Time , priceLine types . SeriesExtend , profit types . Series , cumProfit types . Series , zeroPoints types . Series ) {
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
}
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-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-09 08:25:36 +00:00
canvas = types . NewCanvas ( s . InstanceID ( ) )
if s . GraphPNLDeductFee {
canvas . PlotRaw ( "pnl % (with Fee Deducted)" , profit , profit . Length ( ) )
} else {
canvas . PlotRaw ( "pnl %" , profit , profit . Length ( ) )
}
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-09 08:25:36 +00:00
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-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-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-07-22 04:32:37 +00:00
if tag == "close" {
if ! buyPrice . IsZero ( ) {
2022-08-09 08:25:36 +00:00
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 . IsZero ( ) {
buyPrice = fixedpoint . Zero
}
if ! sellPrice . IsZero ( ) {
panic ( "sellprice shouldn't be zero" )
2022-07-22 04:32:37 +00:00
}
} else if ! sellPrice . IsZero ( ) {
2022-08-09 08:25:36 +00:00
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 . IsZero ( ) {
sellPrice = fixedpoint . Zero
2022-07-15 10:59:37 +00:00
}
2022-08-09 08:25:36 +00:00
if ! buyPrice . IsZero ( ) {
panic ( "buyprice shouldn't be zero" )
2022-08-02 11:21:44 +00:00
}
2022-08-09 08:25:36 +00:00
} else {
2022-08-02 11:21:44 +00:00
// position changed by strategy
if trade . Side == types . SideTypeBuy {
buyPrice = trade . Price
Volume = Volume . Add ( trade . Quantity )
} else if trade . Side == types . SideTypeSell {
sellPrice = trade . Price
Volume = Volume . Sub ( trade . Quantity )
}
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-08-09 08:25:36 +00:00
s . highestPrice = s . buyPrice
2022-07-22 04:32:37 +00:00
s . sellPrice = sellPrice . Float64 ( )
2022-08-09 08:25:36 +00:00
s . lowestPrice = s . sellPrice
2022-07-22 04:32:37 +00:00
} )
2022-07-15 03:30:04 +00:00
2022-08-09 08:25:36 +00:00
if err := s . initIndicators ( ) ; 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-08-09 08:25:36 +00:00
dynamicKLine := & types . KLine { }
priceLine := types . NewQueue ( 300 )
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-08-03 11:41:16 +00:00
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-02 11:21:44 +00:00
stoploss = s . StopLoss . Float64 ( )
2022-08-09 08:25:36 +00:00
2022-08-02 11:21:44 +00:00
exitShortCondition := ( /*avg+atr/2 <= highf || avg*(1.+stoploss) <= pricef || (drift > 0 || ddrift > DDriftFilterPos) ||*/ avg - atr * takeProfitFactor >= pricef ||
s . trailingCheck ( highf , "short" ) ) &&
2022-08-09 08:25:36 +00:00
( s . Position . IsShort ( ) && ! s . Position . IsDust ( price ) )
2022-08-02 11:21:44 +00:00
exitLongCondition := ( /*avg-atr/2 >= lowf || avg*(1.-stoploss) >= pricef || (drift < 0 || ddrift < DDriftFilterNeg) ||*/ avg + atr * takeProfitFactor <= pricef ||
s . trailingCheck ( lowf , "long" ) ) &&
2022-08-09 08:25:36 +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-09 08:25:36 +00:00
s . takeProfitFactor . Update ( ( s . highestPrice - avg ) / atr * 4 )
2022-07-28 10:34:12 +00:00
} else if exitShortCondition && avg > s . lowestPrice {
2022-08-09 08:25:36 +00:00
s . takeProfitFactor . Update ( ( avg - s . lowestPrice ) / atr * 4 )
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-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-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-09 08:25:36 +00:00
if ! s . IsBackTesting ( ) {
2022-08-02 11:21:44 +00:00
balances := s . GeneralOrderExecutor . Session ( ) . GetAccount ( ) . Balances ( )
2022-08-09 08:25:36 +00:00
bbgo . Notify ( "zeroPoint: %.4f, source: %.4f, price: %.4f, driftPred: %.4f, drift: %.4f, drift[1]: %.4f, atr: %.4f, avg: %.4f" ,
zeroPoint , sourcef , pricef , driftPred , drift [ 0 ] , drift [ 1 ] , atr , avg )
// Notify will parse args to strings and process separately
bbgo . Notify ( "balances: [Base] %s [Quote] %s" , balances [ s . Market . BaseCurrency ] . String ( ) , balances [ s . Market . QuoteCurrency ] . String ( ) )
}
2022-08-02 11:21:44 +00:00
drift1m := s . drift1m . Predict ( 3 )
2022-07-29 06:55:23 +00:00
shortCondition := ( drift [ 1 ] >= DriftFilterNeg || ddrift [ 1 ] >= 0 ) && ( driftPred <= DDriftFilterNeg || ddriftPred <= 0 )
longCondition := ( drift [ 1 ] <= DriftFilterPos || ddrift [ 1 ] <= 0 ) && ( driftPred >= DDriftFilterPos || ddriftPred >= 0 )
2022-08-02 11:21:44 +00:00
exitShortCondition := ( ( drift [ 0 ] >= DDriftFilterPos || ddrift [ 0 ] >= 0 ) && drift1m > 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 ( )
exitLongCondition := ( ( drift [ 0 ] <= DDriftFilterNeg || ddrift [ 0 ] <= 0 ) && drift1m < 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-09 08:25:36 +00:00
s . takeProfitFactor . Update ( ( avg - s . lowestPrice ) / atr * 4 )
2022-07-28 10:34:12 +00:00
} else if exitLongCondition && avg < s . highestPrice {
2022-08-09 08:25:36 +00:00
s . takeProfitFactor . Update ( ( s . highestPrice - avg ) / atr * 4 )
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-08-09 08:25:36 +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-08-09 08:25:36 +00:00
baseBalance , ok := s . Session . GetAccount ( ) . Balance ( s . Market . BaseCurrency )
if ! ok {
log . Errorf ( "unable to get baseBalance" )
return
}
source = source . Add ( fixedpoint . NewFromFloat ( s . stdevHigh . Last ( ) * s . HighLowVarianceMultiplier ) )
if source . Compare ( price ) < 0 {
2022-07-12 10:14:57 +00:00
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-08-09 08:25:36 +00:00
if s . Market . IsDustQuantity ( baseBalance . Available , source ) {
2022-08-09 04:26:02 +00:00
return
2022-07-28 10:34:12 +00:00
}
2022-08-09 08:25:36 +00:00
if avg < s . highestPrice && avg > 0 && s . Position . IsLong ( ) {
s . takeProfitFactor . Update ( ( s . highestPrice - avg ) / atr * 4 )
if s . takeProfitFactor . Last ( ) == 0 {
log . Errorf ( "short %f %f" , s . highestPrice , avg )
}
2022-08-09 04:26:02 +00:00
}
2022-08-09 08:25:36 +00:00
// Cleanup pending StopOrders
quantity := baseBalance . Available
2022-07-14 10:33:47 +00:00
createdOrders , err := s . GeneralOrderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : s . Symbol ,
2022-08-09 08:25:36 +00:00
Side : types . SideTypeSell ,
2022-07-14 10:33:47 +00:00
Type : types . OrderTypeLimit ,
Price : source ,
Quantity : quantity ,
2022-08-09 08:25:36 +00:00
Tag : "short" ,
2022-07-04 12:13:54 +00:00
} )
if err != nil {
2022-08-09 08:25:36 +00:00
log . WithError ( err ) . Errorf ( "cannot place sell order" )
2022-07-04 12:13:54 +00:00
return
}
2022-08-09 08:25:36 +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-08-09 08:25:36 +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-08-09 08:25:36 +00:00
source = source . Sub ( fixedpoint . NewFromFloat ( s . stdevLow . Last ( ) * s . HighLowVarianceMultiplier ) )
if source . Compare ( price ) > 0 {
2022-07-12 10:14:57 +00:00
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-08-09 08:25:36 +00:00
quoteBalance , ok := s . Session . GetAccount ( ) . Balance ( s . Market . QuoteCurrency )
if ! ok {
log . Errorf ( "unable to get quoteCurrency" )
2022-07-04 12:13:54 +00:00
return
}
2022-08-09 08:25:36 +00:00
if s . Market . IsDustQuantity (
quoteBalance . Available . Div ( source ) , source ) {
return
2022-07-28 10:34:12 +00:00
}
2022-08-09 08:25:36 +00:00
if avg > s . lowestPrice && s . Position . IsShort ( ) {
s . takeProfitFactor . Update ( ( avg - s . lowestPrice ) / atr * 4 )
if s . takeProfitFactor . Last ( ) == 0 {
log . Errorf ( "long %f %f" , s . lowestPrice , avg )
}
}
quantity := quoteBalance . Available . Div ( source )
2022-07-14 10:33:47 +00:00
createdOrders , err := s . GeneralOrderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : s . Symbol ,
2022-08-09 08:25:36 +00:00
Side : types . SideTypeBuy ,
2022-07-14 10:33:47 +00:00
Type : types . OrderTypeLimit ,
Price : source ,
Quantity : quantity ,
2022-08-09 08:25:36 +00:00
Tag : "long" ,
2022-07-04 12:13:54 +00:00
} )
if err != nil {
2022-08-09 08:25:36 +00:00
log . WithError ( err ) . Errorf ( "cannot place buy order" )
2022-07-04 12:13:54 +00:00
return
2022-07-01 10:38:25 +00:00
}
2022-08-09 08:25:36 +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-09 08:25:36 +00:00
defer s . Print ( os . Stdout )
2022-07-15 08:00:38 +00:00
2022-08-09 08:25:36 +00:00
defer fmt . Fprintln ( os . Stdout , s . TradeStats . BriefString ( ) )
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
}