2022-08-30 16:35:38 +00:00
package pivotshort
import (
"context"
"github.com/c9s/bbgo/pkg/bbgo"
2022-09-15 17:15:18 +00:00
"github.com/c9s/bbgo/pkg/datatype/floats"
2022-08-30 16:35:38 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
2022-09-15 17:15:18 +00:00
type MACDDivergence struct {
* indicator . MACDConfig
2022-09-15 17:23:15 +00:00
PivotWindow int ` json:"pivotWindow" `
2022-09-15 17:15:18 +00:00
}
2022-08-30 16:35:38 +00:00
// FailedBreakHigh -- when price breaks the previous pivot low, we set a trade entry
type FailedBreakHigh struct {
Symbol string
Market types . Market
2022-08-31 04:59:28 +00:00
// IntervalWindow is used for finding the pivot high
2022-08-30 16:35:38 +00:00
types . IntervalWindow
2022-09-13 05:09:11 +00:00
FastWindow int
2022-09-12 15:38:27 +00:00
bbgo . OpenPositionOptions
2022-08-31 04:59:28 +00:00
// BreakInterval is used for checking failed break
BreakInterval types . Interval ` json:"breakInterval" `
2022-08-30 16:35:38 +00:00
Enabled bool ` json:"enabled" `
// Ratio is a number less than 1.0, price * ratio will be the price triggers the short order.
Ratio fixedpoint . Value ` json:"ratio" `
2022-09-13 10:43:03 +00:00
// EarlyStopRatio adjusts the break high price with the given ratio
// this is for stop loss earlier if the price goes above the previous price
EarlyStopRatio fixedpoint . Value ` json:"earlyStopRatio" `
2022-08-30 17:23:08 +00:00
VWMA * types . IntervalWindow ` json:"vwma" `
2022-08-30 16:35:38 +00:00
StopEMA * bbgo . StopEMA ` json:"stopEMA" `
TrendEMA * bbgo . TrendEMA ` json:"trendEMA" `
2022-09-15 17:15:18 +00:00
MACDDivergence * MACDDivergence ` json:"macdDivergence" `
macd * indicator . MACD
2022-09-15 17:23:15 +00:00
2022-09-15 17:15:18 +00:00
macdTopDivergence bool
2022-09-14 10:41:11 +00:00
2022-08-31 09:04:46 +00:00
lastFailedBreakHigh , lastHigh , lastFastHigh fixedpoint . Value
2022-09-14 10:20:02 +00:00
lastHighInvalidated bool
pivotHighPrices [ ] fixedpoint . Value
2022-08-30 16:35:38 +00:00
2022-08-31 09:04:46 +00:00
pivotHigh , fastPivotHigh * indicator . PivotHigh
vwma * indicator . VWMA
2022-08-30 16:35:38 +00:00
orderExecutor * bbgo . GeneralOrderExecutor
session * bbgo . ExchangeSession
2022-09-11 06:09:46 +00:00
// StrategyController
bbgo . StrategyController
2022-08-30 16:35:38 +00:00
}
func ( s * FailedBreakHigh ) Subscribe ( session * bbgo . ExchangeSession ) {
2022-08-31 04:59:28 +00:00
if s . BreakInterval == "" {
s . BreakInterval = types . Interval1m
}
2022-08-30 16:35:38 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . Interval } )
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : types . Interval1m } )
2022-08-31 04:59:28 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . BreakInterval } )
2022-08-30 16:35:38 +00:00
if s . StopEMA != nil {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . StopEMA . Interval } )
}
if s . TrendEMA != nil {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . TrendEMA . Interval } )
}
2022-09-14 10:41:11 +00:00
2022-09-15 17:15:18 +00:00
if s . MACDDivergence != nil {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . MACDDivergence . Interval } )
2022-09-14 10:41:11 +00:00
}
2022-08-30 16:35:38 +00:00
}
func ( s * FailedBreakHigh ) Bind ( session * bbgo . ExchangeSession , orderExecutor * bbgo . GeneralOrderExecutor ) {
s . session = session
s . orderExecutor = orderExecutor
if ! s . Enabled {
return
}
2022-09-14 10:41:11 +00:00
// set default value for StrategyController
s . Status = types . StrategyStatusRunning
2022-09-13 05:09:11 +00:00
if s . FastWindow == 0 {
s . FastWindow = 3
}
2022-08-30 16:35:38 +00:00
position := orderExecutor . Position ( )
symbol := position . Symbol
standardIndicator := session . StandardIndicatorSet ( s . Symbol )
s . lastHigh = fixedpoint . Zero
s . pivotHigh = standardIndicator . PivotHigh ( s . IntervalWindow )
2022-08-31 09:04:46 +00:00
s . fastPivotHigh = standardIndicator . PivotHigh ( types . IntervalWindow {
Interval : s . IntervalWindow . Interval ,
2022-09-13 05:09:11 +00:00
Window : s . FastWindow ,
2022-08-31 09:04:46 +00:00
} )
2022-08-30 16:35:38 +00:00
2022-09-15 17:15:18 +00:00
// Experimental: MACD divergence detection
if s . MACDDivergence != nil {
log . Infof ( "MACD divergence detection is enabled" )
s . macd = standardIndicator . MACD ( s . MACDDivergence . IntervalWindow , s . MACDDivergence . ShortPeriod , s . MACDDivergence . LongPeriod )
2022-09-15 09:53:12 +00:00
s . macd . OnUpdate ( func ( macd , signal , histogram float64 ) {
log . Infof ( "MACD %+v: macd: %f, signal: %f histogram: %f" , s . macd . IntervalWindow , macd , signal , histogram )
2022-09-15 17:15:18 +00:00
s . detectMacdDivergence ( )
2022-09-14 10:41:11 +00:00
} )
2022-09-15 17:15:18 +00:00
s . detectMacdDivergence ( )
2022-09-14 10:41:11 +00:00
}
2022-09-11 06:09:46 +00:00
2022-08-30 17:23:08 +00:00
if s . VWMA != nil {
2022-08-30 17:29:27 +00:00
s . vwma = standardIndicator . VWMA ( types . IntervalWindow {
2022-08-31 04:59:28 +00:00
Interval : s . BreakInterval ,
2022-08-30 17:29:27 +00:00
Window : s . VWMA . Window ,
} )
2022-08-30 17:23:08 +00:00
}
2022-08-30 16:35:38 +00:00
if s . StopEMA != nil {
s . StopEMA . Bind ( session , orderExecutor )
}
if s . TrendEMA != nil {
s . TrendEMA . Bind ( session , orderExecutor )
}
// update pivot low data
session . MarketDataStream . OnStart ( func ( ) {
if s . updatePivotHigh ( ) {
bbgo . Notify ( "%s new pivot high: %f" , s . Symbol , s . pivotHigh . Last ( ) )
}
s . pilotQuantityCalculation ( )
} )
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( symbol , s . Interval , func ( kline types . KLine ) {
if s . updatePivotHigh ( ) {
// when position is opened, do not send pivot low notify
if position . IsOpened ( kline . Close ) {
return
}
2022-09-13 10:43:03 +00:00
bbgo . Notify ( "%s new pivot high: %f" , s . Symbol , s . pivotHigh . Last ( ) )
2022-08-30 16:35:38 +00:00
}
} ) )
// if the position is already opened, and we just break the low, this checks if the kline closed above the low,
// so that we can close the position earlier
2022-08-31 04:59:28 +00:00
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , s . BreakInterval , func ( k types . KLine ) {
2022-08-30 16:35:38 +00:00
if ! s . Enabled {
return
}
2022-09-11 06:09:46 +00:00
// StrategyController
if s . Status != types . StrategyStatusRunning {
return
}
2022-08-30 16:35:38 +00:00
// make sure the position is opened, and it's a short position
if ! position . IsOpened ( k . Close ) || ! position . IsShort ( ) {
return
}
// make sure we recorded the last break low
if s . lastFailedBreakHigh . IsZero ( ) {
return
}
2022-09-13 10:43:03 +00:00
lastHigh := s . lastFastHigh
if ! s . EarlyStopRatio . IsZero ( ) {
lastHigh = lastHigh . Mul ( one . Add ( s . EarlyStopRatio ) )
}
2022-08-30 16:35:38 +00:00
// the kline opened below the last break low, and closed above the last break low
2022-09-13 10:43:03 +00:00
if k . Open . Compare ( lastHigh ) < 0 && k . Close . Compare ( lastHigh ) > 0 && k . Open . Compare ( k . Close ) > 0 {
bbgo . Notify ( "kLine closed %f above the last break high %f (ratio %f), triggering stop earlier" , k . Close . Float64 ( ) , lastHigh . Float64 ( ) , s . EarlyStopRatio . Float64 ( ) )
2022-09-13 05:36:07 +00:00
2022-08-30 17:23:08 +00:00
if err := s . orderExecutor . ClosePosition ( context . Background ( ) , one , "failedBreakHighStop" ) ; err != nil {
2022-08-30 16:35:38 +00:00
log . WithError ( err ) . Error ( "position close error" )
}
// reset to zero
s . lastFailedBreakHigh = fixedpoint . Zero
}
} ) )
2022-08-31 04:59:28 +00:00
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , s . BreakInterval , func ( kline types . KLine ) {
2022-08-31 09:04:46 +00:00
if len ( s . pivotHighPrices ) == 0 || s . lastHigh . IsZero ( ) {
2022-09-14 10:20:02 +00:00
log . Infof ( "%s currently there is no pivot high prices, can not check failed break high..." , s . Symbol )
return
}
if s . lastHighInvalidated {
2022-09-15 17:15:18 +00:00
log . Infof ( "%s last high %f is invalidated by the fast pivot" , s . Symbol , s . lastHigh . Float64 ( ) )
2022-08-30 16:35:38 +00:00
return
}
2022-09-11 06:09:46 +00:00
// StrategyController
if s . Status != types . StrategyStatusRunning {
return
}
2022-08-30 16:35:38 +00:00
previousHigh := s . lastHigh
ratio := fixedpoint . One . Add ( s . Ratio )
breakPrice := previousHigh . Mul ( ratio )
openPrice := kline . Open
closePrice := kline . Close
// we need few conditions:
// 1) kline.High is higher than the previous high
// 2) kline.Close is lower than the previous high
if kline . High . Compare ( breakPrice ) < 0 || closePrice . Compare ( breakPrice ) >= 0 {
return
}
2022-09-13 05:57:49 +00:00
// 3) kline.Close is lower than kline.Open
2022-08-30 16:35:38 +00:00
if closePrice . Compare ( openPrice ) > 0 {
2022-09-13 05:57:49 +00:00
bbgo . Notify ( "the %s closed price %f is higher than the open price %f, skip failed break high short" , s . Symbol , closePrice . Float64 ( ) , openPrice . Float64 ( ) )
2022-08-30 16:35:38 +00:00
return
}
2022-08-30 17:23:08 +00:00
if s . vwma != nil {
vma := fixedpoint . NewFromFloat ( s . vwma . Last ( ) )
if kline . Volume . Compare ( vma ) < 0 {
bbgo . Notify ( "%s %s kline volume %f is less than VMA %f, skip failed break high short" , kline . Symbol , kline . Interval , kline . Volume . Float64 ( ) , vma . Float64 ( ) )
return
}
}
2022-08-30 16:35:38 +00:00
bbgo . Notify ( "%s FailedBreakHigh signal detected, closed price %f < breakPrice %f" , kline . Symbol , closePrice . Float64 ( ) , breakPrice . Float64 ( ) )
if position . IsOpened ( kline . Close ) {
2022-09-15 17:15:18 +00:00
bbgo . Notify ( "%s position is already opened, skip" , s . Symbol )
2022-08-30 16:35:38 +00:00
return
}
// trend EMA protection
if s . TrendEMA != nil && ! s . TrendEMA . GradientAllowed ( ) {
bbgo . Notify ( "trendEMA protection: close price %f, gradient %f" , kline . Close . Float64 ( ) , s . TrendEMA . Gradient ( ) )
return
}
// stop EMA protection
if s . StopEMA != nil {
if ! s . StopEMA . Allowed ( closePrice ) {
2022-09-08 15:17:35 +00:00
bbgo . Notify ( "stopEMA protection: close price %f %s" , kline . Close . Float64 ( ) , s . StopEMA . String ( ) )
2022-08-30 16:35:38 +00:00
return
}
}
2022-09-15 17:15:18 +00:00
if s . macd != nil && ! s . macdTopDivergence {
bbgo . Notify ( "Detected MACD top divergence" )
return
}
if s . lastFailedBreakHigh . IsZero ( ) || previousHigh . Compare ( s . lastFailedBreakHigh ) < 0 {
s . lastFailedBreakHigh = previousHigh
}
2022-08-30 16:35:38 +00:00
ctx := context . Background ( )
2022-09-12 15:38:27 +00:00
bbgo . Notify ( "%s price %f failed breaking the previous high %f with ratio %f, opening short position" ,
symbol ,
kline . Close . Float64 ( ) ,
previousHigh . Float64 ( ) ,
s . Ratio . Float64 ( ) )
2022-08-30 16:35:38 +00:00
// graceful cancel all active orders
_ = orderExecutor . GracefulCancel ( ctx )
2022-09-12 15:38:27 +00:00
opts := s . OpenPositionOptions
opts . Short = true
opts . Price = closePrice
opts . Tags = [ ] string { "FailedBreakHighMarket" }
2022-09-22 04:01:26 +00:00
if _ , err := s . orderExecutor . OpenPosition ( ctx , opts ) ; err != nil {
2022-09-12 15:38:27 +00:00
log . WithError ( err ) . Errorf ( "failed to open short position" )
2022-08-30 16:35:38 +00:00
}
} ) )
}
func ( s * FailedBreakHigh ) pilotQuantityCalculation ( ) {
2022-09-13 19:10:48 +00:00
if s . lastHigh . IsZero ( ) {
return
}
2022-08-30 16:35:38 +00:00
log . Infof ( "pilot calculation for max position: last low = %f, quantity = %f, leverage = %f" ,
s . lastHigh . Float64 ( ) ,
s . Quantity . Float64 ( ) ,
s . Leverage . Float64 ( ) )
2022-09-09 09:40:17 +00:00
quantity , err := bbgo . CalculateBaseQuantity ( s . session , s . Market , s . lastHigh , s . Quantity , s . Leverage )
2022-08-30 16:35:38 +00:00
if err != nil {
log . WithError ( err ) . Errorf ( "quantity calculation error" )
}
if quantity . IsZero ( ) {
log . WithError ( err ) . Errorf ( "quantity is zero, can not submit order" )
return
}
bbgo . Notify ( "%s %f quantity will be used for failed break high short" , s . Symbol , quantity . Float64 ( ) )
}
2022-09-15 17:15:18 +00:00
func ( s * FailedBreakHigh ) detectMacdDivergence ( ) {
2022-09-15 17:23:15 +00:00
if s . MACDDivergence == nil {
return
}
2022-09-15 17:24:01 +00:00
// always reset the top divergence to false
2022-09-15 17:15:18 +00:00
s . macdTopDivergence = false
histogramValues := s . macd . Histogram
2022-09-15 17:23:15 +00:00
pivotWindow := s . MACDDivergence . PivotWindow
if pivotWindow == 0 {
pivotWindow = 3
}
2022-09-15 17:15:18 +00:00
if len ( histogramValues ) < pivotWindow * 2 {
log . Warnf ( "histogram values is not enough for finding pivots, length=%d" , len ( histogramValues ) )
return
}
var histogramPivots floats . Slice
for i := pivotWindow ; i > 0 && i < len ( histogramValues ) ; i ++ {
// find positive histogram and the top
pivot , ok := floats . CalculatePivot ( histogramValues [ 0 : i ] , pivotWindow , pivotWindow , func ( a , pivot float64 ) bool {
return pivot > 0 && pivot > a
} )
if ok {
histogramPivots = append ( histogramPivots , pivot )
}
}
log . Infof ( "histogram pivots: %+v" , histogramPivots )
// take the last 2-3 pivots to check if there is a divergence
if len ( histogramPivots ) < 3 {
return
}
histogramPivots = histogramPivots [ len ( histogramPivots ) - 3 : ]
minDiff := 0.01
for i := len ( histogramPivots ) - 1 ; i > 0 ; i -- {
p1 := histogramPivots [ i ]
p2 := histogramPivots [ i - 1 ]
diff := p1 - p2
if diff > - minDiff || diff > minDiff {
continue
}
// negative value = MACD top divergence
if diff < - minDiff {
log . Infof ( "MACD TOP DIVERGENCE DETECTED: diff %f" , diff )
s . macdTopDivergence = true
} else {
s . macdTopDivergence = false
}
return
}
}
2022-08-30 16:35:38 +00:00
func ( s * FailedBreakHigh ) updatePivotHigh ( ) bool {
2022-09-13 03:57:38 +00:00
high := fixedpoint . NewFromFloat ( s . pivotHigh . Last ( ) )
if high . IsZero ( ) {
2022-08-30 16:35:38 +00:00
return false
}
2022-09-13 03:57:38 +00:00
lastHighChanged := high . Compare ( s . lastHigh ) != 0
2022-08-31 09:04:46 +00:00
if lastHighChanged {
2022-09-14 10:20:02 +00:00
s . lastHigh = high
s . lastHighInvalidated = false
s . pivotHighPrices = append ( s . pivotHighPrices , high )
2022-08-31 09:04:46 +00:00
}
2022-09-13 03:57:38 +00:00
fastHigh := fixedpoint . NewFromFloat ( s . fastPivotHigh . Last ( ) )
if ! fastHigh . IsZero ( ) {
if fastHigh . Compare ( s . lastHigh ) > 0 {
2022-08-31 09:04:46 +00:00
// invalidate the last low
lastHighChanged = false
2022-09-14 10:20:02 +00:00
s . lastHighInvalidated = true
2022-08-31 09:04:46 +00:00
}
2022-09-13 03:57:38 +00:00
s . lastFastHigh = fastHigh
2022-08-31 09:04:46 +00:00
}
return lastHighChanged
2022-08-30 16:35:38 +00:00
}