2022-06-30 17:24:34 +00:00
package pivotshort
import (
"context"
"github.com/c9s/bbgo/pkg/bbgo"
2022-08-26 08:15:39 +00:00
"github.com/c9s/bbgo/pkg/datatype/floats"
2022-06-30 17:24:34 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
type ResistanceShort struct {
Enabled bool ` json:"enabled" `
Symbol string ` json:"-" `
Market types . Market ` json:"-" `
types . IntervalWindow
2022-07-02 10:51:17 +00:00
MinDistance fixedpoint . Value ` json:"minDistance" `
GroupDistance fixedpoint . Value ` json:"groupDistance" `
NumLayers int ` json:"numLayers" `
LayerSpread fixedpoint . Value ` json:"layerSpread" `
Quantity fixedpoint . Value ` json:"quantity" `
2022-07-27 04:46:25 +00:00
Leverage fixedpoint . Value ` json:"leverage" `
2022-07-02 10:51:17 +00:00
Ratio fixedpoint . Value ` json:"ratio" `
2022-06-30 17:24:34 +00:00
2022-08-26 09:51:43 +00:00
TrendEMA * bbgo . TrendEMA ` json:"trendEMA" `
2022-07-27 07:17:28 +00:00
2022-06-30 17:24:34 +00:00
session * bbgo . ExchangeSession
orderExecutor * bbgo . GeneralOrderExecutor
2022-07-26 10:59:00 +00:00
resistancePivot * indicator . PivotLow
2022-07-03 07:26:05 +00:00
resistancePrices [ ] float64
currentResistancePrice fixedpoint . Value
2022-06-30 17:24:34 +00:00
activeOrders * bbgo . ActiveOrderBook
}
2022-07-27 07:17:28 +00:00
func ( s * ResistanceShort ) Subscribe ( session * bbgo . ExchangeSession ) {
2022-08-26 09:55:59 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . Interval } )
2022-07-27 07:17:28 +00:00
if s . TrendEMA != nil {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . TrendEMA . Interval } )
}
}
2022-06-30 17:24:34 +00:00
func ( s * ResistanceShort ) Bind ( session * bbgo . ExchangeSession , orderExecutor * bbgo . GeneralOrderExecutor ) {
2022-07-27 07:17:28 +00:00
if s . GroupDistance . IsZero ( ) {
s . GroupDistance = fixedpoint . NewFromFloat ( 0.01 )
}
2022-06-30 17:24:34 +00:00
s . session = session
s . orderExecutor = orderExecutor
s . activeOrders = bbgo . NewActiveOrderBook ( s . Symbol )
2022-07-14 08:28:30 +00:00
s . activeOrders . OnFilled ( func ( o types . Order ) {
// reset resistance price
s . currentResistancePrice = fixedpoint . Zero
} )
2022-06-30 17:24:34 +00:00
s . activeOrders . BindStream ( session . UserDataStream )
2022-07-27 07:17:28 +00:00
if s . TrendEMA != nil {
s . TrendEMA . Bind ( session , orderExecutor )
2022-07-02 10:51:17 +00:00
}
2022-07-26 10:59:00 +00:00
s . resistancePivot = session . StandardIndicatorSet ( s . Symbol ) . PivotLow ( s . IntervalWindow )
2022-06-30 17:24:34 +00:00
// use the last kline from the history before we get the next closed kline
2022-07-27 03:30:32 +00:00
s . updateResistanceOrders ( fixedpoint . NewFromFloat ( s . resistancePivot . Last ( ) ) )
2022-06-30 17:24:34 +00:00
session . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , s . Interval , func ( kline types . KLine ) {
2022-07-27 07:17:28 +00:00
// trend EMA protection
2022-07-28 03:29:27 +00:00
if s . TrendEMA != nil && ! s . TrendEMA . GradientAllowed ( ) {
2022-07-28 02:27:16 +00:00
return
2022-07-27 07:17:28 +00:00
}
2022-06-30 17:24:34 +00:00
position := s . orderExecutor . Position ( )
if position . IsOpened ( kline . Close ) {
return
}
2022-07-12 09:41:29 +00:00
s . updateResistanceOrders ( kline . Close )
2022-06-30 17:24:34 +00:00
} ) )
}
2022-07-27 05:54:19 +00:00
// updateCurrentResistancePrice updates the current resistance price
2022-07-12 09:41:29 +00:00
// we should only update the resistance price when:
// 1) the close price is already above the current resistance price by (1 + minDistance)
// 2) the next resistance price is lower than the current resistance price.
func ( s * ResistanceShort ) updateCurrentResistancePrice ( closePrice fixedpoint . Value ) bool {
2022-06-30 17:24:34 +00:00
minDistance := s . MinDistance . Float64 ( )
2022-07-03 07:26:05 +00:00
groupDistance := s . GroupDistance . Float64 ( )
2022-07-27 03:30:32 +00:00
resistancePrices := findPossibleResistancePrices ( closePrice . Float64 ( ) * ( 1.0 + minDistance ) , groupDistance , s . resistancePivot . Values . Tail ( 6 ) )
2022-07-03 07:26:05 +00:00
if len ( resistancePrices ) == 0 {
return false
}
log . Infof ( "%s close price: %f, min distance: %f, possible resistance prices: %+v" , s . Symbol , closePrice . Float64 ( ) , minDistance , resistancePrices )
nextResistancePrice := fixedpoint . NewFromFloat ( resistancePrices [ 0 ] )
if s . currentResistancePrice . IsZero ( ) {
s . currentResistancePrice = nextResistancePrice
return true
}
2022-07-03 09:13:01 +00:00
// if the current sell price is out-dated
// or
// the next resistance is lower than the current one.
2022-07-12 09:41:29 +00:00
minPriceToUpdate := s . currentResistancePrice . Mul ( one . Add ( s . MinDistance ) )
if closePrice . Compare ( minPriceToUpdate ) > 0 || nextResistancePrice . Compare ( s . currentResistancePrice ) < 0 {
2022-07-03 07:26:05 +00:00
s . currentResistancePrice = nextResistancePrice
return true
}
2022-06-30 17:24:34 +00:00
2022-07-03 07:26:05 +00:00
return false
}
2022-06-30 17:24:34 +00:00
2022-07-12 09:41:29 +00:00
func ( s * ResistanceShort ) updateResistanceOrders ( closePrice fixedpoint . Value ) {
2022-06-30 17:24:34 +00:00
ctx := context . Background ( )
2022-07-12 09:41:29 +00:00
resistanceUpdated := s . updateCurrentResistancePrice ( closePrice )
2022-07-03 07:26:05 +00:00
if resistanceUpdated {
2022-07-14 08:34:03 +00:00
s . placeResistanceOrders ( ctx , s . currentResistancePrice )
} else if s . activeOrders . NumOfOrders ( ) == 0 && ! s . currentResistancePrice . IsZero ( ) {
2022-07-03 07:26:05 +00:00
s . placeResistanceOrders ( ctx , s . currentResistancePrice )
2022-06-30 17:24:34 +00:00
}
}
func ( s * ResistanceShort ) placeResistanceOrders ( ctx context . Context , resistancePrice fixedpoint . Value ) {
2022-09-09 09:40:17 +00:00
totalQuantity , err := bbgo . CalculateBaseQuantity ( s . session , s . Market , resistancePrice , s . Quantity , s . Leverage )
2022-07-27 04:46:25 +00:00
if err != nil {
log . WithError ( err ) . Errorf ( "quantity calculation error" )
}
if totalQuantity . IsZero ( ) {
return
}
2022-06-30 17:24:34 +00:00
2022-07-27 05:01:19 +00:00
bbgo . Notify ( "Next %s resistance price at %f, updating resistance orders with total quantity %f" , s . Symbol , s . currentResistancePrice . Float64 ( ) , totalQuantity . Float64 ( ) )
2022-07-27 04:51:04 +00:00
2022-06-30 17:24:34 +00:00
numLayers := s . NumLayers
if numLayers == 0 {
numLayers = 1
}
numLayersF := fixedpoint . NewFromInt ( int64 ( numLayers ) )
layerSpread := s . LayerSpread
quantity := totalQuantity . Div ( numLayersF )
if s . activeOrders . NumOfOrders ( ) > 0 {
if err := s . orderExecutor . GracefulCancelActiveOrderBook ( ctx , s . activeOrders ) ; err != nil {
log . WithError ( err ) . Errorf ( "can not cancel resistance orders: %+v" , s . activeOrders . Orders ( ) )
}
}
log . Infof ( "placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d" , resistancePrice . Float64 ( ) , quantity . Float64 ( ) , numLayers )
2022-07-03 07:44:37 +00:00
var sellPriceStart = resistancePrice . Mul ( fixedpoint . One . Add ( s . Ratio ) )
2022-06-30 17:24:34 +00:00
var orderForms [ ] types . SubmitOrder
for i := 0 ; i < numLayers ; i ++ {
balances := s . session . GetAccount ( ) . Balances ( )
quoteBalance := balances [ s . Market . QuoteCurrency ]
baseBalance := balances [ s . Market . BaseCurrency ]
_ = quoteBalance
_ = baseBalance
spread := layerSpread . Mul ( fixedpoint . NewFromInt ( int64 ( i ) ) )
2022-07-03 07:44:37 +00:00
price := sellPriceStart . Mul ( one . Add ( spread ) )
2022-07-12 09:41:29 +00:00
log . Infof ( "resistance sell price = %f" , price . Float64 ( ) )
2022-07-03 07:44:37 +00:00
log . Infof ( "placing resistance short order #%d: price = %f, quantity = %f" , i , price . Float64 ( ) , quantity . Float64 ( ) )
2022-06-30 17:24:34 +00:00
orderForms = append ( orderForms , types . SubmitOrder {
2022-07-01 09:22:09 +00:00
Symbol : s . Symbol ,
Side : types . SideTypeSell ,
Type : types . OrderTypeLimitMaker ,
Price : price ,
Quantity : quantity ,
Tag : "resistanceShort" ,
2022-07-01 07:34:21 +00:00
MarginSideEffect : types . SideEffectTypeMarginBuy ,
2022-06-30 17:24:34 +00:00
} )
}
createdOrders , err := s . orderExecutor . SubmitOrders ( ctx , orderForms ... )
if err != nil {
log . WithError ( err ) . Errorf ( "can not place resistance order" )
}
s . activeOrders . Add ( createdOrders ... )
}
2022-07-01 09:22:09 +00:00
2022-07-03 18:20:15 +00:00
func findPossibleSupportPrices ( closePrice float64 , groupDistance float64 , lows [ ] float64 ) [ ] float64 {
2022-08-26 08:15:39 +00:00
return floats . Group ( floats . Lower ( lows , closePrice ) , groupDistance )
2022-07-02 10:51:17 +00:00
}
2022-07-01 09:22:09 +00:00
2022-07-03 18:20:15 +00:00
func findPossibleResistancePrices ( closePrice float64 , groupDistance float64 , lows [ ] float64 ) [ ] float64 {
2022-08-26 08:15:39 +00:00
return floats . Group ( floats . Higher ( lows , closePrice ) , groupDistance )
2022-07-02 10:51:17 +00:00
}