2022-07-01 09:22:09 +00:00
package pivotshort
import (
"context"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
2022-07-27 04:04:54 +00:00
type FakeBreakStop struct {
2022-07-27 03:47:12 +00:00
types . IntervalWindow
}
2022-07-01 09:22:09 +00:00
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
type BreakLow struct {
Symbol string
Market types . Market
types . IntervalWindow
2022-09-12 15:26:40 +00:00
// FastWindow is used for fast pivot (this is to to filter the nearest high/low)
FastWindow int ` json:"fastWindow" `
2022-07-01 09:22:09 +00:00
// 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-12 15:24:37 +00:00
bbgo . OpenPositionOptions
2022-07-01 09:22:09 +00:00
// BounceRatio is a ratio used for placing the limit order sell price
// limit sell price = breakLowPrice * (1 + BounceRatio)
BounceRatio fixedpoint . Value ` json:"bounceRatio" `
2022-08-26 09:51:43 +00:00
StopEMA * bbgo . StopEMA ` json:"stopEMA" `
2022-07-26 17:53:53 +00:00
2022-08-26 09:51:43 +00:00
TrendEMA * bbgo . TrendEMA ` json:"trendEMA" `
2022-07-01 09:22:09 +00:00
2022-07-27 04:04:54 +00:00
FakeBreakStop * FakeBreakStop ` json:"fakeBreakStop" `
2022-07-27 03:47:12 +00:00
2022-08-31 08:42:45 +00:00
lastLow , lastFastLow fixedpoint . Value
2022-07-27 03:47:12 +00:00
2022-09-14 10:20:02 +00:00
lastLowInvalidated bool
2022-07-27 03:47:12 +00:00
// lastBreakLow is the low that the price just break
lastBreakLow fixedpoint . Value
2022-08-31 08:42:45 +00:00
pivotLow , fastPivotLow * indicator . PivotLow
pivotLowPrices [ ] fixedpoint . Value
2022-07-13 02:49:52 +00:00
2022-07-01 09:22:09 +00:00
orderExecutor * bbgo . GeneralOrderExecutor
session * bbgo . ExchangeSession
2022-09-11 06:09:46 +00:00
// StrategyController
bbgo . StrategyController
2022-07-01 09:22:09 +00:00
}
2022-07-02 05:21:27 +00:00
func ( s * BreakLow ) Subscribe ( session * bbgo . ExchangeSession ) {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . Interval } )
2022-07-12 09:13:32 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : types . Interval1m } )
2022-07-13 02:49:52 +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-07-27 03:47:12 +00:00
2022-07-27 04:04:54 +00:00
if s . FakeBreakStop != nil {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . FakeBreakStop . Interval } )
2022-07-27 03:47:12 +00:00
}
2022-07-02 05:21:27 +00:00
}
2022-07-01 09:22:09 +00:00
func ( s * BreakLow ) Bind ( session * bbgo . ExchangeSession , orderExecutor * bbgo . GeneralOrderExecutor ) {
2022-09-12 15:26:40 +00:00
if s . FastWindow == 0 {
s . FastWindow = 3
}
2022-07-01 09:22:09 +00:00
s . session = session
s . orderExecutor = orderExecutor
2022-09-11 06:09:46 +00:00
// StrategyController
s . Status = types . StrategyStatusRunning
2022-07-01 09:22:09 +00:00
position := orderExecutor . Position ( )
symbol := position . Symbol
2022-07-26 10:35:50 +00:00
standardIndicator := session . StandardIndicatorSet ( s . Symbol )
2022-07-01 09:22:09 +00:00
s . lastLow = fixedpoint . Zero
2022-07-26 17:57:28 +00:00
s . pivotLow = standardIndicator . PivotLow ( s . IntervalWindow )
2022-08-31 08:42:45 +00:00
s . fastPivotLow = standardIndicator . PivotLow ( types . IntervalWindow {
Interval : s . Interval ,
2022-09-12 15:26:40 +00:00
Window : s . FastWindow , // make it faster
2022-08-31 08:42:45 +00:00
} )
2022-07-01 09:22:09 +00:00
if s . StopEMA != nil {
2022-08-26 09:51:43 +00:00
s . StopEMA . Bind ( session , orderExecutor )
2022-07-01 09:22:09 +00:00
}
2022-07-13 03:09:57 +00:00
if s . TrendEMA != nil {
2022-07-27 07:17:28 +00:00
s . TrendEMA . Bind ( session , orderExecutor )
2022-07-13 03:09:57 +00:00
}
2022-07-01 09:22:09 +00:00
// update pivot low data
2022-07-14 10:35:58 +00:00
session . MarketDataStream . OnStart ( func ( ) {
2022-07-26 17:57:28 +00:00
if s . updatePivotLow ( ) {
2023-05-31 11:35:44 +00:00
bbgo . Notify ( "%s new pivot low: %f" , s . Symbol , s . pivotLow . Last ( 0 ) )
2022-07-14 10:35:58 +00:00
}
2022-07-26 17:57:28 +00:00
s . pilotQuantityCalculation ( )
2022-07-14 10:35:58 +00:00
} )
2022-07-01 09:22:09 +00:00
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( symbol , s . Interval , func ( kline types . KLine ) {
2022-07-26 17:57:28 +00:00
if s . updatePivotLow ( ) {
// when position is opened, do not send pivot low notify
if position . IsOpened ( kline . Close ) {
return
}
2022-07-26 08:50:45 +00:00
2023-05-31 11:35:44 +00:00
bbgo . Notify ( "%s new pivot low: %f" , s . Symbol , s . pivotLow . Last ( 0 ) )
2022-07-26 08:50:45 +00:00
}
2022-07-01 09:22:09 +00:00
} ) )
2022-07-27 04:04:54 +00:00
if s . FakeBreakStop != nil {
2022-07-27 03:47:12 +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-07-27 04:04:54 +00:00
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , s . FakeBreakStop . Interval , func ( k types . KLine ) {
2022-07-27 03:47:12 +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 . lastBreakLow . IsZero ( ) {
return
}
// the kline opened below the last break low, and closed above the last break low
if k . Open . Compare ( s . lastBreakLow ) < 0 && k . Close . Compare ( s . lastBreakLow ) > 0 {
bbgo . Notify ( "kLine closed above the last break low, triggering stop earlier" )
2022-07-28 01:29:10 +00:00
if err := s . orderExecutor . ClosePosition ( context . Background ( ) , one , "fakeBreakStop" ) ; err != nil {
2022-07-27 03:47:12 +00:00
log . WithError ( err ) . Error ( "position close error" )
}
// reset to zero
s . lastBreakLow = fixedpoint . Zero
}
} ) )
}
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , types . Interval1m , func ( kline types . KLine ) {
2022-08-31 08:42:45 +00:00
if len ( s . pivotLowPrices ) == 0 || s . lastLow . IsZero ( ) {
2022-07-01 10:10:39 +00:00
log . Infof ( "currently there is no pivot low prices, can not check break low..." )
2022-07-01 09:22:09 +00:00
return
}
2022-09-14 10:20:02 +00:00
if s . lastLowInvalidated {
log . Infof ( "the last low is invalidated, skip" )
return
}
2022-08-31 08:42:45 +00:00
previousLow := s . lastLow
2022-07-01 09:22:09 +00:00
ratio := fixedpoint . One . Add ( s . Ratio )
breakPrice := previousLow . Mul ( ratio )
2022-09-11 06:09:46 +00:00
// StrategyController
if s . Status != types . StrategyStatusRunning {
return
}
2022-07-01 09:22:09 +00:00
openPrice := kline . Open
closePrice := kline . Close
2022-07-01 09:43:51 +00:00
2022-07-03 18:20:15 +00:00
// if the previous low is not break, or the kline is not strong enough to break it, skip
2022-07-01 09:22:09 +00:00
if closePrice . Compare ( breakPrice ) >= 0 {
return
}
2022-07-03 18:20:15 +00:00
// we need the price cross the break line, or we do nothing:
2022-09-14 11:08:21 +00:00
// 1) open > break price > close price
// 2) high > break price > open price and close price
// v2
if ! ( ( openPrice . Compare ( breakPrice ) > 0 && closePrice . Compare ( breakPrice ) < 0 ) ||
( kline . High . Compare ( breakPrice ) > 0 && openPrice . Compare ( breakPrice ) < 0 && closePrice . Compare ( breakPrice ) < 0 ) ) {
2022-07-01 09:22:09 +00:00
return
}
2022-07-01 09:26:45 +00:00
// force direction to be down
2022-07-01 09:43:51 +00:00
if closePrice . Compare ( openPrice ) >= 0 {
2022-07-30 10:14:53 +00:00
bbgo . Notify ( "%s price %f is closed higher than the open price %f, skip this break" , kline . Symbol , closePrice . Float64 ( ) , openPrice . Float64 ( ) )
2022-07-01 09:26:45 +00:00
// skip UP klines
return
}
2022-09-14 11:08:54 +00:00
bbgo . Notify ( "%s breakLow signal detected, closed price %f < breakPrice %f" , kline . Symbol , closePrice . Float64 ( ) , breakPrice . Float64 ( ) , kline )
2022-07-01 09:22:09 +00:00
2022-07-27 03:47:12 +00:00
if s . lastBreakLow . IsZero ( ) || previousLow . Compare ( s . lastBreakLow ) < 0 {
s . lastBreakLow = previousLow
}
2022-07-13 03:09:57 +00:00
if position . IsOpened ( kline . Close ) {
2022-09-16 03:18:11 +00:00
bbgo . Notify ( "%s position is already opened, skip" , s . Symbol )
2022-07-13 03:09:57 +00:00
return
}
// trend EMA protection
2022-07-28 03:29:27 +00:00
if s . TrendEMA != nil && ! s . TrendEMA . GradientAllowed ( ) {
2022-09-16 03:18:11 +00:00
bbgo . Notify ( "trendEMA protection: %s close price %f, gradient %f" , s . Symbol , kline . Close . Float64 ( ) , s . TrendEMA . Gradient ( ) )
2022-07-28 02:27:16 +00:00
return
2022-07-13 03:09:57 +00:00
}
2022-07-01 09:22:09 +00:00
// stop EMA protection
2022-08-26 09:51:43 +00:00
if s . StopEMA != nil {
if ! s . StopEMA . Allowed ( closePrice ) {
2022-07-01 09:22:09 +00:00
return
}
}
ctx := context . Background ( )
// graceful cancel all active orders
_ = orderExecutor . GracefulCancel ( ctx )
2022-09-12 15:24:37 +00:00
bbgo . Notify ( "%s price %f breaks the previous low %f with ratio %f, opening short position" , symbol , kline . Close . Float64 ( ) , previousLow . Float64 ( ) , s . Ratio . Float64 ( ) )
opts := s . OpenPositionOptions
opts . Short = true
opts . Price = closePrice
opts . Tags = [ ] string { "breakLowMarket" }
if opts . LimitOrder && ! s . BounceRatio . IsZero ( ) {
opts . Price = previousLow . Mul ( fixedpoint . One . Add ( s . BounceRatio ) )
2022-07-14 09:36:03 +00:00
}
2022-09-22 04:01:26 +00:00
if _ , err := s . orderExecutor . OpenPosition ( ctx , opts ) ; err != nil {
2022-09-12 15:24:37 +00:00
log . WithError ( err ) . Errorf ( "failed to open short position" )
2022-07-01 09:22:09 +00:00
}
} ) )
}
2022-07-26 17:57:28 +00:00
func ( s * BreakLow ) pilotQuantityCalculation ( ) {
2022-09-13 19:10:48 +00:00
if s . lastLow . IsZero ( ) {
return
}
2022-07-26 17:57:28 +00:00
log . Infof ( "pilot calculation for max position: last low = %f, quantity = %f, leverage = %f" ,
s . lastLow . Float64 ( ) ,
s . Quantity . Float64 ( ) ,
s . Leverage . Float64 ( ) )
2022-09-09 09:40:17 +00:00
quantity , err := bbgo . CalculateBaseQuantity ( s . session , s . Market , s . lastLow , s . Quantity , s . Leverage )
2022-07-26 17:57:28 +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 shorting" , s . Symbol , quantity . Float64 ( ) )
}
func ( s * BreakLow ) updatePivotLow ( ) bool {
2023-05-31 11:35:44 +00:00
low := fixedpoint . NewFromFloat ( s . pivotLow . Last ( 0 ) )
2022-09-12 15:32:53 +00:00
if low . IsZero ( ) {
2022-07-26 17:57:28 +00:00
return false
}
2022-09-14 10:20:02 +00:00
// if the last low is different
2022-09-12 15:32:53 +00:00
lastLowChanged := low . Compare ( s . lastLow ) != 0
2022-08-31 08:42:45 +00:00
if lastLowChanged {
2022-09-14 10:20:02 +00:00
s . lastLow = low
s . lastLowInvalidated = false
s . pivotLowPrices = append ( s . pivotLowPrices , low )
2022-08-31 08:42:45 +00:00
}
2023-05-31 11:35:44 +00:00
fastLow := fixedpoint . NewFromFloat ( s . fastPivotLow . Last ( 0 ) )
2022-09-12 15:32:53 +00:00
if ! fastLow . IsZero ( ) {
if fastLow . Compare ( s . lastLow ) < 0 {
2022-09-14 10:20:02 +00:00
s . lastLowInvalidated = true
2022-08-31 08:42:45 +00:00
lastLowChanged = false
}
2022-09-12 15:32:53 +00:00
s . lastFastLow = fastLow
2022-08-31 08:42:45 +00:00
}
return lastLowChanged
2022-07-26 17:57:28 +00:00
}