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-07-14 03:46:19 +00:00
"math"
2022-07-01 10:38:25 +00:00
"os"
2022-08-10 11:36:30 +00:00
"strconv"
2022-07-01 10:38:25 +00:00
"sync"
2022-09-02 03:05:00 +00:00
"time"
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"
2022-08-25 09:31:42 +00:00
"github.com/c9s/bbgo/pkg/datatype/floats"
2022-07-01 10:38:25 +00:00
"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"
)
const ID = "drift"
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-08-10 11:36:30 +00:00
var Fee = 0.0008 // taker fee % * 2, for upper bound
2022-07-01 10:38:25 +00:00
func init ( ) {
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
type Strategy struct {
Symbol string ` json:"symbol" `
bbgo . StrategyController
types . Market
types . IntervalWindow
2022-09-06 05:43:05 +00:00
bbgo . SourceSelector
2022-07-01 10:38:25 +00:00
* 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
2022-09-02 03:05:00 +00:00
priceLines * types . Queue
2022-08-02 11:21:44 +00:00
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
2022-08-10 11:36:30 +00:00
lock sync . RWMutex ` ignore:"true" `
positionLock sync . RWMutex ` ignore:"true" `
2022-09-02 03:05:00 +00:00
startTime time . Time
2022-07-29 06:55:23 +00:00
minutesCounter int
orderPendingCounter map [ uint64 ] int
2022-08-10 11:36:30 +00:00
frameKLine * types . KLine
kline1m * types . KLine
2022-08-02 11:21:44 +00:00
2022-08-02 11:21:44 +00:00
beta float64
2022-09-13 11:40:07 +00:00
StopLoss fixedpoint . Value ` json:"stoploss" modifiable:"true" `
2022-07-19 09:38:42 +00:00
CanvasPath string ` json:"canvasPath" `
PredictOffset int ` json:"predictOffset" `
2022-09-13 11:40:07 +00:00
HighLowVarianceMultiplier float64 ` json:"hlVarianceMultiplier" modifiable:"true" `
NoTrailingStopLoss bool ` json:"noTrailingStopLoss" modifiable:"true" `
TrailingStopLossType string ` json:"trailingStopLossType" modifiable:"true" ` // 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" `
2022-08-15 11:50:34 +00:00
Window1m int ` json:"window1m" `
FisherTransformWindow1m int ` json:"fisherTransformWindow1m" `
2022-08-23 08:22:45 +00:00
SmootherWindow1m int ` json:"smootherWindow1m" `
2022-07-28 10:34:12 +00:00
SmootherWindow int ` json:"smootherWindow" `
FisherTransformWindow int ` json:"fisherTransformWindow" `
ATRWindow int ` json:"atrWindow" `
2022-09-13 11:40:07 +00:00
PendingMinutes int ` json:"pendingMinutes" modifiable:"true" ` // if order not be traded for pendingMinutes of time, cancel it.
NoRebalance bool ` json:"noRebalance" modifiable:"true" ` // 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" modifiable:"true" ` // beta filter on the Linear Regression of trendLine
TrailingCallbackRate [ ] float64 ` json:"trailingCallbackRate" modifiable:"true" `
TrailingActivationRatio [ ] float64 ` json:"trailingActivationRatio" modifiable:"true" `
DriftFilterNeg float64 ` json:"driftFilterNeg" modifiable:"true" `
DriftFilterPos float64 ` json:"driftFilterPos" modifiable:"true" `
DDriftFilterNeg float64 ` json:"ddriftFilterNeg" modifiable:"true" `
DDriftFilterPos float64 ` json:"ddriftFilterPos" modifiable:"true" `
2022-08-23 08:22:45 +00:00
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-01 10:38:25 +00:00
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) InstanceID ( ) string {
2022-08-16 03:50:30 +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 ) {
2022-09-06 05:26:17 +00:00
// by default, bbgo only pre-subscribe 1000 klines.
// this is not enough if we're subscribing 30m intervals using SerialMarketDataStore
2022-09-16 10:11:36 +00:00
maxWindow := ( s . Window + s . SmootherWindow + s . FisherTransformWindow ) * s . Interval . Minutes ( )
maxWindow1m := s . Window1m + s . SmootherWindow1m + s . FisherTransformWindow1m
if maxWindow < maxWindow1m {
maxWindow = maxWindow1m
}
bbgo . KLinePreloadLimit = int64 ( ( maxWindow / 1000 + 1 ) * 1000 )
log . Errorf ( "set kLinePreloadLimit to %d, %d %d" , bbgo . KLinePreloadLimit , s . Interval . Minutes ( ) , maxWindow )
2022-07-01 10:38:25 +00:00
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-09-06 05:26:17 +00:00
func ( s * Strategy ) initIndicators ( store * bbgo . SerialMarketDataStore ) 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 {
2022-08-23 08:22:45 +00:00
drift : & indicator . WeightedDrift {
2022-07-25 10:36:22 +00:00
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 {
2022-08-23 08:22:45 +00:00
drift : & indicator . WeightedDrift {
2022-08-15 11:50:34 +00:00
MA : & indicator . SMA { IntervalWindow : types . IntervalWindow { Interval : types . Interval1m , Window : s . Window1m } } ,
IntervalWindow : types . IntervalWindow { Interval : types . Interval1m , Window : s . Window1m } ,
2022-08-08 05:55:01 +00:00
} ,
ma1 : & indicator . EWMA {
2022-08-15 11:50:34 +00:00
IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . SmootherWindow1m } ,
2022-08-08 05:55:01 +00:00
} ,
2022-08-23 08:22:45 +00:00
2022-08-08 05:55:01 +00:00
ma2 : & indicator . FisherTransform {
2022-08-15 11:50:34 +00:00
IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . FisherTransformWindow1m } ,
2022-08-08 05:55:01 +00:00
} ,
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 } }
2022-08-02 11:21:44 +00:00
s . trendLine = & indicator . EWMA { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . TrendWindow } }
2022-07-01 10:38:25 +00:00
klines , ok := store . KLinesOfInterval ( s . Interval )
2022-09-02 03:05:00 +00:00
klinesLength := len ( * klines )
if ! ok || klinesLength == 0 {
2022-07-15 03:30:04 +00:00
return errors . New ( "klines not exists" )
2022-07-01 10:38:25 +00:00
}
for _ , kline := range * klines {
2022-09-02 03:05:00 +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-09-16 10:11:36 +00:00
s . drift . Update ( source , kline . Volume . Abs ( ) . Float64 ( ) )
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-09-02 03:05:00 +00:00
s . priceLines . Update ( source )
2022-08-03 06:10:56 +00:00
}
2022-08-10 11:36:30 +00:00
if s . frameKLine != nil && klines != nil {
s . frameKLine . Set ( & ( * klines ) [ len ( * klines ) - 1 ] )
2022-08-02 11:21:44 +00:00
}
2022-08-02 11:21:44 +00:00
klines , ok = store . KLinesOfInterval ( types . Interval1m )
2022-09-02 03:05:00 +00:00
klinesLength = len ( * klines )
if ! ok || klinesLength == 0 {
2022-08-02 11:21:44 +00:00
return errors . New ( "klines not exists" )
}
for _ , kline := range * klines {
2022-09-02 03:05:00 +00:00
source := s . GetSource ( & kline ) . Float64 ( )
2022-09-16 10:11:36 +00:00
s . drift1m . Update ( source , kline . Volume . Abs ( ) . Float64 ( ) )
2022-08-15 11:50:34 +00:00
if s . drift1m . Last ( ) != s . drift1m . Last ( ) {
panic ( fmt . Sprintf ( "%f %v %f %f" , source , s . drift1m . drift . Values . Index ( 1 ) , s . drift1m . ma2 . Last ( ) , s . drift1m . drift . LastValue ) )
}
2022-08-02 11:21:44 +00:00
}
2022-08-10 11:36:30 +00:00
if s . kline1m != nil && klines != nil {
s . kline1m . Set ( & ( * klines ) [ len ( * klines ) - 1 ] )
}
2022-09-06 05:26:17 +00:00
s . startTime = s . kline1m . StartTime . Time ( ) . Add ( s . kline1m . Interval . Duration ( ) )
2022-07-15 03:30:04 +00:00
return nil
}
2022-07-01 10:38:25 +00:00
2022-08-15 11:50:34 +00:00
func ( s * Strategy ) smartCancel ( ctx context . Context , pricef , atr float64 ) ( int , error ) {
2022-07-29 06:55:23 +00:00
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 {
2022-09-06 10:08:05 +00:00
if order . Status != types . OrderStatusNew && order . Status != types . OrderStatusPartiallyFilled {
continue
}
2022-09-06 05:26:17 +00:00
log . Warnf ( "%v | counter: %d, system: %d" , order , s . orderPendingCounter [ order . OrderID ] , s . minutesCounter )
2022-07-29 06:55:23 +00:00
if s . minutesCounter - s . orderPendingCounter [ order . OrderID ] > s . PendingMinutes {
2022-08-10 11:36:30 +00:00
if order . Side == types . SideTypeBuy && drift [ 1 ] < drift [ 0 ] {
2022-08-08 05:55:01 +00:00
continue
2022-08-10 11:36:30 +00:00
} else if order . Side == types . SideTypeSell && drift [ 1 ] > drift [ 0 ] {
2022-08-08 05:55:01 +00:00
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 )
}
}
2022-08-10 11:36:30 +00:00
log . Warnf ( "cancel all %v" , err )
2022-07-29 06:55:23 +00:00
return 0 , err
}
}
return len ( nonTraded ) , nil
}
2022-08-02 11:21:44 +00:00
func ( s * Strategy ) trailingCheck ( price float64 , direction string ) bool {
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 {
2022-08-10 11:36:30 +00:00
if ( s . sellPrice - s . lowestPrice ) / s . lowestPrice > trailingActivationRatio {
2022-08-02 11:21:44 +00:00
return ( price - s . lowestPrice ) / s . lowestPrice > trailingCallbackRate
}
} else {
2022-08-10 11:36:30 +00:00
if ( s . highestPrice - s . buyPrice ) / s . buyPrice > trailingActivationRatio {
2022-08-02 11:21:44 +00:00
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-09-13 11:40:07 +00:00
var pricef float64
2022-08-15 11:50:34 +00:00
if ! util . TryLock ( & s . lock ) {
2022-07-15 03:30:04 +00:00
return
2022-07-14 03:46:19 +00:00
}
2022-08-15 11:50:34 +00:00
if ! bestAsk . IsZero ( ) && ! bestBid . IsZero ( ) {
s . midPrice = bestAsk . Add ( bestBid ) . Div ( Two )
} else if ! bestAsk . IsZero ( ) {
s . midPrice = bestAsk
} else {
s . midPrice = bestBid
}
pricef = s . midPrice . Float64 ( )
2022-07-29 06:55:23 +00:00
2022-08-10 11:36:30 +00:00
s . lock . Unlock ( )
2022-08-15 11:50:34 +00:00
if ! util . TryLock ( & s . positionLock ) {
return
}
if s . highestPrice > 0 && s . highestPrice < pricef {
s . highestPrice = pricef
}
if s . lowestPrice > 0 && s . lowestPrice > pricef {
s . lowestPrice = pricef
}
2022-07-29 06:55:23 +00:00
// for trailing stoploss during the realtime
2022-08-02 11:21:44 +00:00
if s . NoTrailingStopLoss || s . TrailingStopLossType == "kline" {
2022-08-15 11:50:34 +00:00
s . positionLock . Unlock ( )
2022-07-29 06:55:23 +00:00
return
}
2022-08-10 11:36:30 +00:00
stoploss := s . StopLoss . Float64 ( )
2022-07-29 06:55:23 +00:00
2022-08-15 11:50:34 +00:00
exitShortCondition := s . sellPrice > 0 && ( s . sellPrice * ( 1. + stoploss ) <= pricef ||
2022-08-10 11:36:30 +00:00
s . trailingCheck ( pricef , "short" ) )
2022-08-15 11:50:34 +00:00
exitLongCondition := s . buyPrice > 0 && ( s . buyPrice * ( 1. - stoploss ) >= pricef ||
2022-08-10 11:36:30 +00:00
s . trailingCheck ( pricef , "long" ) )
2022-07-15 03:30:04 +00:00
if exitShortCondition || exitLongCondition {
2022-08-08 05:55:01 +00:00
log . Infof ( "Close position by orderbook changes" )
2022-08-10 11:36:30 +00:00
s . positionLock . Unlock ( )
2022-07-20 10:49:56 +00:00
_ = s . ClosePosition ( ctx , fixedpoint . One )
2022-08-10 11:36:30 +00:00
} else {
s . positionLock . Unlock ( )
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 ( )
2022-08-10 11:36:30 +00:00
defer s . lock . RUnlock ( )
2022-07-14 03:46:19 +00:00
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
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-09-02 03:05:00 +00:00
func ( s * Strategy ) DrawIndicators ( time types . Time ) * types . Canvas {
2022-07-15 08:00:38 +00:00
canvas := types . NewCanvas ( s . InstanceID ( ) , s . Interval )
2022-09-02 03:05:00 +00:00
Length := s . priceLines . 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-09-02 03:05:00 +00:00
mean := s . priceLines . Mean ( Length )
highestPrice := s . priceLines . Minus ( mean ) . Abs ( ) . Highest ( Length )
2022-07-15 08:00:38 +00:00
highestDrift := s . drift . Abs ( ) . Highest ( Length )
2022-07-25 10:36:22 +00:00
hi := s . drift . drift . Abs ( ) . Highest ( Length )
2022-08-15 11:50:34 +00:00
h1m := s . drift1m . Abs ( ) . Highest ( Length * s . Interval . Minutes ( ) )
2022-07-25 10:36:22 +00:00
ratio := highestPrice / highestDrift
2022-08-16 03:30:29 +00:00
2022-09-16 10:11:36 +00:00
//canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length)
2022-07-25 10:36:22 +00:00
canvas . Plot ( "ma" , s . ma , time , Length )
2022-09-16 10:11:36 +00:00
//canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length)
canvas . Plot ( "pos" , types . NumberSeries ( s . DriftFilterPos * ratio + mean ) , time , Length )
canvas . Plot ( "neg" , types . NumberSeries ( s . DriftFilterNeg * ratio + mean ) , time , Length )
fmt . Printf ( "%f %f\n" , highestPrice , hi )
canvas . Plot ( "ppos" , types . NumberSeries ( s . DDriftFilterPos * ( highestPrice / hi ) + mean ) , time , Length )
canvas . Plot ( "nneg" , types . NumberSeries ( s . DDriftFilterNeg * ( highestPrice / hi ) + mean ) , time , Length )
2022-07-25 10:36:22 +00:00
canvas . Plot ( "drift" , s . drift . Mul ( ratio ) . Add ( mean ) , time , Length )
canvas . Plot ( "driftOrig" , s . drift . drift . Mul ( highestPrice / hi ) . Add ( mean ) , time , Length )
2022-08-15 11:50:34 +00:00
canvas . Plot ( "drift1m" , s . drift1m . Mul ( highestPrice / h1m ) . Add ( mean ) , time , Length * s . Interval . Minutes ( ) , types . Interval1m )
2022-07-25 10:36:22 +00:00
canvas . Plot ( "zero" , types . NumberSeries ( mean ) , time , Length )
2022-09-02 03:05:00 +00:00
canvas . Plot ( "price" , s . priceLines , 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 ( ) )
2022-08-10 11:36:30 +00:00
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
}
2022-09-02 03:05:00 +00:00
func ( s * Strategy ) Draw ( time types . Time , profit types . Series , cumProfit types . Series ) {
canvas := s . DrawIndicators ( time )
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
2022-08-10 11:36:30 +00:00
func ( s * Strategy ) Rebalance ( ctx context . Context ) {
2022-08-02 11:21:44 +00:00
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
}
} 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
}
} 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-08-16 03:30:29 +00:00
func ( s * Strategy ) CalcAssetValue ( price fixedpoint . Value ) fixedpoint . Value {
balances := s . Session . GetAccount ( ) . Balances ( )
return balances [ s . Market . BaseCurrency ] . Total ( ) . Mul ( price ) . Add ( balances [ s . Market . QuoteCurrency ] . Total ( ) )
}
2022-09-02 03:05:00 +00:00
func ( s * Strategy ) klineHandler1m ( ctx context . Context , kline types . KLine ) {
s . kline1m . Set ( & kline )
2022-09-16 10:11:36 +00:00
s . drift1m . Update ( s . GetSource ( & kline ) . Float64 ( ) , kline . Volume . Abs ( ) . Float64 ( ) )
2022-09-02 03:05:00 +00:00
if s . Status != types . StrategyStatusRunning {
return
}
// for doing the trailing stoploss during backtesting
atr := s . atr . Last ( )
price := s . getLastPrice ( )
pricef := price . Float64 ( )
stoploss := s . StopLoss . Float64 ( )
2022-09-13 11:40:07 +00:00
lowf := math . Min ( kline . Low . Float64 ( ) , pricef )
highf := math . Max ( kline . High . Float64 ( ) , pricef )
s . positionLock . Lock ( )
if s . lowestPrice > 0 && lowf < s . lowestPrice {
s . lowestPrice = lowf
}
if s . highestPrice > 0 && highf > s . highestPrice {
s . highestPrice = highf
}
2022-09-16 10:11:36 +00:00
drift := s . drift1m . Array ( 2 )
if len ( drift ) < 2 {
s . positionLock . Unlock ( )
return
}
2022-09-13 11:40:07 +00:00
2022-09-02 03:05:00 +00:00
numPending := 0
2022-09-13 11:40:07 +00:00
var err error
2022-09-02 03:05:00 +00:00
if numPending , err = s . smartCancel ( ctx , pricef , atr ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot cancel orders" )
2022-09-13 11:40:07 +00:00
s . positionLock . Unlock ( )
2022-09-02 03:05:00 +00:00
return
}
if numPending > 0 {
2022-09-13 11:40:07 +00:00
s . positionLock . Unlock ( )
2022-09-02 03:05:00 +00:00
return
}
2022-09-13 11:40:07 +00:00
if s . NoTrailingStopLoss || s . TrailingStopLossType == "realtime" {
s . positionLock . Unlock ( )
return
2022-09-02 03:05:00 +00:00
}
2022-09-13 11:40:07 +00:00
2022-09-02 03:05:00 +00:00
//log.Infof("d1m: %f, hf: %f, lf: %f", s.drift1m.Last(), highf, lowf)
exitShortCondition := s . sellPrice > 0 && ( s . sellPrice * ( 1. + stoploss ) <= highf ||
s . trailingCheck ( highf , "short" ) /* || s.drift1m.Last() > 0*/ )
exitLongCondition := s . buyPrice > 0 && ( s . buyPrice * ( 1. - stoploss ) >= lowf ||
s . trailingCheck ( lowf , "long" ) /* || s.drift1m.Last() < 0*/ )
if exitShortCondition || exitLongCondition {
s . positionLock . Unlock ( )
_ = s . ClosePosition ( ctx , fixedpoint . One )
} else {
s . positionLock . Unlock ( )
}
}
func ( s * Strategy ) klineHandler ( ctx context . Context , kline types . KLine ) {
var driftPred , atr float64
var drift [ ] float64
s . frameKLine . Set ( & kline )
source := s . GetSource ( s . frameKLine )
sourcef := source . Float64 ( )
s . priceLines . Update ( sourcef )
s . ma . Update ( sourcef )
s . trendLine . Update ( sourcef )
2022-09-16 10:11:36 +00:00
s . drift . Update ( sourcef , kline . Volume . Abs ( ) . Float64 ( ) )
2022-09-02 03:05:00 +00:00
s . atr . PushK ( kline )
2022-09-16 10:11:36 +00:00
2022-09-02 03:05:00 +00:00
driftPred = s . drift . Predict ( s . PredictOffset )
ddriftPred := s . drift . drift . Predict ( s . PredictOffset )
atr = s . atr . Last ( )
price := s . getLastPrice ( )
pricef := price . Float64 ( )
lowf := math . Min ( kline . Low . Float64 ( ) , pricef )
highf := math . Max ( kline . High . Float64 ( ) , pricef )
lowdiff := s . ma . Last ( ) - lowf
s . stdevLow . Update ( lowdiff )
highdiff := highf - s . ma . Last ( )
s . stdevHigh . Update ( highdiff )
2022-09-16 10:11:36 +00:00
drift = s . drift . Array ( 2 )
if len ( drift ) < 2 || len ( drift ) < s . PredictOffset {
return
}
ddrift := s . drift . drift . Array ( 2 )
if len ( ddrift ) < 2 || len ( ddrift ) < s . PredictOffset {
return
}
2022-09-02 03:05:00 +00:00
if s . Status != types . StrategyStatusRunning {
return
}
stoploss := s . StopLoss . Float64 ( )
s . positionLock . Lock ( )
2022-09-16 10:11:36 +00:00
log . Infof ( "highdiff: %3.2f ma: %.2f, close: %8v, high: %8v, low: %8v, time: %v %v" , s . stdevHigh . Last ( ) , s . ma . Last ( ) , kline . Close , kline . High , kline . Low , kline . StartTime , kline . EndTime )
2022-09-02 03:05:00 +00:00
if s . lowestPrice > 0 && lowf < s . lowestPrice {
s . lowestPrice = lowf
}
if s . highestPrice > 0 && highf > s . highestPrice {
s . highestPrice = highf
}
if ! s . NoRebalance {
s . Rebalance ( ctx )
}
balances := s . GeneralOrderExecutor . Session ( ) . GetAccount ( ) . Balances ( )
bbgo . Notify ( "source: %.4f, price: %.4f, driftPred: %.4f, ddriftPred: %.4f, drift[1]: %.4f, ddrift[1]: %.4f, atr: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f" ,
sourcef , pricef , driftPred , ddriftPred , drift [ 1 ] , ddrift [ 1 ] , atr , lowf , highf , s . lowestPrice , s . highestPrice , s . sellPrice , s . buyPrice )
// Notify will parse args to strings and process separately
bbgo . Notify ( "balances: [Total] %v %s [Base] %s(%v %s) [Quote] %s" ,
s . CalcAssetValue ( price ) ,
s . Market . QuoteCurrency ,
balances [ s . Market . BaseCurrency ] . String ( ) ,
balances [ s . Market . BaseCurrency ] . Total ( ) . Mul ( price ) ,
s . Market . QuoteCurrency ,
balances [ s . Market . QuoteCurrency ] . String ( ) ,
)
shortCondition := ( drift [ 1 ] >= s . DriftFilterNeg || ddrift [ 1 ] >= 0 ) && ( driftPred <= s . DDriftFilterNeg || ddriftPred <= 0 ) || drift [ 1 ] < 0 && drift [ 0 ] < 0
longCondition := ( drift [ 1 ] <= s . DriftFilterPos || ddrift [ 1 ] <= 0 ) && ( driftPred >= s . DDriftFilterPos || ddriftPred >= 0 ) || drift [ 1 ] > 0 && drift [ 0 ] > 0
if shortCondition && longCondition {
if drift [ 1 ] > drift [ 0 ] {
longCondition = false
} else {
shortCondition = false
}
}
exitShortCondition := s . sellPrice > 0 && ! shortCondition && ! longCondition && ( s . sellPrice * ( 1. + stoploss ) <= highf ||
s . trailingCheck ( pricef , "short" ) )
exitLongCondition := s . buyPrice > 0 && ! longCondition && ! shortCondition && ( s . buyPrice * ( 1. - stoploss ) >= lowf ||
s . trailingCheck ( pricef , "long" ) )
if exitShortCondition || exitLongCondition {
if err := s . GeneralOrderExecutor . GracefulCancel ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot cancel orders" )
s . positionLock . Unlock ( )
return
}
s . positionLock . Unlock ( )
_ = s . ClosePosition ( ctx , fixedpoint . One )
}
if longCondition {
if err := s . GeneralOrderExecutor . GracefulCancel ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot cancel orders" )
s . positionLock . Unlock ( )
return
}
source = source . Sub ( fixedpoint . NewFromFloat ( s . stdevLow . Last ( ) * s . HighLowVarianceMultiplier ) )
if source . Compare ( price ) > 0 {
source = price
}
sourcef = source . Float64 ( )
log . Infof ( "source in long %v %v %f" , source , price , s . stdevLow . Last ( ) )
quoteBalance , ok := s . Session . GetAccount ( ) . Balance ( s . Market . QuoteCurrency )
if ! ok {
log . Errorf ( "unable to get quoteCurrency" )
s . positionLock . Unlock ( )
return
}
if s . Market . IsDustQuantity (
quoteBalance . Available . Div ( source ) , source ) {
s . positionLock . Unlock ( )
return
}
s . positionLock . Unlock ( )
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 ,
Tag : "long" ,
} )
log . Infof ( "orders %v" , createdOrders )
if err != nil {
log . WithError ( err ) . Errorf ( "cannot place buy order" )
return
}
s . orderPendingCounter [ createdOrders [ 0 ] . OrderID ] = s . minutesCounter
return
}
if shortCondition {
if err := s . GeneralOrderExecutor . GracefulCancel ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot cancel orders" )
s . positionLock . Unlock ( )
return
}
baseBalance , ok := s . Session . GetAccount ( ) . Balance ( s . Market . BaseCurrency )
if ! ok {
log . Errorf ( "unable to get baseBalance" )
s . positionLock . Unlock ( )
return
}
source = source . Add ( fixedpoint . NewFromFloat ( s . stdevHigh . Last ( ) * s . HighLowVarianceMultiplier ) )
if source . Compare ( price ) < 0 {
source = price
}
sourcef = source . Float64 ( )
log . Infof ( "source in short: %v" , source )
if s . Market . IsDustQuantity ( baseBalance . Available , source ) {
s . positionLock . Unlock ( )
return
}
s . positionLock . Unlock ( )
// Cleanup pending StopOrders
quantity := baseBalance . Available
createdOrders , err := s . GeneralOrderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : s . Symbol ,
Side : types . SideTypeSell ,
Type : types . OrderTypeLimit ,
Price : source ,
Quantity : quantity ,
Tag : "short" ,
} )
if err != nil {
log . WithError ( err ) . Errorf ( "cannot place sell order" )
return
}
s . orderPendingCounter [ createdOrders [ 0 ] . OrderID ] = s . minutesCounter
return
}
s . positionLock . Unlock ( )
}
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 )
}
// StrategyController
s . Status = types . StrategyStatusRunning
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-08-16 03:30:29 +00:00
2022-08-25 09:31:42 +00:00
profit := floats . Slice { 1. , 1. }
2022-08-16 03:30:29 +00:00
price , _ := s . Session . LastPrice ( s . Symbol )
2022-08-16 03:45:40 +00:00
initAsset := s . CalcAssetValue ( price ) . Float64 ( )
2022-08-25 09:31:42 +00:00
cumProfit := floats . Slice { initAsset , initAsset }
2022-08-10 11:36:30 +00:00
modify := func ( p float64 ) float64 {
2022-07-22 04:32:37 +00:00
return p
}
if s . GraphPNLDeductFee {
2022-08-10 11:36:30 +00:00
modify = func ( p float64 ) float64 {
return p * ( 1. - Fee )
2022-07-15 10:59:37 +00:00
}
2022-07-22 04:32:37 +00:00
}
2022-08-10 11:36:30 +00:00
s . GeneralOrderExecutor . TradeCollector ( ) . OnTrade ( func ( trade types . Trade , _profit , _netProfit fixedpoint . Value ) {
2022-08-02 11:21:44 +00:00
s . p . AddTrade ( trade )
2022-08-10 11:36:30 +00:00
order , ok := s . GeneralOrderExecutor . TradeCollector ( ) . OrderStore ( ) . Get ( trade . OrderID )
2022-07-22 04:32:37 +00:00
if ! ok {
panic ( fmt . Sprintf ( "cannot find order: %v" , trade ) )
2022-07-15 10:59:37 +00:00
}
2022-08-10 11:36:30 +00:00
tag := order . Tag
price := trade . Price . Float64 ( )
if s . buyPrice > 0 {
profit . Update ( modify ( price / s . buyPrice ) )
2022-08-16 03:30:29 +00:00
cumProfit . Update ( s . CalcAssetValue ( trade . Price ) . Float64 ( ) )
2022-08-10 11:36:30 +00:00
} else if s . sellPrice > 0 {
profit . Update ( modify ( s . sellPrice / price ) )
2022-08-16 03:30:29 +00:00
cumProfit . Update ( s . CalcAssetValue ( trade . Price ) . Float64 ( ) )
2022-08-10 11:36:30 +00:00
}
s . positionLock . Lock ( )
defer s . positionLock . Unlock ( )
2022-08-23 08:22:45 +00:00
// tag == "" is for exits trades
if tag == "close" || tag == "" {
2022-08-10 11:36:30 +00:00
if s . p . IsDust ( trade . Price ) {
s . buyPrice = 0
s . sellPrice = 0
s . highestPrice = 0
s . lowestPrice = 0
} else if s . p . IsLong ( ) {
s . buyPrice = trade . Price . Float64 ( )
s . sellPrice = 0
s . highestPrice = s . buyPrice
s . lowestPrice = 0
2022-07-22 04:32:37 +00:00
} else {
2022-08-10 11:36:30 +00:00
s . sellPrice = trade . Price . Float64 ( )
s . buyPrice = 0
s . highestPrice = 0
s . lowestPrice = s . sellPrice
2022-07-22 04:32:37 +00:00
}
} else if tag == "long" {
2022-08-10 11:36:30 +00:00
if s . p . IsDust ( trade . Price ) {
s . buyPrice = 0
s . sellPrice = 0
s . highestPrice = 0
s . lowestPrice = 0
} else if s . p . IsLong ( ) {
s . buyPrice = trade . Price . Float64 ( )
s . sellPrice = 0
s . highestPrice = s . buyPrice
s . lowestPrice = 0
2022-07-15 10:59:37 +00:00
}
2022-08-10 11:36:30 +00:00
} else if tag == "short" {
if s . p . IsDust ( trade . Price ) {
s . sellPrice = 0
s . buyPrice = 0
s . highestPrice = 0
s . lowestPrice = 0
} else if s . p . IsShort ( ) {
s . sellPrice = trade . Price . Float64 ( )
s . buyPrice = 0
s . highestPrice = 0
s . lowestPrice = s . sellPrice
2022-08-02 11:21:44 +00:00
}
2022-08-10 11:36:30 +00:00
} else {
panic ( "tag unknown" )
2022-08-09 04:26:02 +00:00
}
2022-08-10 11:36:30 +00:00
bbgo . Notify ( "tag: %s, sp: %.4f bp: %.4f hp: %.4f lp: %.4f, trade: %s, pos: %s" , tag , s . sellPrice , s . buyPrice , s . highestPrice , s . lowestPrice , trade . String ( ) , s . p . String ( ) )
2022-07-22 04:32:37 +00:00
} )
2022-07-15 03:30:04 +00:00
2022-08-10 11:36:30 +00:00
s . frameKLine = & types . KLine { }
s . kline1m = & types . KLine { }
2022-09-02 03:05:00 +00:00
s . priceLines = types . NewQueue ( 300 )
2022-07-15 03:30:04 +00:00
2022-09-06 05:26:17 +00:00
s . initTickerFunctions ( ctx )
2022-09-02 03:05:00 +00:00
startTime := s . Environment . StartTime ( )
s . TradeStats . SetIntervalProfitCollector ( types . NewIntervalProfitCollector ( types . Interval1d , startTime ) )
s . TradeStats . SetIntervalProfitCollector ( types . NewIntervalProfitCollector ( types . Interval1w , startTime ) )
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-08-03 08:34:51 +00:00
bbgo . RegisterCommand ( "/draw" , "Draw Indicators" , func ( reply interact . Reply ) {
2022-09-02 03:05:00 +00:00
canvas := s . DrawIndicators ( s . frameKLine . StartTime )
2022-08-03 06:10:56 +00:00
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-16 03:30:29 +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-16 03:30:29 +00:00
bbgo . RegisterCommand ( "/cumpnl" , "Draw Cummulative PNL(Quote)" , 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-08-10 11:36:30 +00:00
bbgo . RegisterCommand ( "/pos" , "Show internal position" , func ( reply interact . Reply ) {
reply . Message ( s . p . String ( ) )
} )
bbgo . RegisterCommand ( "/dump" , "Dump internal params" , func ( reply interact . Reply ) {
reply . Message ( "Please enter series output length:" )
} ) . Next ( func ( length string , reply interact . Reply ) {
var buffer bytes . Buffer
l , err := strconv . Atoi ( length )
if err != nil {
s . ParamDump ( & buffer )
} else {
s . ParamDump ( & buffer , l )
2022-07-01 10:38:25 +00:00
}
2022-08-10 11:36:30 +00:00
reply . Message ( buffer . String ( ) )
} )
2022-09-13 11:40:07 +00:00
bbgo . RegisterModifier ( s )
2022-09-02 03:05:00 +00:00
// event trigger order: s.Interval => Interval1m
store , ok := session . SerialMarketDataStore ( s . Symbol , [ ] types . Interval { s . Interval , types . Interval1m } )
if ! ok {
panic ( "cannot get 1m history" )
}
2022-09-06 05:26:17 +00:00
if err := s . initIndicators ( store ) ; err != nil {
log . WithError ( err ) . Errorf ( "initIndicator failed" )
return nil
}
2022-09-02 03:05:00 +00:00
store . OnKLineClosed ( func ( kline types . KLine ) {
2022-09-06 05:26:17 +00:00
s . minutesCounter = int ( kline . StartTime . Time ( ) . Add ( kline . Interval . Duration ( ) ) . Sub ( s . startTime ) . Minutes ( ) )
2022-07-12 10:14:57 +00:00
if kline . Interval == types . Interval1m {
2022-09-02 03:05:00 +00:00
s . klineHandler1m ( ctx , kline )
} else if kline . Interval == s . Interval {
s . klineHandler ( ctx , kline )
2022-08-02 11:21:44 +00:00
}
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 )
2022-08-15 11:50:34 +00:00
fmt . Fprintln ( & buffer , "--- NonProfitable Dates ---" )
for _ , daypnl := range s . TradeStats . IntervalProfits [ types . Interval1d ] . GetNonProfitableIntervals ( ) {
fmt . Fprintf ( & buffer , "%s\n" , daypnl )
}
2022-08-08 05:55:01 +00:00
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-09-02 03:05:00 +00:00
s . Draw ( s . frameKLine . StartTime , & profit , & cumProfit )
2022-07-15 10:59:37 +00:00
}
2022-07-01 10:38:25 +00:00
wg . Done ( )
} )
return nil
}