2022-05-27 06:36:48 +00:00
package supertrend
import (
"context"
"fmt"
2022-07-05 08:30:13 +00:00
"os"
2022-06-19 04:29:36 +00:00
"sync"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
2022-09-09 09:40:17 +00:00
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/datatype/floats"
2022-05-27 06:36:48 +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"
)
const ID = "supertrend"
var log = logrus . WithField ( "strategy" , ID )
2022-07-05 08:55:48 +00:00
// TODO: limit order for ATR TP
2022-05-27 06:36:48 +00:00
func init ( ) {
// Register the pointer of the strategy struct,
// so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON)
// Note: built-in strategies need to imported manually in the bbgo cmd package.
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
2022-08-23 10:43:13 +00:00
// AccumulatedProfitReport For accumulated profit report output
type AccumulatedProfitReport struct {
// AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades
AccumulatedProfitMAWindow int ` json:"accumulatedProfitMAWindow" `
// IntervalWindow interval window, in days
IntervalWindow int ` json:"intervalWindow" `
// NumberOfInterval How many intervals to output to TSV
NumberOfInterval int ` json:"NumberOfInterval" `
// TsvReportPath The path to output report to
TsvReportPath string ` json:"tsvReportPath" `
2022-08-24 03:23:48 +00:00
// AccumulatedDailyProfitWindow The window to sum up the daily profit, in days
AccumulatedDailyProfitWindow int ` json:"accumulatedDailyProfitWindow" `
2022-08-23 10:43:13 +00:00
// Accumulated profit
accumulatedProfit fixedpoint . Value
2022-08-25 09:31:42 +00:00
accumulatedProfitPerDay floats . Slice
2022-08-23 10:43:13 +00:00
previousAccumulatedProfit fixedpoint . Value
// Accumulated profit MA
accumulatedProfitMA * indicator . SMA
2022-08-25 09:31:42 +00:00
accumulatedProfitMAPerDay floats . Slice
2022-08-23 10:43:13 +00:00
// Daily profit
2022-08-25 09:31:42 +00:00
dailyProfit floats . Slice
2022-08-23 10:43:13 +00:00
// Accumulated fee
accumulatedFee fixedpoint . Value
2022-08-25 09:31:42 +00:00
accumulatedFeePerDay floats . Slice
2022-08-23 10:43:13 +00:00
// Win ratio
2022-08-25 09:31:42 +00:00
winRatioPerDay floats . Slice
2022-08-23 10:43:13 +00:00
// Profit factor
2022-08-25 09:31:42 +00:00
profitFactorPerDay floats . Slice
2022-09-07 10:27:11 +00:00
// Trade number
dailyTrades floats . Slice
accumulatedTrades int
previousAccumulatedTrades int
2022-08-23 10:43:13 +00:00
}
func ( r * AccumulatedProfitReport ) Initialize ( ) {
if r . AccumulatedProfitMAWindow <= 0 {
r . AccumulatedProfitMAWindow = 60
}
if r . IntervalWindow <= 0 {
r . IntervalWindow = 7
}
2022-08-24 03:23:48 +00:00
if r . AccumulatedDailyProfitWindow <= 0 {
r . AccumulatedDailyProfitWindow = 7
}
2022-08-23 10:43:13 +00:00
if r . NumberOfInterval <= 0 {
r . NumberOfInterval = 1
}
r . accumulatedProfitMA = & indicator . SMA { IntervalWindow : types . IntervalWindow { Interval : types . Interval1d , Window : r . AccumulatedProfitMAWindow } }
}
func ( r * AccumulatedProfitReport ) RecordProfit ( profit fixedpoint . Value ) {
r . accumulatedProfit = r . accumulatedProfit . Add ( profit )
}
2022-09-07 10:27:11 +00:00
func ( r * AccumulatedProfitReport ) RecordTrade ( fee fixedpoint . Value ) {
2022-08-23 10:43:13 +00:00
r . accumulatedFee = r . accumulatedFee . Add ( fee )
2022-09-07 10:27:11 +00:00
r . accumulatedTrades += 1
2022-08-23 10:43:13 +00:00
}
func ( r * AccumulatedProfitReport ) DailyUpdate ( tradeStats * types . TradeStats ) {
// Daily profit
r . dailyProfit . Update ( r . accumulatedProfit . Sub ( r . previousAccumulatedProfit ) . Float64 ( ) )
r . previousAccumulatedProfit = r . accumulatedProfit
// Accumulated profit
r . accumulatedProfitPerDay . Update ( r . accumulatedProfit . Float64 ( ) )
// Accumulated profit MA
2022-09-07 10:27:11 +00:00
r . accumulatedProfitMA . Update ( r . accumulatedProfit . Float64 ( ) )
2023-05-31 11:35:44 +00:00
r . accumulatedProfitMAPerDay . Update ( r . accumulatedProfitMA . Last ( 0 ) )
2022-08-23 10:43:13 +00:00
// Accumulated Fee
r . accumulatedFeePerDay . Update ( r . accumulatedFee . Float64 ( ) )
// Win ratio
r . winRatioPerDay . Update ( tradeStats . WinningRatio . Float64 ( ) )
// Profit factor
r . profitFactorPerDay . Update ( tradeStats . ProfitFactor . Float64 ( ) )
2022-09-07 10:27:11 +00:00
// Daily trades
r . dailyTrades . Update ( float64 ( r . accumulatedTrades - r . previousAccumulatedTrades ) )
r . previousAccumulatedTrades = r . accumulatedTrades
2022-08-23 10:43:13 +00:00
}
// Output Accumulated profit report to a TSV file
func ( r * AccumulatedProfitReport ) Output ( symbol string ) {
if r . TsvReportPath != "" {
tsvwiter , err := tsv . AppendWriterFile ( r . TsvReportPath )
if err != nil {
panic ( err )
}
defer tsvwiter . Close ( )
// Output symbol, total acc. profit, acc. profit 60MA, interval acc. profit, fee, win rate, profit factor
2022-09-07 10:27:11 +00:00
_ = tsvwiter . Write ( [ ] string { "#" , "Symbol" , "accumulatedProfit" , "accumulatedProfitMA" , fmt . Sprintf ( "%dd profit" , r . AccumulatedDailyProfitWindow ) , "accumulatedFee" , "winRatio" , "profitFactor" , "60D trades" } )
2022-08-23 10:43:13 +00:00
for i := 0 ; i <= r . NumberOfInterval - 1 ; i ++ {
2022-09-07 10:27:11 +00:00
accumulatedProfit := r . accumulatedProfitPerDay . Index ( r . IntervalWindow * i )
accumulatedProfitStr := fmt . Sprintf ( "%f" , accumulatedProfit )
accumulatedProfitMA := r . accumulatedProfitMAPerDay . Index ( r . IntervalWindow * i )
accumulatedProfitMAStr := fmt . Sprintf ( "%f" , accumulatedProfitMA )
intervalAccumulatedProfit := r . dailyProfit . Tail ( r . AccumulatedDailyProfitWindow + r . IntervalWindow * i ) . Sum ( ) - r . dailyProfit . Tail ( r . IntervalWindow * i ) . Sum ( )
intervalAccumulatedProfitStr := fmt . Sprintf ( "%f" , intervalAccumulatedProfit )
2022-08-23 10:43:13 +00:00
accumulatedFee := fmt . Sprintf ( "%f" , r . accumulatedFeePerDay . Index ( r . IntervalWindow * i ) )
winRatio := fmt . Sprintf ( "%f" , r . winRatioPerDay . Index ( r . IntervalWindow * i ) )
profitFactor := fmt . Sprintf ( "%f" , r . profitFactorPerDay . Index ( r . IntervalWindow * i ) )
2022-09-07 10:27:11 +00:00
trades := r . dailyTrades . Tail ( 60 + r . IntervalWindow * i ) . Sum ( ) - r . dailyTrades . Tail ( r . IntervalWindow * i ) . Sum ( )
tradesStr := fmt . Sprintf ( "%f" , trades )
2022-08-23 10:43:13 +00:00
2022-09-07 10:27:11 +00:00
_ = tsvwiter . Write ( [ ] string { fmt . Sprintf ( "%d" , i + 1 ) , symbol , accumulatedProfitStr , accumulatedProfitMAStr , intervalAccumulatedProfitStr , accumulatedFee , winRatio , profitFactor , tradesStr } )
2022-08-23 10:43:13 +00:00
}
}
}
2022-05-27 06:36:48 +00:00
type Strategy struct {
2022-05-27 10:24:08 +00:00
Environment * bbgo . Environment
Market types . Market
2022-05-27 06:36:48 +00:00
// persistence fields
2022-07-05 08:30:13 +00:00
Position * types . Position ` persistence:"position" `
ProfitStats * types . ProfitStats ` persistence:"profit_stats" `
TradeStats * types . TradeStats ` persistence:"trade_stats" `
2022-05-27 06:36:48 +00:00
// Symbol is the market symbol you want to trade
Symbol string ` json:"symbol" `
2022-07-08 08:31:28 +00:00
types . IntervalWindow
2022-07-06 09:05:38 +00:00
// Double DEMA
2022-07-11 05:37:01 +00:00
doubleDema * DoubleDema
// FastDEMAWindow DEMA window for checking breakout
FastDEMAWindow int ` json:"fastDEMAWindow" `
// SlowDEMAWindow DEMA window for checking breakout
SlowDEMAWindow int ` json:"slowDEMAWindow" `
2022-07-06 09:05:38 +00:00
2022-05-27 06:36:48 +00:00
// SuperTrend indicator
2022-06-02 05:32:57 +00:00
Supertrend * indicator . Supertrend
// SupertrendMultiplier ATR multiplier for calculation of supertrend
SupertrendMultiplier float64 ` json:"supertrendMultiplier" `
2022-05-27 06:36:48 +00:00
2022-07-05 07:59:35 +00:00
// LinearRegression Use linear regression as trend confirmation
2022-08-08 04:43:38 +00:00
LinearRegression * LinReg ` json:"linearRegression,omitempty" `
2022-06-30 08:35:00 +00:00
2022-08-05 03:47:36 +00:00
// Leverage uses the account net value to calculate the order qty
Leverage fixedpoint . Value ` json:"leverage" `
// Quantity sets the fixed order qty, takes precedence over Leverage
Quantity fixedpoint . Value ` json:"quantity" `
2022-09-09 09:40:17 +00:00
AccountValueCalculator * bbgo . AccountValueCalculator
2022-05-30 08:07:36 +00:00
2022-07-05 07:59:35 +00:00
// TakeProfitAtrMultiplier TP according to ATR multiple, 0 to disable this
TakeProfitAtrMultiplier float64 ` json:"takeProfitAtrMultiplier" `
2022-05-31 04:53:14 +00:00
2022-07-05 07:59:35 +00:00
// StopLossByTriggeringK Set SL price to the low/high of the triggering Kline
2022-05-31 04:53:14 +00:00
StopLossByTriggeringK bool ` json:"stopLossByTriggeringK" `
2022-07-05 07:59:35 +00:00
// StopByReversedSupertrend TP/SL by reversed supertrend signal
StopByReversedSupertrend bool ` json:"stopByReversedSupertrend" `
// StopByReversedDema TP/SL by reversed DEMA signal
StopByReversedDema bool ` json:"stopByReversedDema" `
// StopByReversedLinGre TP/SL by reversed linear regression signal
StopByReversedLinGre bool ` json:"stopByReversedLinGre" `
2022-05-31 04:53:14 +00:00
2022-07-08 07:41:28 +00:00
// ExitMethods Exit methods
ExitMethods bbgo . ExitMethodSet ` json:"exits" `
2022-09-10 23:19:23 +00:00
// whether to draw graph or not by the end of backtest
DrawGraph bool ` json:"drawGraph" `
GraphPNLPath string ` json:"graphPNLPath" `
GraphCumPNLPath string ` json:"graphCumPNLPath" `
// for position
buyPrice float64 ` persistence:"buy_price" `
sellPrice float64 ` persistence:"sell_price" `
highestPrice float64 ` persistence:"highest_price" `
lowestPrice float64 ` persistence:"lowest_price" `
2022-07-08 07:41:28 +00:00
session * bbgo . ExchangeSession
orderExecutor * bbgo . GeneralOrderExecutor
2022-05-31 04:53:14 +00:00
currentTakeProfitPrice fixedpoint . Value
currentStopLossPrice fixedpoint . Value
2022-05-27 06:36:48 +00:00
// StrategyController
bbgo . StrategyController
2022-08-03 06:04:30 +00:00
// Accumulated profit report
2022-08-24 05:58:30 +00:00
AccumulatedProfitReport * AccumulatedProfitReport ` json:"accumulatedProfitReport" `
2022-05-27 06:36:48 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) InstanceID ( ) string {
return fmt . Sprintf ( "%s:%s" , ID , s . Symbol )
}
func ( s * Strategy ) Validate ( ) error {
if len ( s . Symbol ) == 0 {
return errors . New ( "symbol is required" )
}
2022-05-30 08:22:13 +00:00
if len ( s . Interval ) == 0 {
return errors . New ( "interval is required" )
}
2022-05-27 06:36:48 +00:00
return nil
}
func ( s * Strategy ) Subscribe ( session * bbgo . ExchangeSession ) {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . Interval } )
2022-07-08 08:42:31 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . LinearRegression . Interval } )
2022-07-25 06:11:55 +00:00
s . ExitMethods . SetAndSubscribe ( session , s )
2022-08-03 07:31:20 +00:00
// Accumulated profit report
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : types . Interval1d } )
2022-05-30 06:52:51 +00:00
}
2022-05-27 06:36:48 +00:00
2022-05-30 08:26:17 +00:00
// Position control
func ( s * Strategy ) CurrentPosition ( ) * types . Position {
return s . Position
}
func ( s * Strategy ) ClosePosition ( ctx context . Context , percentage fixedpoint . Value ) error {
base := s . Position . GetBase ( )
if base . IsZero ( ) {
return fmt . Errorf ( "no opened %s position" , s . Position . Symbol )
}
// make it negative
quantity := base . Mul ( percentage ) . Abs ( )
side := types . SideTypeBuy
if base . Sign ( ) > 0 {
side = types . SideTypeSell
}
if quantity . Compare ( s . Market . MinQuantity ) < 0 {
2022-06-16 09:14:50 +00:00
return fmt . Errorf ( "%s order quantity %v is too small, less than %v" , s . Symbol , quantity , s . Market . MinQuantity )
2022-05-30 08:26:17 +00:00
}
2022-06-01 02:26:04 +00:00
orderForm := s . generateOrderForm ( side , quantity , types . SideEffectTypeAutoRepay )
2022-05-30 08:26:17 +00:00
2022-07-05 08:25:02 +00:00
bbgo . Notify ( "submitting %s %s order to close position by %v" , s . Symbol , side . String ( ) , percentage , orderForm )
2022-05-30 08:26:17 +00:00
2022-06-20 05:39:07 +00:00
_ , err := s . orderExecutor . SubmitOrders ( ctx , orderForm )
2022-05-30 08:26:17 +00:00
if err != nil {
2022-06-16 09:14:50 +00:00
log . WithError ( err ) . Errorf ( "can not place %s position close order" , s . Symbol )
2022-06-19 04:29:36 +00:00
bbgo . Notify ( "can not place %s position close order" , s . Symbol )
2022-05-30 08:26:17 +00:00
}
return err
}
2022-06-01 02:26:04 +00:00
// setupIndicators initializes indicators
func ( s * Strategy ) setupIndicators ( ) {
2022-07-05 08:25:02 +00:00
// K-line store for indicators
kLineStore , _ := s . session . MarketDataStore ( s . Symbol )
2022-07-06 08:45:19 +00:00
// Double DEMA
2022-07-11 05:37:01 +00:00
s . doubleDema = newDoubleDema ( kLineStore , s . Interval , s . FastDEMAWindow , s . SlowDEMAWindow )
2022-05-27 06:36:48 +00:00
2022-07-05 08:25:02 +00:00
// Supertrend
2022-07-08 08:42:31 +00:00
if s . Window == 0 {
s . Window = 39
2022-05-30 06:52:51 +00:00
}
2022-06-02 05:32:57 +00:00
if s . SupertrendMultiplier == 0 {
s . SupertrendMultiplier = 3
2022-05-30 06:52:51 +00:00
}
2022-07-08 08:42:31 +00:00
s . Supertrend = & indicator . Supertrend { IntervalWindow : types . IntervalWindow { Window : s . Window , Interval : s . Interval } , ATRMultiplier : s . SupertrendMultiplier }
s . Supertrend . AverageTrueRange = & indicator . ATR { IntervalWindow : types . IntervalWindow { Window : s . Window , Interval : s . Interval } }
2022-08-08 05:07:59 +00:00
s . Supertrend . BindK ( s . session . MarketDataStream , s . Symbol , s . Supertrend . Interval )
if klines , ok := kLineStore . KLinesOfInterval ( s . Supertrend . Interval ) ; ok {
s . Supertrend . LoadK ( ( * klines ) [ 0 : ] )
}
2022-06-02 05:32:57 +00:00
2022-07-05 08:25:02 +00:00
// Linear Regression
if s . LinearRegression != nil {
if s . LinearRegression . Window == 0 {
s . LinearRegression = nil
2022-07-06 08:45:19 +00:00
} else if s . LinearRegression . Interval == "" {
s . LinearRegression = nil
2022-07-05 08:25:02 +00:00
} else {
2022-08-08 04:43:38 +00:00
s . LinearRegression . BindK ( s . session . MarketDataStream , s . Symbol , s . LinearRegression . Interval )
if klines , ok := kLineStore . KLinesOfInterval ( s . LinearRegression . Interval ) ; ok {
s . LinearRegression . LoadK ( ( * klines ) [ 0 : ] )
}
2022-07-05 08:25:02 +00:00
}
2022-05-30 08:07:36 +00:00
}
}
2022-07-06 08:26:30 +00:00
func ( s * Strategy ) shouldStop ( kline types . KLine , stSignal types . Direction , demaSignal types . Direction , lgSignal types . Direction ) bool {
stopNow := false
base := s . Position . GetBase ( )
baseSign := base . Sign ( )
if s . StopLossByTriggeringK && ! s . currentStopLossPrice . IsZero ( ) && ( ( baseSign < 0 && kline . GetClose ( ) . Compare ( s . currentStopLossPrice ) > 0 ) || ( baseSign > 0 && kline . GetClose ( ) . Compare ( s . currentStopLossPrice ) < 0 ) ) {
// SL by triggering Kline low/high
bbgo . Notify ( "%s stop loss by triggering the kline low/high" , s . Symbol )
stopNow = true
} else if s . TakeProfitAtrMultiplier > 0 && ! s . currentTakeProfitPrice . IsZero ( ) && ( ( baseSign < 0 && kline . GetClose ( ) . Compare ( s . currentTakeProfitPrice ) < 0 ) || ( baseSign > 0 && kline . GetClose ( ) . Compare ( s . currentTakeProfitPrice ) > 0 ) ) {
// TP by multiple of ATR
bbgo . Notify ( "%s take profit by multiple of ATR" , s . Symbol )
stopNow = true
} else if s . StopByReversedSupertrend && ( ( baseSign < 0 && stSignal == types . DirectionUp ) || ( baseSign > 0 && stSignal == types . DirectionDown ) ) {
// Use supertrend signal to TP/SL
bbgo . Notify ( "%s stop by the reversed signal of Supertrend" , s . Symbol )
stopNow = true
} else if s . StopByReversedDema && ( ( baseSign < 0 && demaSignal == types . DirectionUp ) || ( baseSign > 0 && demaSignal == types . DirectionDown ) ) {
// Use DEMA signal to TP/SL
bbgo . Notify ( "%s stop by the reversed signal of DEMA" , s . Symbol )
stopNow = true
} else if s . StopByReversedLinGre && ( ( baseSign < 0 && lgSignal == types . DirectionUp ) || ( baseSign > 0 && lgSignal == types . DirectionDown ) ) {
// Use linear regression signal to TP/SL
bbgo . Notify ( "%s stop by the reversed signal of linear regression" , s . Symbol )
stopNow = true
}
return stopNow
}
2022-07-06 10:11:09 +00:00
func ( s * Strategy ) getSide ( stSignal types . Direction , demaSignal types . Direction , lgSignal types . Direction ) types . SideType {
var side types . SideType
if stSignal == types . DirectionUp && demaSignal == types . DirectionUp && ( s . LinearRegression == nil || lgSignal == types . DirectionUp ) {
side = types . SideTypeBuy
} else if stSignal == types . DirectionDown && demaSignal == types . DirectionDown && ( s . LinearRegression == nil || lgSignal == types . DirectionDown ) {
side = types . SideTypeSell
}
return side
}
2022-06-01 02:26:04 +00:00
func ( s * Strategy ) generateOrderForm ( side types . SideType , quantity fixedpoint . Value , marginOrderSideEffect types . MarginOrderSideEffectType ) types . SubmitOrder {
2022-05-30 08:07:36 +00:00
orderForm := types . SubmitOrder {
2022-05-31 07:46:55 +00:00
Symbol : s . Symbol ,
Market : s . Market ,
Side : side ,
Type : types . OrderTypeMarket ,
Quantity : quantity ,
MarginSideEffect : marginOrderSideEffect ,
2022-05-30 08:07:36 +00:00
}
return orderForm
}
2022-06-01 02:26:04 +00:00
// calculateQuantity returns leveraged quantity
2022-08-05 07:11:15 +00:00
func ( s * Strategy ) calculateQuantity ( ctx context . Context , currentPrice fixedpoint . Value , side types . SideType ) fixedpoint . Value {
2022-08-05 03:47:36 +00:00
// Quantity takes precedence
if ! s . Quantity . IsZero ( ) {
return s . Quantity
2022-06-01 02:26:04 +00:00
}
2022-08-05 03:47:36 +00:00
usingLeverage := s . session . Margin || s . session . IsolatedMargin || s . session . Futures || s . session . IsolatedFutures
2022-05-30 08:07:36 +00:00
2022-08-05 07:11:15 +00:00
if bbgo . IsBackTesting { // Backtesting
2022-08-05 03:47:36 +00:00
balance , ok := s . session . GetAccount ( ) . Balance ( s . Market . QuoteCurrency )
if ! ok {
log . Errorf ( "can not update %s quote balance from exchange" , s . Symbol )
return fixedpoint . Zero
}
2022-08-05 07:11:15 +00:00
return balance . Available . Mul ( fixedpoint . Min ( s . Leverage , fixedpoint . One ) ) . Div ( currentPrice )
2022-08-05 08:28:42 +00:00
} else if ! usingLeverage && side == types . SideTypeSell { // Spot sell
balance , ok := s . session . GetAccount ( ) . Balance ( s . Market . BaseCurrency )
if ! ok {
log . Errorf ( "can not update %s base balance from exchange" , s . Symbol )
return fixedpoint . Zero
2022-08-05 07:11:15 +00:00
}
2022-08-05 08:28:42 +00:00
return balance . Available . Mul ( fixedpoint . Min ( s . Leverage , fixedpoint . One ) )
} else { // Using leverage or spot buy
2022-09-09 09:40:17 +00:00
quoteQty , err := bbgo . CalculateQuoteQuantity ( ctx , s . session , s . Market . QuoteCurrency , s . Leverage )
2022-08-05 03:47:36 +00:00
if err != nil {
2022-08-05 08:28:42 +00:00
log . WithError ( err ) . Errorf ( "can not update %s quote balance from exchange" , s . Symbol )
2022-08-05 03:47:36 +00:00
return fixedpoint . Zero
}
2022-08-05 08:28:42 +00:00
return quoteQty . Div ( currentPrice )
2022-08-05 03:47:36 +00:00
}
2022-05-30 08:07:36 +00:00
}
2022-09-10 23:19:23 +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-05-27 06:36:48 +00:00
func ( s * Strategy ) Run ( ctx context . Context , orderExecutor bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
s . session = session
2022-07-08 07:41:28 +00:00
s . currentStopLossPrice = fixedpoint . Zero
s . currentTakeProfitPrice = fixedpoint . Zero
2022-06-20 05:39:07 +00:00
// calculate group id for orders
instanceID := s . InstanceID ( )
2022-05-27 06:36:48 +00:00
// If position is nil, we need to allocate a new position for calculation
if s . Position == nil {
s . Position = types . NewPositionFromMarket ( s . Market )
}
// Always update the position fields
s . Position . Strategy = ID
s . Position . StrategyInstanceID = s . InstanceID ( )
2022-07-05 08:30:13 +00:00
// Profit stats
if s . ProfitStats == nil {
s . ProfitStats = types . NewProfitStats ( s . Market )
}
if s . TradeStats == nil {
2022-07-08 07:44:32 +00:00
s . TradeStats = types . NewTradeStats ( s . Symbol )
2022-07-05 08:30:13 +00:00
}
2022-08-08 09:42:21 +00:00
// Interval profit report
if bbgo . IsBackTesting {
startTime := s . Environment . StartTime ( )
s . TradeStats . SetIntervalProfitCollector ( types . NewIntervalProfitCollector ( types . Interval1d , startTime ) )
s . TradeStats . SetIntervalProfitCollector ( types . NewIntervalProfitCollector ( types . Interval1w , startTime ) )
s . TradeStats . SetIntervalProfitCollector ( types . NewIntervalProfitCollector ( types . Interval1mo , startTime ) )
}
2022-07-05 08:30:13 +00:00
2022-06-20 05:39:07 +00:00
// Set fee rate
if s . session . MakerFeeRate . Sign ( ) > 0 || s . session . TakerFeeRate . Sign ( ) > 0 {
s . Position . SetExchangeFeeRate ( s . session . ExchangeName , types . ExchangeFee {
MakerFeeRate : s . session . MakerFeeRate ,
TakerFeeRate : s . session . TakerFeeRate ,
} )
}
2022-05-30 08:48:07 +00:00
2022-06-20 05:39:07 +00:00
// Setup order executor
s . orderExecutor = bbgo . NewGeneralOrderExecutor ( session , s . Symbol , ID , instanceID , s . Position )
s . orderExecutor . BindEnvironment ( s . Environment )
s . orderExecutor . BindProfitStats ( s . ProfitStats )
2022-07-05 08:30:13 +00:00
s . orderExecutor . BindTradeStats ( s . TradeStats )
2022-06-20 05:39:07 +00:00
s . orderExecutor . Bind ( )
2022-08-05 03:47:36 +00:00
// AccountValueCalculator
2022-09-09 09:40:17 +00:00
s . AccountValueCalculator = bbgo . NewAccountValueCalculator ( s . session , s . Market . QuoteCurrency )
2022-08-05 03:47:36 +00:00
2022-08-03 06:04:30 +00:00
// Accumulated profit report
2022-08-23 10:43:13 +00:00
if bbgo . IsBackTesting {
2022-08-24 05:58:30 +00:00
if s . AccumulatedProfitReport == nil {
s . AccumulatedProfitReport = & AccumulatedProfitReport { }
}
2022-08-23 10:43:13 +00:00
s . AccumulatedProfitReport . Initialize ( )
s . orderExecutor . TradeCollector ( ) . OnProfit ( func ( trade types . Trade , profit * types . Profit ) {
if profit == nil {
return
}
2022-08-03 06:04:30 +00:00
2022-08-23 10:43:13 +00:00
s . AccumulatedProfitReport . RecordProfit ( profit . Profit )
} )
2022-09-10 23:19:23 +00:00
// s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
// s.AccumulatedProfitReport.RecordTrade(trade.Fee)
// })
2022-08-23 10:43:13 +00:00
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , types . Interval1d , func ( kline types . KLine ) {
s . AccumulatedProfitReport . DailyUpdate ( s . TradeStats )
} ) )
2022-08-03 07:31:20 +00:00
}
2022-08-03 06:04:30 +00:00
2022-09-10 23:19:23 +00:00
// For drawing
2022-09-11 06:44:59 +00:00
profitSlice := floats . Slice { 1. , 1. }
2022-09-10 23:19:23 +00:00
price , _ := session . LastPrice ( s . Symbol )
initAsset := s . CalcAssetValue ( price ) . Float64 ( )
2022-09-11 06:44:59 +00:00
cumProfitSlice := floats . Slice { initAsset , initAsset }
2022-09-10 23:19:23 +00:00
s . orderExecutor . TradeCollector ( ) . OnTrade ( func ( trade types . Trade , profit fixedpoint . Value , netProfit fixedpoint . Value ) {
if bbgo . IsBackTesting {
s . AccumulatedProfitReport . RecordTrade ( trade . Fee )
}
// For drawing/charting
price := trade . Price . Float64 ( )
if s . buyPrice > 0 {
2022-09-11 06:44:59 +00:00
profitSlice . Update ( price / s . buyPrice )
cumProfitSlice . Update ( s . CalcAssetValue ( trade . Price ) . Float64 ( ) )
2022-09-10 23:19:23 +00:00
} else if s . sellPrice > 0 {
2022-09-11 06:44:59 +00:00
profitSlice . Update ( s . sellPrice / price )
cumProfitSlice . Update ( s . CalcAssetValue ( trade . Price ) . Float64 ( ) )
2022-09-10 23:19:23 +00:00
}
if s . Position . IsDust ( trade . Price ) {
s . buyPrice = 0
s . sellPrice = 0
s . highestPrice = 0
s . lowestPrice = 0
} else if s . Position . IsLong ( ) {
s . buyPrice = price
s . sellPrice = 0
s . highestPrice = s . buyPrice
s . lowestPrice = 0
} else {
s . sellPrice = price
s . buyPrice = 0
s . highestPrice = 0
s . lowestPrice = s . sellPrice
}
} )
2022-09-11 07:48:08 +00:00
s . InitDrawCommands ( & profitSlice , & cumProfitSlice )
2022-09-10 23:19:23 +00:00
2022-06-20 05:39:07 +00:00
// Sync position to redis on trade
s . orderExecutor . TradeCollector ( ) . OnPositionUpdate ( func ( position * types . Position ) {
2022-10-03 10:45:24 +00:00
bbgo . Sync ( ctx , s )
2022-06-20 05:39:07 +00:00
} )
2022-05-27 06:36:48 +00:00
// StrategyController
s . Status = types . StrategyStatusRunning
2022-05-30 08:35:10 +00:00
s . OnSuspend ( func ( ) {
2022-06-20 05:39:07 +00:00
_ = s . orderExecutor . GracefulCancel ( ctx )
2022-10-03 10:45:24 +00:00
bbgo . Sync ( ctx , s )
2022-05-30 08:35:10 +00:00
} )
s . OnEmergencyStop ( func ( ) {
2022-06-20 05:39:07 +00:00
_ = s . orderExecutor . GracefulCancel ( ctx )
2022-05-30 08:35:10 +00:00
// Close 100% position
2022-06-20 05:39:07 +00:00
_ = s . ClosePosition ( ctx , fixedpoint . One )
2022-05-30 08:35:10 +00:00
} )
2022-05-27 06:36:48 +00:00
// Setup indicators
2022-06-01 02:26:04 +00:00
s . setupIndicators ( )
2022-05-27 06:36:48 +00:00
2022-07-05 08:55:48 +00:00
// Exit methods
for _ , method := range s . ExitMethods {
method . Bind ( session , s . orderExecutor )
}
2022-07-08 07:41:28 +00:00
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , s . Interval , func ( kline types . KLine ) {
2022-05-30 08:35:10 +00:00
// StrategyController
if s . Status != types . StrategyStatusRunning {
return
}
2022-07-06 10:11:09 +00:00
closePrice := kline . GetClose ( )
openPrice := kline . GetOpen ( )
closePrice64 := closePrice . Float64 ( )
openPrice64 := openPrice . Float64 ( )
2022-07-05 07:59:35 +00:00
// Supertrend signal
2022-06-02 05:32:57 +00:00
stSignal := s . Supertrend . GetSignal ( )
2022-07-05 07:59:35 +00:00
// DEMA signal
2022-07-11 05:37:01 +00:00
demaSignal := s . doubleDema . getDemaSignal ( openPrice64 , closePrice64 )
2022-05-27 06:36:48 +00:00
2022-07-05 07:59:35 +00:00
// Linear Regression signal
var lgSignal types . Direction
if s . LinearRegression != nil {
2022-07-06 08:26:30 +00:00
lgSignal = s . LinearRegression . GetSignal ( )
2022-07-05 07:59:35 +00:00
}
// TP/SL if there's non-dust position and meets the criteria
2022-07-06 10:11:09 +00:00
if ! s . Market . IsDustQuantity ( s . Position . GetBase ( ) . Abs ( ) , closePrice ) && s . shouldStop ( kline , stSignal , demaSignal , lgSignal ) {
2022-07-06 08:26:30 +00:00
if err := s . ClosePosition ( ctx , fixedpoint . One ) ; err == nil {
s . currentStopLossPrice = fixedpoint . Zero
s . currentTakeProfitPrice = fixedpoint . Zero
2022-06-30 08:35:00 +00:00
}
}
2022-07-06 10:11:09 +00:00
// Get order side
side := s . getSide ( stSignal , demaSignal , lgSignal )
// Set TP/SL price if needed
if side == types . SideTypeBuy {
2022-05-31 04:53:14 +00:00
if s . StopLossByTriggeringK {
s . currentStopLossPrice = kline . GetLow ( )
}
2022-07-05 07:59:35 +00:00
if s . TakeProfitAtrMultiplier > 0 {
2023-05-31 11:35:44 +00:00
s . currentTakeProfitPrice = closePrice . Add ( fixedpoint . NewFromFloat ( s . Supertrend . AverageTrueRange . Last ( 0 ) * s . TakeProfitAtrMultiplier ) )
2022-05-31 04:53:14 +00:00
}
2022-07-06 10:11:09 +00:00
} else if side == types . SideTypeSell {
2022-05-31 04:53:14 +00:00
if s . StopLossByTriggeringK {
s . currentStopLossPrice = kline . GetHigh ( )
}
2022-07-05 07:59:35 +00:00
if s . TakeProfitAtrMultiplier > 0 {
2023-05-31 11:35:44 +00:00
s . currentTakeProfitPrice = closePrice . Sub ( fixedpoint . NewFromFloat ( s . Supertrend . AverageTrueRange . Last ( 0 ) * s . TakeProfitAtrMultiplier ) )
2022-05-31 04:53:14 +00:00
}
2022-05-27 06:36:48 +00:00
}
2022-07-06 10:11:09 +00:00
// Open position
2022-06-17 02:15:54 +00:00
// The default value of side is an empty string. Unless side is set by the checks above, the result of the following condition is false
2022-05-27 06:36:48 +00:00
if side == types . SideTypeSell || side == types . SideTypeBuy {
2022-06-19 04:29:36 +00:00
bbgo . Notify ( "open %s position for signal %v" , s . Symbol , side )
2023-04-20 09:53:20 +00:00
amount := s . calculateQuantity ( ctx , closePrice , side )
// Add opposite position amount if any
if ( side == types . SideTypeSell && s . Position . IsLong ( ) ) || ( side == types . SideTypeBuy && s . Position . IsShort ( ) ) {
2023-04-20 10:37:48 +00:00
if bbgo . IsBackTesting {
_ = s . ClosePosition ( ctx , fixedpoint . One )
bbgo . Notify ( "close existing %s position before open a new position" , s . Symbol )
amount = s . calculateQuantity ( ctx , closePrice , side )
} else {
bbgo . Notify ( "add existing opposite position amount %f of %s to the amount %f of open new position order" , s . Position . GetQuantity ( ) . Float64 ( ) , s . Symbol , amount . Float64 ( ) )
amount = amount . Add ( s . Position . GetQuantity ( ) )
}
2023-04-20 09:53:20 +00:00
} else if ! s . Position . IsDust ( closePrice ) {
bbgo . Notify ( "existing %s position has the same direction as the signal" , s . Symbol )
return
2022-05-31 04:53:14 +00:00
}
2023-04-20 09:53:20 +00:00
orderForm := s . generateOrderForm ( side , amount , types . SideEffectTypeMarginBuy )
2022-05-30 08:07:36 +00:00
log . Infof ( "submit open position order %v" , orderForm )
2022-06-20 05:39:07 +00:00
_ , err := s . orderExecutor . SubmitOrders ( ctx , orderForm )
2022-05-27 06:36:48 +00:00
if err != nil {
2022-06-16 09:14:50 +00:00
log . WithError ( err ) . Errorf ( "can not place %s open position order" , s . Symbol )
2022-06-19 04:29:36 +00:00
bbgo . Notify ( "can not place %s open position order" , s . Symbol )
2022-05-27 06:36:48 +00:00
}
}
2022-07-08 07:41:28 +00:00
} ) )
2022-05-27 06:36:48 +00:00
2022-05-30 08:48:07 +00:00
// Graceful shutdown
2022-10-03 08:01:08 +00:00
bbgo . OnShutdown ( ctx , func ( ctx context . Context , wg * sync . WaitGroup ) {
2022-05-30 08:48:07 +00:00
defer wg . Done ( )
2022-08-23 10:43:13 +00:00
// Output accumulated profit report
if bbgo . IsBackTesting {
defer s . AccumulatedProfitReport . Output ( s . Symbol )
2022-09-10 23:19:23 +00:00
if s . DrawGraph {
2022-09-11 07:48:08 +00:00
if err := s . Draw ( & profitSlice , & cumProfitSlice ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot draw graph" )
}
2022-09-10 23:19:23 +00:00
}
2022-08-23 10:43:13 +00:00
}
2022-08-03 06:04:30 +00:00
2022-06-20 05:39:07 +00:00
_ = s . orderExecutor . GracefulCancel ( ctx )
2022-07-05 08:30:13 +00:00
_ , _ = fmt . Fprintln ( os . Stderr , s . TradeStats . String ( ) )
2022-05-30 08:48:07 +00:00
} )
2022-05-27 06:36:48 +00:00
return nil
}