2022-07-01 09:22:09 +00:00
package pivotshort
import (
"context"
2022-07-14 09:36:03 +00:00
"fmt"
2022-07-01 09:22:09 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
2022-07-14 09:36:03 +00:00
"github.com/c9s/bbgo/pkg/risk"
2022-07-01 09:22:09 +00:00
"github.com/c9s/bbgo/pkg/types"
)
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
type BreakLow struct {
Symbol string
Market types . Market
types . IntervalWindow
// Ratio is a number less than 1.0, price * ratio will be the price triggers the short order.
Ratio fixedpoint . Value ` json:"ratio" `
// MarketOrder is the option to enable market order short.
MarketOrder bool ` json:"marketOrder" `
// BounceRatio is a ratio used for placing the limit order sell price
// limit sell price = breakLowPrice * (1 + BounceRatio)
BounceRatio fixedpoint . Value ` json:"bounceRatio" `
2022-07-14 09:44:25 +00:00
Leverage fixedpoint . Value ` json:"leverage" `
2022-07-01 09:22:09 +00:00
Quantity fixedpoint . Value ` json:"quantity" `
StopEMARange fixedpoint . Value ` json:"stopEMARange" `
StopEMA * types . IntervalWindow ` json:"stopEMA" `
2022-07-13 02:49:52 +00:00
TrendEMA * types . IntervalWindow ` json:"trendEMA" `
2022-07-13 03:09:57 +00:00
lastLow fixedpoint . Value
pivot * indicator . Pivot
stopEWMA * indicator . EWMA
trendEWMA * indicator . EWMA
trendEWMALast , trendEWMACurrent float64
2022-07-01 09:22:09 +00:00
pivotLowPrices [ ] fixedpoint . Value
orderExecutor * bbgo . GeneralOrderExecutor
session * bbgo . ExchangeSession
}
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-02 05:21:27 +00:00
}
2022-07-01 09:22:09 +00:00
func ( s * BreakLow ) Bind ( session * bbgo . ExchangeSession , orderExecutor * bbgo . GeneralOrderExecutor ) {
s . session = session
s . orderExecutor = orderExecutor
position := orderExecutor . Position ( )
symbol := position . Symbol
2022-07-14 08:28:30 +00:00
store , _ := session . MarketDataStore ( s . Symbol )
standardIndicator , _ := session . StandardIndicatorSet ( s . Symbol )
2022-07-01 09:22:09 +00:00
s . lastLow = fixedpoint . Zero
s . pivot = & indicator . Pivot { IntervalWindow : s . IntervalWindow }
s . pivot . Bind ( store )
preloadPivot ( s . pivot , store )
if s . StopEMA != nil {
s . stopEWMA = standardIndicator . EWMA ( * s . StopEMA )
}
2022-07-13 03:09:57 +00:00
if s . TrendEMA != nil {
s . trendEWMA = standardIndicator . EWMA ( * s . TrendEMA )
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , s . TrendEMA . Interval , func ( kline types . KLine ) {
s . trendEWMALast = s . trendEWMACurrent
s . trendEWMACurrent = s . trendEWMA . Last ( )
} ) )
}
2022-07-01 09:22:09 +00:00
// update pivot low data
2022-07-14 10:35:58 +00:00
session . MarketDataStream . OnStart ( func ( ) {
lastLow := fixedpoint . NewFromFloat ( s . pivot . LastLow ( ) )
if lastLow . IsZero ( ) {
return
}
if lastLow . Compare ( s . lastLow ) != 0 {
2022-07-21 04:05:05 +00:00
bbgo . Notify ( "%s found new pivot low: %f" , s . Symbol , s . pivot . LastLow ( ) )
2022-07-14 10:35:58 +00:00
}
s . lastLow = lastLow
s . pivotLowPrices = append ( s . pivotLowPrices , s . lastLow )
2022-07-21 04:05:05 +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 ( ) )
quantity , err := useQuantityOrBaseBalance ( s . session , s . Market , s . lastLow , s . Quantity , s . Leverage )
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 ( ) )
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 ) {
lastLow := fixedpoint . NewFromFloat ( s . pivot . LastLow ( ) )
if lastLow . IsZero ( ) {
return
}
if lastLow . Compare ( s . lastLow ) != 0 {
2022-07-21 04:05:05 +00:00
bbgo . Notify ( "%s new pivot low: %f" , s . Symbol , s . pivot . LastLow ( ) )
2022-07-01 09:22:09 +00:00
}
s . lastLow = lastLow
s . pivotLowPrices = append ( s . pivotLowPrices , s . lastLow )
} ) )
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( symbol , types . Interval1m , func ( kline types . KLine ) {
if len ( s . pivotLowPrices ) == 0 {
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
}
previousLow := s . pivotLowPrices [ len ( s . pivotLowPrices ) - 1 ]
ratio := fixedpoint . One . Add ( s . Ratio )
breakPrice := previousLow . Mul ( ratio )
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:
// open > break price > close price
2022-07-01 09:22:09 +00:00
if ! ( openPrice . Compare ( breakPrice ) > 0 && closePrice . Compare ( breakPrice ) < 0 ) {
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-01 09:26:45 +00:00
log . Infof ( "%s price %f is closed higher than the open price %f, skip this break" , kline . Symbol , closePrice . Float64 ( ) , openPrice . Float64 ( ) )
// skip UP klines
return
}
2022-07-01 09:22:09 +00:00
log . Infof ( "%s breakLow signal detected, closed price %f < breakPrice %f" , kline . Symbol , closePrice . Float64 ( ) , breakPrice . Float64 ( ) )
2022-07-13 03:09:57 +00:00
if position . IsOpened ( kline . Close ) {
log . Infof ( "position is already opened, skip short" )
return
}
// trend EMA protection
if s . trendEWMALast > 0.0 && s . trendEWMACurrent > 0.0 {
slope := s . trendEWMALast / s . trendEWMACurrent
if slope > 1.0 {
log . Infof ( "trendEMA %+v current=%f last=%f slope=%f: skip short" , s . TrendEMA , s . trendEWMACurrent , s . trendEWMALast , slope )
return
}
log . Infof ( "trendEMA %+v current=%f last=%f slope=%f: short is enabled" , s . TrendEMA , s . trendEWMACurrent , s . trendEWMALast , slope )
}
2022-07-01 09:22:09 +00:00
// stop EMA protection
if s . stopEWMA != nil {
ema := fixedpoint . NewFromFloat ( s . stopEWMA . Last ( ) )
if ema . IsZero ( ) {
return
}
emaStopShortPrice := ema . Mul ( fixedpoint . One . Sub ( s . StopEMARange ) )
if closePrice . Compare ( emaStopShortPrice ) < 0 {
log . Infof ( "stopEMA protection: close price %f < EMA(%v) = %f" , closePrice . Float64 ( ) , s . StopEMA , ema . Float64 ( ) )
return
}
}
ctx := context . Background ( )
// graceful cancel all active orders
_ = orderExecutor . GracefulCancel ( ctx )
2022-07-14 09:44:25 +00:00
quantity , err := useQuantityOrBaseBalance ( s . session , s . Market , closePrice , s . Quantity , s . Leverage )
2022-07-14 09:36:03 +00:00
if err != nil {
log . WithError ( err ) . Errorf ( "quantity calculation error" )
}
if quantity . IsZero ( ) {
return
}
2022-07-01 09:22:09 +00:00
if s . MarketOrder {
bbgo . Notify ( "%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position" , symbol , kline . Close . Float64 ( ) , previousLow . Float64 ( ) , s . Ratio . Float64 ( ) )
2022-07-01 09:26:45 +00:00
_ , _ = s . orderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : s . Symbol ,
Side : types . SideTypeSell ,
Type : types . OrderTypeMarket ,
Quantity : quantity ,
MarginSideEffect : types . SideEffectTypeMarginBuy ,
Tag : "breakLowMarket" ,
} )
2022-07-01 09:22:09 +00:00
} else {
sellPrice := previousLow . Mul ( fixedpoint . One . Add ( s . BounceRatio ) )
bbgo . Notify ( "%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f" , symbol , kline . Close . Float64 ( ) , previousLow . Float64 ( ) , s . Ratio . Float64 ( ) , sellPrice . Float64 ( ) )
2022-07-01 09:26:45 +00:00
_ , _ = s . orderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : kline . Symbol ,
Side : types . SideTypeSell ,
Type : types . OrderTypeLimit ,
Price : sellPrice ,
Quantity : quantity ,
MarginSideEffect : types . SideEffectTypeMarginBuy ,
Tag : "breakLowLimit" ,
} )
2022-07-01 09:22:09 +00:00
}
} ) )
}
2022-07-14 09:36:03 +00:00
func useQuantityOrBaseBalance ( session * bbgo . ExchangeSession , market types . Market , price , quantity , leverage fixedpoint . Value ) ( fixedpoint . Value , error ) {
usingLeverage := session . Margin || session . IsolatedMargin || session . Futures || session . IsolatedFutures
if usingLeverage {
if ! quantity . IsZero ( ) {
return quantity , nil
}
2022-07-14 09:44:25 +00:00
if leverage . IsZero ( ) {
leverage = fixedpoint . NewFromInt ( 3 )
}
2022-07-14 09:36:03 +00:00
// quantity is zero, we need to calculate the quantity
baseBalance , _ := session . Account . Balance ( market . BaseCurrency )
quoteBalance , _ := session . Account . Balance ( market . QuoteCurrency )
2022-07-21 04:05:05 +00:00
log . Infof ( "calculating quantity: base balance = %+v, quote balance = %+v" , baseBalance , quoteBalance )
2022-07-14 09:36:03 +00:00
// calculate the quantity automatically
if session . Margin || session . IsolatedMargin {
2022-07-21 04:05:05 +00:00
baseBalanceValue := baseBalance . Net ( ) . Mul ( price )
accountValue := baseBalanceValue . Add ( quoteBalance . Net ( ) )
2022-07-21 05:05:46 +00:00
// avoid using all account value since there will be some trade loss for interests and the fee
2022-07-21 05:17:46 +00:00
accountValue = accountValue . Mul ( one . Sub ( fixedpoint . NewFromFloat ( 0.01 ) ) )
2022-07-21 05:05:46 +00:00
2022-07-21 04:05:05 +00:00
log . Infof ( "calculated account value %f %s" , accountValue . Float64 ( ) , market . QuoteCurrency )
2022-07-14 09:36:03 +00:00
2022-07-14 09:38:11 +00:00
if session . IsolatedMargin {
originLeverage := leverage
2022-07-21 05:04:19 +00:00
leverage = fixedpoint . Min ( leverage , fixedpoint . NewFromInt ( 10 ) ) // max leverage is 10
2022-07-14 09:38:11 +00:00
log . Infof ( "using isolated margin, maxLeverage=10 originalLeverage=%f currentLeverage=%f" ,
originLeverage . Float64 ( ) ,
leverage . Float64 ( ) )
}
2022-07-14 09:36:03 +00:00
// spot margin use the equity value, so we use the total quote balance here
maxPositionQuantity := risk . CalculateMaxPosition ( price , accountValue , leverage )
log . Infof ( "margin leverage: calculated maxPositionQuantity=%f price=%f accountValue=%f %s leverage=%f" ,
maxPositionQuantity . Float64 ( ) ,
price . Float64 ( ) ,
accountValue . Float64 ( ) ,
market . QuoteCurrency ,
leverage . Float64 ( ) )
return maxPositionQuantity , nil
}
if session . Futures || session . IsolatedFutures {
// TODO: get mark price here
maxPositionQuantity := risk . CalculateMaxPosition ( price , quoteBalance . Available , leverage )
requiredPositionCost := risk . CalculatePositionCost ( price , price , maxPositionQuantity , leverage , types . SideTypeSell )
if quoteBalance . Available . Compare ( requiredPositionCost ) < 0 {
return maxPositionQuantity , fmt . Errorf ( "available margin %f %s is not enough, can not submit order" , quoteBalance . Available . Float64 ( ) , market . QuoteCurrency )
}
return maxPositionQuantity , nil
}
2022-07-01 09:22:09 +00:00
}
2022-07-14 09:36:03 +00:00
// For spot, we simply sell the base currency
balance , hasBalance := session . Account . Balance ( market . BaseCurrency )
2022-07-01 09:22:09 +00:00
if hasBalance {
if quantity . IsZero ( ) {
2022-07-14 09:36:03 +00:00
log . Warnf ( "sell quantity is not set, submitting sell with all base balance: %s" , balance . Available . String ( ) )
2022-07-19 03:25:27 +00:00
if ! balance . Available . IsZero ( ) {
return balance . Available , nil
}
2022-07-01 09:22:09 +00:00
} else {
2022-07-19 03:25:27 +00:00
return fixedpoint . Min ( quantity , balance . Available ) , nil
2022-07-01 09:22:09 +00:00
}
}
2022-07-14 09:36:03 +00:00
return quantity , fmt . Errorf ( "quantity is zero, can not submit sell order, please check your settings" )
2022-07-01 09:22:09 +00:00
}