2022-05-10 09:11:24 +00:00
package pivotshort
2022-05-09 21:11:22 +00:00
import (
"context"
"fmt"
2022-06-09 16:49:32 +00:00
"sync"
2022-06-09 03:30:24 +00:00
2022-06-04 17:09:31 +00:00
"github.com/sirupsen/logrus"
2022-06-09 16:49:32 +00:00
"gopkg.in/yaml.v3"
2022-06-04 17:09:31 +00:00
2022-05-09 21:11:22 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
2022-05-13 10:05:25 +00:00
"github.com/c9s/bbgo/pkg/indicator"
2022-05-09 21:11:22 +00:00
"github.com/c9s/bbgo/pkg/types"
)
2022-06-09 16:49:32 +00:00
type TradeStats struct {
WinningRatio fixedpoint . Value ` json:"winningRatio" yaml:"winningRatio" `
NumOfLossTrade int ` json:"numOfLossTrade" yaml:"numOfLossTrade" `
NumOfProfitTrade int ` json:"numOfProfitTrade" yaml:"numOfProfitTrade" `
GrossProfit fixedpoint . Value ` json:"grossProfit" yaml:"grossProfit" `
GrossLoss fixedpoint . Value ` json:"grossLoss" yaml:"grossLoss" `
Profits [ ] fixedpoint . Value ` json:"profits" yaml:"profits" `
Losses [ ] fixedpoint . Value ` json:"losses" yaml:"losses" `
MostProfitableTrade fixedpoint . Value ` json:"mostProfitableTrade" yaml:"mostProfitableTrade" `
MostLossTrade fixedpoint . Value ` json:"mostLossTrade" yaml:"mostLossTrade" `
}
func ( s * TradeStats ) Add ( pnl fixedpoint . Value ) {
if pnl . Sign ( ) > 0 {
s . NumOfProfitTrade ++
s . Profits = append ( s . Profits , pnl )
s . GrossProfit = s . GrossProfit . Add ( pnl )
s . MostProfitableTrade = fixedpoint . Max ( s . MostProfitableTrade , pnl )
} else {
s . NumOfLossTrade ++
s . Losses = append ( s . Losses , pnl )
s . GrossLoss = s . GrossLoss . Add ( pnl )
s . MostLossTrade = fixedpoint . Min ( s . MostLossTrade , pnl )
}
s . WinningRatio = fixedpoint . NewFromFloat ( float64 ( s . NumOfProfitTrade ) / float64 ( s . NumOfLossTrade ) )
}
func ( s * TradeStats ) String ( ) string {
out , _ := yaml . Marshal ( s )
return string ( out )
}
2022-05-10 09:11:24 +00:00
const ID = "pivotshort"
2022-05-09 21:11:22 +00:00
var log = logrus . WithField ( "strategy" , ID )
func init ( ) {
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
type IntervalWindowSetting struct {
types . IntervalWindow
}
2022-06-09 09:36:22 +00:00
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
type BreakLow struct {
2022-06-09 10:16:32 +00:00
Ratio fixedpoint . Value ` json:"ratio" `
Quantity fixedpoint . Value ` json:"quantity" `
StopEMARange fixedpoint . Value ` json:"stopEMARange" `
StopEMA * types . IntervalWindow ` json:"stopEMA" `
2022-06-09 09:36:22 +00:00
}
2022-06-03 08:38:06 +00:00
type Entry struct {
2022-06-09 03:30:24 +00:00
CatBounceRatio fixedpoint . Value ` json:"catBounceRatio" `
NumLayers int ` json:"numLayers" `
TotalQuantity fixedpoint . Value ` json:"totalQuantity" `
2022-06-03 08:38:06 +00:00
Quantity fixedpoint . Value ` json:"quantity" `
MarginSideEffect types . MarginOrderSideEffectType ` json:"marginOrderSideEffect" `
}
type Exit struct {
2022-06-09 09:36:22 +00:00
RoiStopLossPercentage fixedpoint . Value ` json:"roiStopLossPercentage" `
RoiTakeProfitPercentage fixedpoint . Value ` json:"roiTakeProfitPercentage" `
LowerShadowRatio fixedpoint . Value ` json:"lowerShadowRatio" `
MarginSideEffect types . MarginOrderSideEffectType ` json:"marginOrderSideEffect" `
2022-06-03 08:38:06 +00:00
}
2022-05-09 21:11:22 +00:00
type Strategy struct {
2022-05-12 11:27:57 +00:00
* bbgo . Graceful
* bbgo . Notifiability
* bbgo . Persistence
2022-06-05 23:29:25 +00:00
Environment * bbgo . Environment
Symbol string ` json:"symbol" `
Market types . Market
Interval types . Interval ` json:"interval" `
2022-05-12 11:27:57 +00:00
// persistence fields
Position * types . Position ` json:"position,omitempty" persistence:"position" `
ProfitStats * types . ProfitStats ` json:"profitStats,omitempty" persistence:"profit_stats" `
2022-06-09 16:49:32 +00:00
TradeStats * TradeStats ` persistence:"trade_stats" `
2022-05-09 21:11:22 +00:00
2022-06-03 08:38:06 +00:00
PivotLength int ` json:"pivotLength" `
2022-06-09 09:36:22 +00:00
BreakLow BreakLow ` json:"breakLow" `
Entry Entry ` json:"entry" `
Exit Exit ` json:"exit" `
2022-05-09 21:11:22 +00:00
2022-06-05 21:43:38 +00:00
activeMakerOrders * bbgo . ActiveOrderBook
2022-05-09 21:11:22 +00:00
orderStore * bbgo . OrderStore
tradeCollector * bbgo . TradeCollector
session * bbgo . ExchangeSession
2022-06-09 09:36:22 +00:00
lastLow fixedpoint . Value
2022-06-05 04:56:40 +00:00
pivot * indicator . Pivot
2022-06-09 10:16:32 +00:00
ewma * indicator . EWMA
2022-06-09 03:30:24 +00:00
pivotLowPrices [ ] fixedpoint . Value
2022-05-12 11:27:57 +00:00
// StrategyController
bbgo . StrategyController
2022-05-09 21:11:22 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) Subscribe ( session * bbgo . ExchangeSession ) {
log . Infof ( "subscribe %s" , s . Symbol )
2022-05-19 01:48:36 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . Interval } )
2022-06-05 23:29:25 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : types . Interval1m } )
}
func ( s * Strategy ) submitOrders ( ctx context . Context , orderExecutor bbgo . OrderExecutor , submitOrders ... types . SubmitOrder ) {
createdOrders , err := orderExecutor . SubmitOrders ( ctx , submitOrders ... )
if err != nil {
log . WithError ( err ) . Errorf ( "can not place orders" )
}
s . orderStore . Add ( createdOrders ... )
s . activeMakerOrders . Add ( createdOrders ... )
s . tradeCollector . Process ( )
2022-05-09 21:11:22 +00:00
}
2022-06-09 09:36:22 +00:00
func ( s * Strategy ) placeMarketSell ( ctx context . Context , orderExecutor bbgo . OrderExecutor , quantity fixedpoint . Value ) {
2022-06-05 04:47:15 +00:00
if quantity . IsZero ( ) {
if balance , ok := s . session . Account . Balance ( s . Market . BaseCurrency ) ; ok {
2022-06-05 04:58:12 +00:00
s . Notify ( "sell quantity is not set, submitting sell with all base balance: %s" , balance . Available . String ( ) )
2022-06-05 04:47:15 +00:00
quantity = balance . Available
}
}
2022-06-05 04:58:12 +00:00
if quantity . IsZero ( ) {
log . Errorf ( "quantity is zero, can not submit sell order, please check settings" )
return
}
2022-06-05 04:47:15 +00:00
submitOrder := types . SubmitOrder {
Symbol : s . Symbol ,
Side : types . SideTypeSell ,
Type : types . OrderTypeMarket ,
Quantity : quantity ,
2022-06-09 09:36:22 +00:00
MarginSideEffect : types . SideEffectTypeMarginBuy ,
2022-06-05 04:47:15 +00:00
}
s . submitOrders ( ctx , orderExecutor , submitOrder )
}
2022-05-09 21:11:22 +00:00
func ( s * Strategy ) ClosePosition ( ctx context . Context , percentage fixedpoint . Value ) error {
2022-06-09 09:36:22 +00:00
submitOrder := s . Position . NewMarketCloseOrder ( percentage ) // types.SubmitOrder{
2022-06-09 04:25:36 +00:00
if submitOrder == nil {
return nil
}
2022-05-09 21:11:22 +00:00
2022-06-02 13:42:05 +00:00
if s . session . Margin {
2022-06-03 08:38:06 +00:00
submitOrder . MarginSideEffect = s . Exit . MarginSideEffect
2022-06-02 13:42:05 +00:00
}
2022-05-09 21:11:22 +00:00
2022-06-09 09:36:22 +00:00
s . Notify ( "Closing %s position by %f" , s . Symbol , percentage . Float64 ( ) )
2022-05-09 21:11:22 +00:00
2022-06-05 23:29:25 +00:00
createdOrders , err := s . session . Exchange . SubmitOrders ( ctx , * submitOrder )
2022-05-09 21:11:22 +00:00
if err != nil {
log . WithError ( err ) . Errorf ( "can not place position close order" )
}
s . orderStore . Add ( createdOrders ... )
s . activeMakerOrders . Add ( createdOrders ... )
2022-06-05 04:51:45 +00:00
s . tradeCollector . Process ( )
2022-05-09 21:11:22 +00:00
return err
}
2022-06-09 16:49:32 +00:00
2022-05-12 11:27:57 +00:00
func ( s * Strategy ) InstanceID ( ) string {
return fmt . Sprintf ( "%s:%s" , ID , s . Symbol )
}
2022-05-09 21:11:22 +00:00
func ( s * Strategy ) Run ( ctx context . Context , orderExecutor bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
// initial required information
s . session = session
2022-06-05 22:57:25 +00:00
s . activeMakerOrders = bbgo . NewActiveOrderBook ( s . Symbol )
2022-05-09 21:11:22 +00:00
s . activeMakerOrders . BindStream ( session . UserDataStream )
s . orderStore = bbgo . NewOrderStore ( s . Symbol )
s . orderStore . BindStream ( session . UserDataStream )
if s . Position == nil {
s . Position = types . NewPositionFromMarket ( s . Market )
}
2022-06-04 17:48:56 +00:00
if s . ProfitStats == nil {
s . ProfitStats = types . NewProfitStats ( s . Market )
}
2022-06-09 16:49:32 +00:00
if s . TradeStats == nil {
s . TradeStats = & TradeStats { }
}
2022-05-12 11:27:57 +00:00
instanceID := s . InstanceID ( )
// Always update the position fields
s . Position . Strategy = ID
s . Position . StrategyInstanceID = instanceID
2022-05-09 21:11:22 +00:00
s . tradeCollector = bbgo . NewTradeCollector ( s . Symbol , s . Position , s . orderStore )
2022-05-12 11:27:57 +00:00
s . tradeCollector . OnTrade ( func ( trade types . Trade , profit , netProfit fixedpoint . Value ) {
s . Notifiability . Notify ( trade )
s . ProfitStats . AddTrade ( trade )
if profit . Compare ( fixedpoint . Zero ) == 0 {
s . Environment . RecordPosition ( s . Position , trade , nil )
} else {
log . Infof ( "%s generated profit: %v" , s . Symbol , profit )
2022-06-09 09:36:22 +00:00
2022-05-12 11:27:57 +00:00
p := s . Position . NewProfit ( trade , profit , netProfit )
p . Strategy = ID
p . StrategyInstanceID = instanceID
s . Notify ( & p )
s . ProfitStats . AddProfit ( p )
s . Notify ( & s . ProfitStats )
2022-06-09 16:49:32 +00:00
s . TradeStats . Add ( profit )
2022-05-12 11:27:57 +00:00
s . Environment . RecordPosition ( s . Position , trade , & p )
}
} )
s . tradeCollector . OnPositionUpdate ( func ( position * types . Position ) {
log . Infof ( "position changed: %s" , s . Position )
s . Notify ( s . Position )
} )
2022-05-09 21:11:22 +00:00
s . tradeCollector . BindStream ( session . UserDataStream )
2022-05-11 12:55:11 +00:00
iw := types . IntervalWindow { Window : s . PivotLength , Interval : s . Interval }
2022-06-09 04:34:12 +00:00
store , _ := session . MarketDataStore ( s . Symbol )
2022-05-13 10:05:25 +00:00
s . pivot = & indicator . Pivot { IntervalWindow : iw }
2022-06-09 04:34:12 +00:00
s . pivot . Bind ( store )
2022-05-09 21:11:22 +00:00
2022-06-09 10:16:32 +00:00
standardIndicator , _ := session . StandardIndicatorSet ( s . Symbol )
if s . BreakLow . StopEMA != nil {
s . ewma = standardIndicator . EWMA ( * s . BreakLow . StopEMA )
}
2022-06-09 09:36:22 +00:00
s . lastLow = fixedpoint . Zero
2022-06-03 18:17:58 +00:00
2022-05-09 21:11:22 +00:00
session . UserDataStream . OnStart ( func ( ) {
2022-06-09 03:30:24 +00:00
/ *
if price , ok := session . LastPrice ( s . Symbol ) ; ok {
if limitPrice , ok := s . findHigherPivotLow ( price ) ; ok {
log . Infof ( "%s placing limit sell start from %f adds up to %f percent with %d layers of orders" , s . Symbol , limitPrice . Float64 ( ) , s . Entry . CatBounceRatio . Mul ( fixedpoint . NewFromInt ( 100 ) ) . Float64 ( ) , s . Entry . NumLayers )
s . placeBounceSellOrders ( ctx , limitPrice , price , orderExecutor )
}
}
* /
2022-05-09 21:11:22 +00:00
} )
2022-06-05 23:29:25 +00:00
// Always check whether you can open a short position or not
session . MarketDataStream . OnKLineClosed ( func ( kline types . KLine ) {
if kline . Symbol != s . Symbol || kline . Interval != types . Interval1m {
return
}
2022-06-09 03:46:14 +00:00
2022-06-09 09:36:22 +00:00
isPositionOpened := ! s . Position . IsClosed ( ) && ! s . Position . IsDust ( kline . Close )
if isPositionOpened && s . Position . IsShort ( ) {
2022-06-05 23:29:25 +00:00
// calculate return rate
2022-06-09 09:36:22 +00:00
// TODO: apply quantity to this formula
2022-06-09 16:49:32 +00:00
roi := s . Position . AverageCost . Sub ( kline . Close ) . Div ( s . Position . AverageCost )
if roi . Compare ( s . Exit . RoiStopLossPercentage . Neg ( ) ) < 0 {
2022-06-05 23:29:25 +00:00
// SL
2022-06-09 16:49:32 +00:00
s . Notify ( "%s ROI StopLoss triggered at price %f, ROI = %s" , s . Symbol , kline . Close . Float64 ( ) , roi . Percentage ( ) )
2022-06-09 05:26:30 +00:00
if err := s . activeMakerOrders . GracefulCancel ( ctx , s . session . Exchange ) ; err != nil {
log . WithError ( err ) . Errorf ( "graceful cancel order error" )
}
2022-06-09 09:36:22 +00:00
if err := s . ClosePosition ( ctx , fixedpoint . One ) ; err != nil {
log . WithError ( err ) . Errorf ( "close position error" )
}
2022-06-09 04:25:36 +00:00
return
2022-06-09 17:21:59 +00:00
} else if roi . Compare ( s . Exit . RoiTakeProfitPercentage ) > 0 { // disable this condition temporarily
2022-06-09 16:49:32 +00:00
s . Notify ( "%s TakeProfit triggered at price %f, ROI take profit percentage by %s" , s . Symbol , kline . Close . Float64 ( ) , roi . Percentage ( ) , kline )
2022-06-09 05:26:30 +00:00
if err := s . activeMakerOrders . GracefulCancel ( ctx , s . session . Exchange ) ; err != nil {
log . WithError ( err ) . Errorf ( "graceful cancel order error" )
}
2022-06-05 23:29:25 +00:00
2022-06-09 09:36:22 +00:00
if err := s . ClosePosition ( ctx , fixedpoint . One ) ; err != nil {
log . WithError ( err ) . Errorf ( "close position error" )
2022-06-09 04:25:36 +00:00
}
2022-06-09 17:21:59 +00:00
} else if ! s . Exit . LowerShadowRatio . IsZero ( ) && kline . GetLowerShadowHeight ( ) . Div ( kline . Close ) . Compare ( s . Exit . LowerShadowRatio ) > 0 {
2022-06-09 09:36:22 +00:00
s . Notify ( "%s TakeProfit triggered at price %f: shadow ratio %f" , s . Symbol , kline . Close . Float64 ( ) , kline . GetLowerShadowRatio ( ) . Float64 ( ) , kline )
2022-06-05 23:29:25 +00:00
if err := s . activeMakerOrders . GracefulCancel ( ctx , s . session . Exchange ) ; err != nil {
log . WithError ( err ) . Errorf ( "graceful cancel order error" )
}
2022-06-09 04:25:36 +00:00
2022-06-09 09:36:22 +00:00
if err := s . ClosePosition ( ctx , fixedpoint . One ) ; err != nil {
log . WithError ( err ) . Errorf ( "close position error" )
}
return
2022-06-05 23:29:25 +00:00
}
2022-06-05 04:55:36 +00:00
}
2022-06-09 09:36:22 +00:00
if len ( s . pivotLowPrices ) == 0 {
return
}
previousLow := s . pivotLowPrices [ len ( s . pivotLowPrices ) - 1 ]
// truncate the pivot low prices
if len ( s . pivotLowPrices ) > 10 {
s . pivotLowPrices = s . pivotLowPrices [ len ( s . pivotLowPrices ) - 10 : ]
}
2022-06-09 10:16:32 +00:00
if s . ewma != nil && ! s . BreakLow . StopEMARange . IsZero ( ) {
ema := fixedpoint . NewFromFloat ( s . ewma . Last ( ) )
if ema . IsZero ( ) {
return
}
emaStopShortPrice := ema . Mul ( fixedpoint . One . Sub ( s . BreakLow . StopEMARange ) )
if kline . Close . Compare ( emaStopShortPrice ) < 0 {
return
}
}
2022-06-09 09:36:22 +00:00
ratio := fixedpoint . One . Sub ( s . BreakLow . Ratio )
breakPrice := previousLow . Mul ( ratio )
if kline . Close . Compare ( breakPrice ) > 0 {
return
}
if ! s . Position . IsClosed ( ) && ! s . Position . IsDust ( kline . Close ) {
// s.Notify("skip opening %s position, which is not closed", s.Symbol, s.Position)
return
}
s . Notify ( "%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position" , s . Symbol , kline . Close . Float64 ( ) , previousLow . Float64 ( ) , s . BreakLow . Ratio . Float64 ( ) )
if err := s . activeMakerOrders . GracefulCancel ( ctx , s . session . Exchange ) ; err != nil {
log . WithError ( err ) . Errorf ( "graceful cancel order error" )
}
s . placeMarketSell ( ctx , orderExecutor , s . BreakLow . Quantity )
2022-06-05 04:48:54 +00:00
} )
2022-05-09 21:11:22 +00:00
session . MarketDataStream . OnKLineClosed ( func ( kline types . KLine ) {
if kline . Symbol != s . Symbol || kline . Interval != s . Interval {
return
}
2022-06-02 13:34:26 +00:00
2022-06-09 03:30:24 +00:00
if s . pivot . LastLow ( ) > 0.0 {
2022-06-09 09:36:22 +00:00
log . Infof ( "pivot low detected: %f %s" , s . pivot . LastLow ( ) , kline . EndTime . Time ( ) )
lastLow := fixedpoint . NewFromFloat ( s . pivot . LastLow ( ) )
if lastLow . Compare ( s . lastLow ) != 0 {
s . lastLow = lastLow
s . pivotLowPrices = append ( s . pivotLowPrices , s . lastLow )
}
2022-05-09 21:11:22 +00:00
}
} )
2022-06-09 16:49:32 +00:00
s . Graceful . OnShutdown ( func ( ctx context . Context , wg * sync . WaitGroup ) {
log . Info ( s . TradeStats . String ( ) )
wg . Done ( )
} )
2022-05-09 21:11:22 +00:00
return nil
}
2022-06-09 03:30:24 +00:00
func ( s * Strategy ) findHigherPivotLow ( price fixedpoint . Value ) ( fixedpoint . Value , bool ) {
for l := len ( s . pivotLowPrices ) - 1 ; l > 0 ; l -- {
if s . pivotLowPrices [ l ] . Compare ( price ) > 0 {
return s . pivotLowPrices [ l ] , true
}
}
return price , false
}
func ( s * Strategy ) placeBounceSellOrders ( ctx context . Context , lastLow fixedpoint . Value , limitPrice fixedpoint . Value , currentPrice fixedpoint . Value , orderExecutor bbgo . OrderExecutor ) {
futuresMode := s . session . Futures || s . session . IsolatedFutures
numLayers := fixedpoint . NewFromInt ( int64 ( s . Entry . NumLayers ) )
d := s . Entry . CatBounceRatio . Div ( numLayers )
q := s . Entry . Quantity
if ! s . Entry . TotalQuantity . IsZero ( ) {
q = s . Entry . TotalQuantity . Div ( numLayers )
}
for i := 0 ; i < s . Entry . NumLayers ; i ++ {
balances := s . session . GetAccount ( ) . Balances ( )
quoteBalance , _ := balances [ s . Market . QuoteCurrency ]
baseBalance , _ := balances [ s . Market . BaseCurrency ]
p := limitPrice . Mul ( fixedpoint . One . Add ( s . Entry . CatBounceRatio . Sub ( fixedpoint . NewFromFloat ( d . Float64 ( ) * float64 ( i ) ) ) ) )
if futuresMode {
if q . Mul ( p ) . Compare ( quoteBalance . Available ) <= 0 {
s . placeOrder ( ctx , lastLow , p , currentPrice , q , orderExecutor )
}
} else if s . Environment . IsBackTesting ( ) {
if q . Compare ( baseBalance . Available ) <= 0 {
s . placeOrder ( ctx , lastLow , p , currentPrice , q , orderExecutor )
}
} else {
if q . Compare ( baseBalance . Available ) <= 0 {
s . placeOrder ( ctx , lastLow , p , currentPrice , q , orderExecutor )
}
}
}
}
func ( s * Strategy ) placeOrder ( ctx context . Context , lastLow fixedpoint . Value , limitPrice fixedpoint . Value , currentPrice fixedpoint . Value , qty fixedpoint . Value , orderExecutor bbgo . OrderExecutor ) {
submitOrder := types . SubmitOrder {
Symbol : s . Symbol ,
Side : types . SideTypeSell ,
Type : types . OrderTypeLimit ,
Price : limitPrice ,
Quantity : qty ,
}
2022-06-09 09:36:22 +00:00
if ! lastLow . IsZero ( ) && lastLow . Compare ( currentPrice ) <= 0 {
2022-06-09 03:30:24 +00:00
submitOrder . Type = types . OrderTypeMarket
}
s . submitOrders ( ctx , orderExecutor , submitOrder )
}