2021-02-14 17:26:46 +00:00
package support
2021-02-10 16:21:06 +00:00
import (
"context"
"fmt"
2021-05-30 17:02:35 +00:00
"sync"
2021-02-10 16:21:06 +00:00
2022-01-08 16:35:45 +00:00
"github.com/sirupsen/logrus"
2021-06-21 11:03:50 +00:00
"github.com/c9s/bbgo/pkg/indicator"
2021-05-30 17:02:35 +00:00
"github.com/c9s/bbgo/pkg/service"
2021-02-10 16:21:06 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
2021-02-14 17:26:46 +00:00
const ID = "support"
2021-02-10 16:21:06 +00:00
2021-05-30 17:02:35 +00:00
const stateKey = "state-v1"
2021-02-10 16:21:06 +00:00
var log = logrus . WithField ( "strategy" , ID )
2021-08-17 03:06:41 +00:00
var zeroiw = types . IntervalWindow { }
2021-02-10 16:21:06 +00:00
func init ( ) {
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
2021-05-30 17:02:35 +00:00
type State struct {
2022-01-24 05:09:12 +00:00
Position * types . Position ` json:"position,omitempty" `
2022-01-28 05:14:58 +00:00
CurrentHighestPrice * fixedpoint . Value ` json:"currentHighestPrice,omitempty" `
2021-05-30 17:02:35 +00:00
}
2021-02-10 16:21:06 +00:00
type Target struct {
2022-02-15 05:55:19 +00:00
ProfitPercentage fixedpoint . Value ` json:"profitPercentage" `
QuantityPercentage fixedpoint . Value ` json:"quantityPercentage" `
2021-02-14 17:26:46 +00:00
MarginOrderSideEffect types . MarginOrderSideEffectType ` json:"marginOrderSideEffect" `
2021-02-10 16:21:06 +00:00
}
2021-10-15 04:38:16 +00:00
// PercentageTargetStop is a kind of stop order by setting fixed percentage target
type PercentageTargetStop struct {
Targets [ ] Target ` json:"targets" `
}
2021-10-15 08:10:57 +00:00
// GenerateOrders generates the orders from the given targets
2021-12-11 11:16:16 +00:00
func ( stop * PercentageTargetStop ) GenerateOrders ( market types . Market , pos * types . Position ) [ ] types . SubmitOrder {
2021-10-15 08:10:57 +00:00
var price = pos . AverageCost
2022-01-08 16:35:45 +00:00
var quantity = pos . GetBase ( )
2021-10-15 08:10:57 +00:00
// submit target orders
var targetOrders [ ] types . SubmitOrder
for _ , target := range stop . Targets {
2022-02-04 03:56:49 +00:00
targetPrice := price . Mul ( fixedpoint . One . Add ( target . ProfitPercentage ) )
targetQuantity := quantity . Mul ( target . QuantityPercentage )
targetQuoteQuantity := targetPrice . Mul ( targetQuantity )
2021-10-15 08:10:57 +00:00
2022-02-04 03:56:49 +00:00
if targetQuoteQuantity . Compare ( market . MinNotional ) <= 0 {
2021-10-15 08:10:57 +00:00
continue
}
2022-02-04 03:56:49 +00:00
if targetQuantity . Compare ( market . MinQuantity ) <= 0 {
2021-10-15 08:10:57 +00:00
continue
}
targetOrders = append ( targetOrders , types . SubmitOrder {
2021-12-11 11:16:16 +00:00
Symbol : market . Symbol ,
Market : market ,
Type : types . OrderTypeLimit ,
Side : types . SideTypeSell ,
Price : targetPrice ,
Quantity : targetQuantity ,
2021-10-15 08:10:57 +00:00
MarginSideEffect : target . MarginOrderSideEffect ,
2022-02-18 05:52:13 +00:00
TimeInForce : types . TimeInForceGTC ,
2021-10-15 08:10:57 +00:00
} )
}
return targetOrders
}
2022-01-12 06:33:31 +00:00
type TrailingStopTarget struct {
2022-01-28 07:47:12 +00:00
TrailingStopCallbackRatio fixedpoint . Value ` json:"callbackRatio" `
2022-01-28 05:06:57 +00:00
MinimumProfitPercentage fixedpoint . Value ` json:"minimumProfitPercentage" `
2022-01-12 06:33:31 +00:00
}
type TrailingStopControl struct {
2022-01-24 03:34:57 +00:00
symbol string
market types . Market
2022-01-12 06:33:31 +00:00
marginSideEffect types . MarginOrderSideEffectType
2022-01-28 07:47:12 +00:00
trailingStopCallbackRatio fixedpoint . Value
2022-01-28 05:06:57 +00:00
minimumProfitPercentage fixedpoint . Value
2022-01-12 06:33:31 +00:00
CurrentHighestPrice fixedpoint . Value
OrderID uint64
}
2022-02-10 12:39:20 +00:00
func ( control * TrailingStopControl ) IsHigherThanMin ( minTargetPrice fixedpoint . Value ) bool {
targetPrice := control . CurrentHighestPrice . Mul ( fixedpoint . One . Sub ( control . trailingStopCallbackRatio ) )
2022-01-12 06:33:31 +00:00
2022-02-10 12:39:20 +00:00
return targetPrice . Compare ( minTargetPrice ) >= 0
2022-01-12 06:33:31 +00:00
}
2022-02-10 12:39:20 +00:00
func ( control * TrailingStopControl ) GenerateStopOrder ( quantity fixedpoint . Value ) types . SubmitOrder {
targetPrice := control . CurrentHighestPrice . Mul ( fixedpoint . One . Sub ( control . trailingStopCallbackRatio ) )
2022-01-12 06:33:31 +00:00
orderForm := types . SubmitOrder {
2022-01-24 03:34:57 +00:00
Symbol : control . symbol ,
Market : control . market ,
Side : types . SideTypeSell ,
2022-04-02 13:32:40 +00:00
Type : types . OrderTypeStopLimit ,
2022-01-24 03:34:57 +00:00
Quantity : quantity ,
MarginSideEffect : control . marginSideEffect ,
2022-04-02 13:32:40 +00:00
TimeInForce : types . TimeInForceGTC ,
2022-01-24 03:34:57 +00:00
Price : targetPrice ,
StopPrice : targetPrice ,
}
return orderForm
2022-01-12 06:33:31 +00:00
}
// Not implemented yet
// ResistanceStop is a kind of stop order by detecting resistance
//type ResistanceStop struct {
// Interval types.Interval `json:"interval"`
// sensitivity fixedpoint.Value `json:"sensitivity"`
// MinVolume fixedpoint.Value `json:"minVolume"`
// TakerBuyRatio fixedpoint.Value `json:"takerBuyRatio"`
//}
2021-02-10 16:21:06 +00:00
type Strategy struct {
2021-06-16 12:33:52 +00:00
* bbgo . Notifiability ` json:"-" `
2021-05-30 17:02:35 +00:00
* bbgo . Persistence
2021-06-24 07:38:55 +00:00
* bbgo . Graceful ` json:"-" `
2021-02-20 02:50:57 +00:00
2021-10-15 04:38:16 +00:00
Symbol string ` json:"symbol" `
Market types . Market ` json:"-" `
2021-06-21 11:03:50 +00:00
// Interval for checking support
2021-10-15 04:38:16 +00:00
Interval types . Interval ` json:"interval" `
2021-06-21 11:03:50 +00:00
// moving average window for checking support (support should be under the moving average line)
2021-12-19 10:28:47 +00:00
TriggerMovingAverage types . IntervalWindow ` json:"triggerMovingAverage" `
2021-06-24 07:49:25 +00:00
2021-06-21 11:03:50 +00:00
// LongTermMovingAverage is the second moving average line for checking support position
LongTermMovingAverage types . IntervalWindow ` json:"longTermMovingAverage" `
2021-06-16 12:33:52 +00:00
2021-02-14 17:26:46 +00:00
Quantity fixedpoint . Value ` json:"quantity" `
MinVolume fixedpoint . Value ` json:"minVolume" `
2021-06-16 05:14:10 +00:00
Sensitivity fixedpoint . Value ` json:"sensitivity" `
2021-05-30 16:31:31 +00:00
TakerBuyRatio fixedpoint . Value ` json:"takerBuyRatio" `
2021-02-14 17:26:46 +00:00
MarginOrderSideEffect types . MarginOrderSideEffectType ` json:"marginOrderSideEffect" `
Targets [ ] Target ` json:"targets" `
2021-02-28 03:57:25 +00:00
2022-01-24 03:34:57 +00:00
// Not implemented yet
2022-01-12 06:33:31 +00:00
// ResistanceStop *ResistanceStop `json:"resistanceStop"`
//
//ResistanceTakerBuyRatio fixedpoint.Value `json:"resistanceTakerBuyRatio"`
2021-06-16 05:23:33 +00:00
2021-06-21 11:03:50 +00:00
// Min BaseAsset balance to keep
MinBaseAssetBalance fixedpoint . Value ` json:"minBaseAssetBalance" `
2021-05-30 16:31:31 +00:00
// Max BaseAsset balance to buy
MaxBaseAssetBalance fixedpoint . Value ` json:"maxBaseAssetBalance" `
MinQuoteAssetBalance fixedpoint . Value ` json:"minQuoteAssetBalance" `
2021-02-28 06:51:24 +00:00
ScaleQuantity * bbgo . PriceVolumeScale ` json:"scaleQuantity" `
2021-05-30 16:31:31 +00:00
2022-03-15 11:19:44 +00:00
orderExecutor bbgo . OrderExecutor
2021-06-24 07:38:55 +00:00
tradeCollector * bbgo . TradeCollector
2022-04-21 10:31:02 +00:00
orderStore * bbgo . OrderStore
2022-06-05 21:43:38 +00:00
activeOrders * bbgo . ActiveOrderBook
2022-04-21 10:31:02 +00:00
state * State
2021-06-21 11:03:50 +00:00
triggerEMA * indicator . EWMA
longTermEMA * indicator . EWMA
2022-01-12 06:33:31 +00:00
// Trailing stop
2022-01-24 03:34:57 +00:00
TrailingStopTarget TrailingStopTarget ` json:"trailingStopTarget" `
trailingStopControl * TrailingStopControl
2022-03-21 02:20:12 +00:00
// StrategyController
2022-04-20 11:20:15 +00:00
bbgo . StrategyController
2021-02-10 16:21:06 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
2021-04-02 02:32:24 +00:00
func ( s * Strategy ) Validate ( ) error {
2022-02-04 03:56:49 +00:00
if s . Quantity . IsZero ( ) && s . ScaleQuantity == nil {
2021-04-02 02:32:24 +00:00
return fmt . Errorf ( "quantity or scaleQuantity can not be zero" )
}
2022-02-04 03:56:49 +00:00
if s . MinVolume . IsZero ( ) && s . Sensitivity . IsZero ( ) {
2021-06-16 05:14:10 +00:00
return fmt . Errorf ( "either minVolume nor sensitivity can not be zero" )
2021-04-02 02:32:24 +00:00
}
return nil
}
2021-02-10 16:21:06 +00:00
func ( s * Strategy ) Subscribe ( session * bbgo . ExchangeSession ) {
2022-05-19 01:48:36 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . Interval } )
2021-08-17 03:06:41 +00:00
2021-12-19 10:28:47 +00:00
if s . TriggerMovingAverage != zeroiw {
2022-05-19 01:48:36 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . TriggerMovingAverage . Interval } )
2021-12-19 10:28:47 +00:00
}
2021-08-17 03:06:41 +00:00
if s . LongTermMovingAverage != zeroiw {
2022-05-19 01:48:36 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . LongTermMovingAverage . Interval } )
2021-08-17 03:06:41 +00:00
}
2021-02-10 16:21:06 +00:00
}
2022-04-20 11:20:15 +00:00
func ( s * Strategy ) CurrentPosition ( ) * types . Position {
return s . state . Position
}
func ( s * Strategy ) ClosePosition ( ctx context . Context , percentage fixedpoint . Value ) error {
base := s . state . Position . GetBase ( )
if base . IsZero ( ) {
return fmt . Errorf ( "no opened %s position" , s . state . Position . Symbol )
}
// make it negative
quantity := base . Mul ( percentage ) . Abs ( )
side := types . SideTypeBuy
if base . Sign ( ) > 0 {
side = types . SideTypeSell
}
if quantity . Compare ( s . Market . MinQuantity ) < 0 {
return fmt . Errorf ( "order quantity %v is too small, less than %v" , quantity , s . Market . MinQuantity )
}
submitOrder := types . SubmitOrder {
Symbol : s . Symbol ,
Side : side ,
Type : types . OrderTypeMarket ,
Quantity : quantity ,
Market : s . Market ,
}
s . Notify ( "Submitting %s %s order to close position by %v" , s . Symbol , side . String ( ) , percentage , submitOrder )
createdOrders , err := s . submitOrders ( ctx , s . orderExecutor , submitOrder )
if err != nil {
log . WithError ( err ) . Errorf ( "can not place position close order" )
}
s . orderStore . Add ( createdOrders ... )
2022-04-21 10:31:02 +00:00
s . activeOrders . Add ( createdOrders ... )
2022-04-20 11:20:15 +00:00
return err
}
2021-05-30 17:02:35 +00:00
func ( s * Strategy ) SaveState ( ) error {
if err := s . Persistence . Save ( s . state , ID , s . Symbol , stateKey ) ; err != nil {
return err
} else {
log . Infof ( "state is saved => %+v" , s . state )
}
return nil
}
func ( s * Strategy ) LoadState ( ) error {
var state State
// load position
if err := s . Persistence . Load ( & state , ID , s . Symbol , stateKey ) ; err != nil {
if err != service . ErrPersistenceNotExists {
return err
}
s . state = & State { }
} else {
s . state = & state
log . Infof ( "state is restored: %+v" , s . state )
}
2021-06-24 07:38:55 +00:00
if s . state . Position == nil {
2021-12-11 11:16:16 +00:00
s . state . Position = types . NewPositionFromMarket ( s . Market )
2021-06-24 07:38:55 +00:00
}
2022-01-28 05:26:10 +00:00
if s . trailingStopControl != nil {
if s . state . CurrentHighestPrice == nil {
2022-04-09 16:03:37 +00:00
s . trailingStopControl . CurrentHighestPrice = fixedpoint . Zero
2022-04-08 10:35:02 +00:00
} else {
s . trailingStopControl . CurrentHighestPrice = * s . state . CurrentHighestPrice
2022-01-28 05:26:10 +00:00
}
s . state . CurrentHighestPrice = & s . trailingStopControl . CurrentHighestPrice
2022-01-24 05:09:12 +00:00
}
2021-05-30 17:02:35 +00:00
return nil
}
2022-01-24 04:11:26 +00:00
func ( s * Strategy ) submitOrders ( ctx context . Context , orderExecutor bbgo . OrderExecutor , orderForms ... types . SubmitOrder ) ( types . OrderSlice , error ) {
2021-06-16 12:33:52 +00:00
for _ , o := range orderForms {
s . Notifiability . Notify ( o )
}
createdOrders , err := orderExecutor . SubmitOrders ( ctx , orderForms ... )
if err != nil {
2022-01-12 06:33:31 +00:00
return nil , err
2021-06-16 12:33:52 +00:00
}
s . orderStore . Add ( createdOrders ... )
2022-04-21 10:31:02 +00:00
s . activeOrders . Add ( createdOrders ... )
2021-06-24 07:49:25 +00:00
s . tradeCollector . Emit ( )
2022-01-24 04:11:26 +00:00
return createdOrders , nil
2022-01-12 06:33:31 +00:00
}
// Cancel order
2022-03-16 10:25:27 +00:00
func ( s * Strategy ) cancelOrder ( orderID uint64 , ctx context . Context , orderExecutor bbgo . OrderExecutor ) error {
2022-01-12 06:33:31 +00:00
// Cancel the original order
order , ok := s . orderStore . Get ( orderID )
2022-01-24 03:34:57 +00:00
if ok {
switch order . Status {
case types . OrderStatusCanceled , types . OrderStatusRejected , types . OrderStatusFilled :
// Do nothing
default :
2022-03-16 10:25:27 +00:00
if err := orderExecutor . CancelOrders ( ctx , order ) ; err != nil {
2022-01-24 03:34:57 +00:00
return err
}
2022-01-12 06:33:31 +00:00
}
2022-01-24 03:34:57 +00:00
}
2022-01-12 06:33:31 +00:00
2022-01-24 03:34:57 +00:00
return nil
2021-06-16 12:33:52 +00:00
}
2022-02-04 03:56:49 +00:00
var slippageModifier = fixedpoint . NewFromFloat ( 1.003 )
func ( s * Strategy ) calculateQuantity ( session * bbgo . ExchangeSession , side types . SideType , closePrice fixedpoint . Value , volume fixedpoint . Value ) ( fixedpoint . Value , error ) {
2021-06-16 12:33:52 +00:00
var quantity fixedpoint . Value
2022-02-04 03:56:49 +00:00
if s . Quantity . Sign ( ) > 0 {
2021-06-16 12:33:52 +00:00
quantity = s . Quantity
} else if s . ScaleQuantity != nil {
2022-02-04 03:56:49 +00:00
q , err := s . ScaleQuantity . Scale ( closePrice . Float64 ( ) , volume . Float64 ( ) )
2021-06-16 12:33:52 +00:00
if err != nil {
2022-02-04 03:56:49 +00:00
return fixedpoint . Zero , err
2021-06-16 12:33:52 +00:00
}
2022-02-04 03:56:49 +00:00
quantity = fixedpoint . NewFromFloat ( q )
2021-06-16 12:33:52 +00:00
}
2022-04-23 07:43:11 +00:00
baseBalance , _ := session . GetAccount ( ) . Balance ( s . Market . BaseCurrency )
2021-06-17 11:28:11 +00:00
if side == types . SideTypeSell {
// quantity = bbgo.AdjustQuantityByMaxAmount(quantity, closePrice, quota)
2022-02-15 05:55:19 +00:00
if s . MinBaseAssetBalance . Sign ( ) > 0 &&
2022-02-04 03:56:49 +00:00
baseBalance . Total ( ) . Sub ( quantity ) . Compare ( s . MinBaseAssetBalance ) < 0 {
quota := baseBalance . Available . Sub ( s . MinBaseAssetBalance )
2021-06-17 11:28:11 +00:00
quantity = bbgo . AdjustQuantityByMaxAmount ( quantity , closePrice , quota )
}
} else if side == types . SideTypeBuy {
2022-02-04 03:56:49 +00:00
if s . MaxBaseAssetBalance . Sign ( ) > 0 &&
baseBalance . Total ( ) . Add ( quantity ) . Compare ( s . MaxBaseAssetBalance ) > 0 {
quota := s . MaxBaseAssetBalance . Sub ( baseBalance . Total ( ) )
2021-06-16 12:33:52 +00:00
quantity = bbgo . AdjustQuantityByMaxAmount ( quantity , closePrice , quota )
}
2022-04-23 07:43:11 +00:00
quoteBalance , ok := session . GetAccount ( ) . Balance ( s . Market . QuoteCurrency )
2021-06-16 12:33:52 +00:00
if ! ok {
2022-02-04 03:56:49 +00:00
return fixedpoint . Zero , fmt . Errorf ( "quote balance %s not found" , s . Market . QuoteCurrency )
2021-06-16 12:33:52 +00:00
}
// for spot, we need to modify the quantity according to the quote balance
if ! session . Margin {
// add 0.3% for price slippage
2022-02-04 03:56:49 +00:00
notional := closePrice . Mul ( quantity ) . Mul ( slippageModifier )
2021-06-16 12:33:52 +00:00
2022-02-04 03:56:49 +00:00
if s . MinQuoteAssetBalance . Sign ( ) > 0 &&
quoteBalance . Available . Sub ( notional ) . Compare ( s . MinQuoteAssetBalance ) < 0 {
2022-02-09 10:48:40 +00:00
log . Warnf ( "modifying quantity %v according to the min quote asset balance %v %s" ,
quantity ,
quoteBalance . Available ,
2021-06-16 12:33:52 +00:00
s . Market . QuoteCurrency )
2022-02-04 03:56:49 +00:00
quota := quoteBalance . Available . Sub ( s . MinQuoteAssetBalance )
2021-06-16 12:33:52 +00:00
quantity = bbgo . AdjustQuantityByMinAmount ( quantity , closePrice , quota )
2022-02-04 03:56:49 +00:00
} else if notional . Compare ( quoteBalance . Available ) > 0 {
2022-02-09 10:48:40 +00:00
log . Warnf ( "modifying quantity %v according to the quote asset balance %v %s" ,
quantity ,
quoteBalance . Available ,
2021-06-16 12:33:52 +00:00
s . Market . QuoteCurrency )
quantity = bbgo . AdjustQuantityByMaxAmount ( quantity , closePrice , quoteBalance . Available )
}
}
}
return quantity , nil
}
2021-02-10 16:21:06 +00:00
func ( s * Strategy ) Run ( ctx context . Context , orderExecutor bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
2022-03-15 11:19:44 +00:00
s . orderExecutor = orderExecutor
2022-03-21 02:20:12 +00:00
// StrategyController
2022-04-20 11:20:15 +00:00
s . Status = types . StrategyStatusRunning
2022-04-18 09:26:43 +00:00
2022-04-21 10:03:54 +00:00
s . OnSuspend ( func ( ) {
2022-04-18 09:26:43 +00:00
// Cancel all order
2022-04-21 10:31:02 +00:00
if err := s . activeOrders . GracefulCancel ( ctx , session . Exchange ) ; err != nil {
2022-04-25 10:45:02 +00:00
errMsg := fmt . Sprintf ( "Not all %s orders are cancelled! Please check again." , s . Symbol )
2022-04-18 09:26:43 +00:00
log . WithError ( err ) . Errorf ( errMsg )
s . Notify ( errMsg )
} else {
2022-04-25 10:45:02 +00:00
s . Notify ( "All %s orders are cancelled." , s . Symbol )
2022-04-18 09:26:43 +00:00
}
// Save state
2022-04-21 10:31:02 +00:00
if err := s . SaveState ( ) ; err != nil {
2022-04-21 10:03:54 +00:00
log . WithError ( err ) . Errorf ( "can not save state: %+v" , s . state )
2022-04-18 09:26:43 +00:00
} else {
2022-04-21 10:03:54 +00:00
log . Infof ( "%s state is saved." , s . Symbol )
2022-04-18 09:26:43 +00:00
}
} )
2022-04-21 10:03:54 +00:00
s . OnEmergencyStop ( func ( ) {
2022-04-18 09:26:43 +00:00
// Close 100% position
2022-04-21 09:57:49 +00:00
percentage := fixedpoint . NewFromFloat ( 1.0 )
2022-04-21 10:03:54 +00:00
if err := s . ClosePosition ( context . Background ( ) , percentage ) ; err != nil {
errMsg := "failed to close position"
log . WithError ( err ) . Errorf ( errMsg )
s . Notify ( errMsg )
2022-04-18 09:26:43 +00:00
}
2022-04-21 10:03:54 +00:00
if err := s . Suspend ( ) ; err != nil {
errMsg := "failed to suspend strategy"
log . WithError ( err ) . Errorf ( errMsg )
s . Notify ( errMsg )
}
2022-04-18 09:26:43 +00:00
} )
2022-03-21 02:20:12 +00:00
2021-02-10 16:21:06 +00:00
// set default values
if s . Interval == "" {
s . Interval = types . Interval5m
}
2022-02-04 03:56:49 +00:00
if s . Sensitivity . Sign ( ) > 0 {
2021-06-16 05:14:10 +00:00
volRange , err := s . ScaleQuantity . ByVolumeRule . Range ( )
if err != nil {
return err
}
2022-02-04 03:56:49 +00:00
scaleUp := fixedpoint . NewFromFloat ( volRange [ 1 ] )
scaleLow := fixedpoint . NewFromFloat ( volRange [ 0 ] )
s . MinVolume = scaleUp . Sub ( scaleLow ) .
Mul ( fixedpoint . One . Sub ( s . Sensitivity ) ) .
Add ( scaleLow )
log . Infof ( "adjusted minimal support volume to %s according to sensitivity %s" , s . MinVolume . String ( ) , s . Sensitivity . String ( ) )
2021-06-16 05:23:33 +00:00
}
2021-02-10 16:21:06 +00:00
market , ok := session . Market ( s . Symbol )
if ! ok {
return fmt . Errorf ( "market %s is not defined" , s . Symbol )
}
2021-06-16 12:33:52 +00:00
s . Market = market
2021-02-10 16:21:06 +00:00
standardIndicatorSet , ok := session . StandardIndicatorSet ( s . Symbol )
if ! ok {
return fmt . Errorf ( "standardIndicatorSet is nil, symbol %s" , s . Symbol )
}
2021-12-19 10:28:47 +00:00
if s . TriggerMovingAverage != zeroiw {
s . triggerEMA = standardIndicatorSet . EWMA ( s . TriggerMovingAverage )
2022-01-10 05:51:14 +00:00
} else {
s . triggerEMA = standardIndicatorSet . EWMA ( types . IntervalWindow {
Interval : s . Interval ,
2022-01-24 03:34:57 +00:00
Window : 99 , // default window
2022-01-10 05:51:14 +00:00
} )
2021-12-19 10:28:47 +00:00
}
2021-06-24 12:16:53 +00:00
if s . LongTermMovingAverage != zeroiw {
s . longTermEMA = standardIndicatorSet . EWMA ( s . LongTermMovingAverage )
}
2021-05-30 16:31:31 +00:00
s . orderStore = bbgo . NewOrderStore ( s . Symbol )
s . orderStore . BindStream ( session . UserDataStream )
2021-06-21 11:03:50 +00:00
2022-04-21 10:31:02 +00:00
s . activeOrders = bbgo . NewLocalActiveOrderBook ( s . Symbol )
s . activeOrders . BindStream ( session . UserDataStream )
2022-02-10 12:39:20 +00:00
if ! s . TrailingStopTarget . TrailingStopCallbackRatio . IsZero ( ) {
2022-01-28 04:58:35 +00:00
s . trailingStopControl = & TrailingStopControl {
symbol : s . Symbol ,
market : s . Market ,
marginSideEffect : s . MarginOrderSideEffect ,
2022-02-10 12:39:20 +00:00
CurrentHighestPrice : fixedpoint . Zero ,
2022-01-28 07:47:12 +00:00
trailingStopCallbackRatio : s . TrailingStopTarget . TrailingStopCallbackRatio ,
2022-01-28 04:58:35 +00:00
minimumProfitPercentage : s . TrailingStopTarget . MinimumProfitPercentage ,
}
2022-01-24 05:09:12 +00:00
}
2021-05-30 17:02:35 +00:00
if err := s . LoadState ( ) ; err != nil {
return err
} else {
s . Notify ( "%s state is restored => %+v" , s . Symbol , s . state )
}
2021-06-24 07:38:55 +00:00
s . tradeCollector = bbgo . NewTradeCollector ( s . Symbol , s . state . Position , s . orderStore )
2022-01-12 06:33:31 +00:00
2022-02-10 12:39:20 +00:00
if ! s . TrailingStopTarget . TrailingStopCallbackRatio . IsZero ( ) {
2022-01-12 06:33:31 +00:00
// Update trailing stop when the position changes
s . tradeCollector . OnPositionUpdate ( func ( position * types . Position ) {
2022-03-21 02:20:12 +00:00
// StrategyController
2022-04-20 11:20:15 +00:00
if s . Status != types . StrategyStatusRunning {
2022-03-21 02:20:12 +00:00
return
}
2022-03-14 03:45:24 +00:00
if position . Base . Compare ( s . Market . MinQuantity ) > 0 { // Update order if we have a position
2022-01-12 06:33:31 +00:00
// Cancel the original order
2022-03-16 10:25:27 +00:00
if err := s . cancelOrder ( s . trailingStopControl . OrderID , ctx , orderExecutor ) ; err != nil {
2022-01-24 03:34:57 +00:00
log . WithError ( err ) . Errorf ( "Can not cancel the original trailing stop order!" )
}
s . trailingStopControl . OrderID = 0
// Calculate minimum target price
2022-02-10 12:39:20 +00:00
var minTargetPrice = fixedpoint . Zero
if s . trailingStopControl . minimumProfitPercentage . Sign ( ) > 0 {
minTargetPrice = position . AverageCost . Mul ( fixedpoint . One . Add ( s . trailingStopControl . minimumProfitPercentage ) )
2022-01-24 03:34:57 +00:00
}
// Place new order if the target price is higher than the minimum target price
if s . trailingStopControl . IsHigherThanMin ( minTargetPrice ) {
2022-02-10 12:39:20 +00:00
orderForm := s . trailingStopControl . GenerateStopOrder ( position . Base )
2022-01-24 04:11:26 +00:00
orders , err := s . submitOrders ( ctx , orderExecutor , orderForm )
2022-01-12 06:33:31 +00:00
if err != nil {
2022-01-24 03:34:57 +00:00
log . WithError ( err ) . Error ( "submit profit trailing stop order error" )
2022-03-31 03:10:53 +00:00
s . Notify ( "submit %s profit trailing stop order error" , s . Symbol )
2022-01-24 03:34:57 +00:00
} else {
2022-03-15 08:44:43 +00:00
orderIds := orders . IDs ( )
if len ( orderIds ) > 0 {
s . trailingStopControl . OrderID = orderIds [ 0 ]
2022-03-14 04:01:17 +00:00
} else {
log . Error ( "submit profit trailing stop order error. unknown error" )
2022-03-31 03:10:53 +00:00
s . Notify ( "submit %s profit trailing stop order error" , s . Symbol )
2022-03-14 04:01:17 +00:00
s . trailingStopControl . OrderID = 0
}
2022-01-24 03:34:57 +00:00
}
}
2022-01-12 06:33:31 +00:00
}
2022-01-24 05:09:12 +00:00
// Save state
if err := s . SaveState ( ) ; err != nil {
log . WithError ( err ) . Errorf ( "can not save state: %+v" , s . state )
} else {
s . Notify ( "%s position is saved" , s . Symbol , s . state . Position )
}
2022-01-12 06:33:31 +00:00
} )
}
2021-06-24 07:38:55 +00:00
s . tradeCollector . BindStream ( session . UserDataStream )
2021-10-08 05:23:38 +00:00
// s.tradeCollector.BindStreamForBackground(session.UserDataStream)
// go s.tradeCollector.Run(ctx)
2021-05-30 16:31:31 +00:00
2021-05-27 19:15:29 +00:00
session . MarketDataStream . OnKLineClosed ( func ( kline types . KLine ) {
2022-03-21 02:20:12 +00:00
// StrategyController
2022-04-20 11:20:15 +00:00
if s . Status != types . StrategyStatusRunning {
2022-03-21 02:20:12 +00:00
return
}
2021-02-10 16:21:06 +00:00
// skip k-lines from other symbols
if kline . Symbol != s . Symbol {
return
}
2021-08-19 09:01:02 +00:00
if kline . Interval != s . Interval {
return
}
2021-02-10 16:21:06 +00:00
2022-02-04 03:56:49 +00:00
closePrice := kline . GetClose ( )
highPrice := kline . GetHigh ( )
2022-01-12 06:33:31 +00:00
2022-02-10 12:39:20 +00:00
if s . TrailingStopTarget . TrailingStopCallbackRatio . Sign ( ) > 0 {
2022-03-14 03:45:24 +00:00
if s . state . Position . Base . Compare ( s . Market . MinQuantity ) <= 0 { // Without a position
2022-01-12 06:33:31 +00:00
// Update trailing orders with current high price
s . trailingStopControl . CurrentHighestPrice = highPrice
2022-04-08 10:46:41 +00:00
} else if s . trailingStopControl . CurrentHighestPrice . Compare ( highPrice ) < 0 || s . trailingStopControl . OrderID == 0 { // With a position or no trailing stop order yet
2022-01-12 06:33:31 +00:00
// Update trailing orders with current high price if it's higher
s . trailingStopControl . CurrentHighestPrice = highPrice
// Cancel the original order
2022-03-16 10:25:27 +00:00
if err := s . cancelOrder ( s . trailingStopControl . OrderID , ctx , orderExecutor ) ; err != nil {
2022-01-24 03:34:57 +00:00
log . WithError ( err ) . Errorf ( "Can not cancel the original trailing stop order!" )
}
s . trailingStopControl . OrderID = 0
// Calculate minimum target price
2022-02-04 03:56:49 +00:00
var minTargetPrice = fixedpoint . Zero
if s . trailingStopControl . minimumProfitPercentage . Sign ( ) > 0 {
minTargetPrice = s . state . Position . AverageCost . Mul ( fixedpoint . One . Add ( s . trailingStopControl . minimumProfitPercentage ) )
2022-01-24 03:34:57 +00:00
}
// Place new order if the target price is higher than the minimum target price
if s . trailingStopControl . IsHigherThanMin ( minTargetPrice ) {
2022-02-04 03:56:49 +00:00
orderForm := s . trailingStopControl . GenerateStopOrder ( s . state . Position . Base )
2022-01-24 04:11:26 +00:00
orders , err := s . submitOrders ( ctx , orderExecutor , orderForm )
2022-04-08 10:35:02 +00:00
if err != nil || orders == nil {
2022-04-02 13:27:52 +00:00
log . WithError ( err ) . Errorf ( "submit %s profit trailing stop order error" , s . Symbol )
2022-03-31 03:10:53 +00:00
s . Notify ( "submit %s profit trailing stop order error" , s . Symbol )
2022-01-24 03:34:57 +00:00
} else {
2022-04-08 10:41:19 +00:00
orderIds := orders . IDs ( )
if len ( orderIds ) > 0 {
s . trailingStopControl . OrderID = orderIds [ 0 ]
} else {
log . Error ( "submit profit trailing stop order error. unknown error" )
s . Notify ( "submit %s profit trailing stop order error" , s . Symbol )
s . trailingStopControl . OrderID = 0
}
2022-01-24 03:34:57 +00:00
}
}
2022-01-12 06:33:31 +00:00
}
2022-01-24 05:09:12 +00:00
// Save state
if err := s . SaveState ( ) ; err != nil {
log . WithError ( err ) . Errorf ( "can not save state: %+v" , s . state )
}
2022-01-12 06:33:31 +00:00
}
2021-06-16 05:23:33 +00:00
2021-06-21 11:03:50 +00:00
// check support volume
2022-02-04 03:56:49 +00:00
if kline . Volume . Compare ( s . MinVolume ) < 0 {
2021-02-10 16:21:06 +00:00
return
}
2021-06-21 11:03:50 +00:00
// check taker buy ratio, we need strong buy taker
2022-02-04 03:56:49 +00:00
if s . TakerBuyRatio . Sign ( ) > 0 {
takerBuyRatio := kline . TakerBuyBaseAssetVolume . Div ( kline . Volume )
takerBuyBaseVolumeThreshold := kline . Volume . Mul ( s . TakerBuyRatio )
if takerBuyRatio . Compare ( s . TakerBuyRatio ) < 0 {
s . Notify ( "%s: taker buy base volume %s (volume ratio %s) is less than %s (volume ratio %s)" ,
2021-05-30 16:31:31 +00:00
s . Symbol ,
2022-02-04 03:56:49 +00:00
kline . TakerBuyBaseAssetVolume . String ( ) ,
takerBuyRatio . String ( ) ,
takerBuyBaseVolumeThreshold . String ( ) ,
kline . Volume . String ( ) ,
s . TakerBuyRatio . String ( ) ,
2021-06-24 07:38:55 +00:00
kline ,
2021-05-30 16:31:31 +00:00
)
2021-06-01 08:39:35 +00:00
return
2021-05-30 16:31:31 +00:00
}
}
2022-02-04 03:56:49 +00:00
if s . longTermEMA != nil && closePrice . Float64 ( ) < s . longTermEMA . Last ( ) {
2021-12-19 09:53:34 +00:00
s . Notify ( "%s: closed price is below the long term moving average line %f, skipping this support" ,
2021-06-21 11:03:50 +00:00
s . Symbol ,
2021-06-24 11:29:41 +00:00
s . longTermEMA . Last ( ) ,
2021-06-21 11:03:50 +00:00
kline ,
)
return
}
2022-03-29 03:46:01 +00:00
if s . triggerEMA != nil && closePrice . Float64 ( ) > s . triggerEMA . Last ( ) {
2021-12-19 09:53:34 +00:00
s . Notify ( "%s: closed price is above the trigger moving average line %f, skipping this support" ,
s . Symbol ,
s . triggerEMA . Last ( ) ,
kline ,
)
return
}
2022-01-10 05:49:36 +00:00
if s . triggerEMA != nil && s . longTermEMA != nil {
2022-02-04 03:56:49 +00:00
s . Notify ( "Found %s support: the close price %s is below trigger EMA %f and above long term EMA %f and volume %s > minimum volume %s" ,
2022-01-10 05:49:36 +00:00
s . Symbol ,
2022-02-04 03:56:49 +00:00
closePrice . String ( ) ,
2022-01-10 05:49:36 +00:00
s . triggerEMA . Last ( ) ,
s . longTermEMA . Last ( ) ,
2022-02-04 03:56:49 +00:00
kline . Volume . String ( ) ,
s . MinVolume . String ( ) ,
2022-01-10 05:49:36 +00:00
kline )
} else {
2022-02-04 03:56:49 +00:00
s . Notify ( "Found %s support: the close price %s and volume %s > minimum volume %s" ,
2022-01-10 05:49:36 +00:00
s . Symbol ,
2022-02-04 03:56:49 +00:00
closePrice . String ( ) ,
kline . Volume . String ( ) ,
s . MinVolume . String ( ) ,
2022-01-10 05:49:36 +00:00
kline )
}
2021-02-10 16:21:06 +00:00
2021-06-16 12:33:52 +00:00
quantity , err := s . calculateQuantity ( session , types . SideTypeBuy , closePrice , kline . Volume )
if err != nil {
log . WithError ( err ) . Errorf ( "%s quantity calculation error" , s . Symbol )
2021-05-30 16:31:31 +00:00
return
}
2021-05-11 05:25:29 +00:00
2021-02-14 17:26:46 +00:00
orderForm := types . SubmitOrder {
Symbol : s . Symbol ,
Market : market ,
Side : types . SideTypeBuy ,
Type : types . OrderTypeMarket ,
2022-02-04 03:56:49 +00:00
Quantity : quantity ,
2021-02-14 17:26:46 +00:00
MarginSideEffect : s . MarginOrderSideEffect ,
}
2022-02-04 03:56:49 +00:00
s . Notify ( "Submitting %s market order buy with quantity %s according to the base volume %s, taker buy base volume %s" ,
2021-06-16 12:33:52 +00:00
s . Symbol ,
2022-02-04 03:56:49 +00:00
quantity . String ( ) ,
kline . Volume . String ( ) ,
kline . TakerBuyBaseAssetVolume . String ( ) ,
2021-06-16 12:33:52 +00:00
orderForm )
2022-01-12 06:33:31 +00:00
if _ , err := s . submitOrders ( ctx , orderExecutor , orderForm ) ; err != nil {
2021-02-10 16:21:06 +00:00
log . WithError ( err ) . Error ( "submit order error" )
return
}
2022-01-24 05:09:12 +00:00
// Save state
if err := s . SaveState ( ) ; err != nil {
log . WithError ( err ) . Errorf ( "can not save state: %+v" , s . state )
} else {
s . Notify ( "%s position is saved" , s . Symbol , s . state . Position )
}
2021-06-16 12:33:52 +00:00
2022-02-10 12:39:20 +00:00
if s . TrailingStopTarget . TrailingStopCallbackRatio . IsZero ( ) { // submit fixed target orders
2022-01-24 03:34:57 +00:00
var targetOrders [ ] types . SubmitOrder
for _ , target := range s . Targets {
2022-02-04 03:56:49 +00:00
targetPrice := closePrice . Mul ( fixedpoint . One . Add ( target . ProfitPercentage ) )
targetQuantity := quantity . Mul ( target . QuantityPercentage )
targetQuoteQuantity := targetPrice . Mul ( targetQuantity )
2022-01-24 03:34:57 +00:00
2022-02-04 03:56:49 +00:00
if targetQuoteQuantity . Compare ( market . MinNotional ) <= 0 {
2022-01-24 03:34:57 +00:00
continue
}
2022-02-04 03:56:49 +00:00
if targetQuantity . Compare ( market . MinQuantity ) <= 0 {
2022-01-24 03:34:57 +00:00
continue
}
targetOrders = append ( targetOrders , types . SubmitOrder {
Symbol : kline . Symbol ,
Market : market ,
Type : types . OrderTypeLimit ,
Side : types . SideTypeSell ,
Price : targetPrice ,
Quantity : targetQuantity ,
MarginSideEffect : target . MarginOrderSideEffect ,
2022-02-18 05:52:13 +00:00
TimeInForce : types . TimeInForceGTC ,
2022-01-24 03:34:57 +00:00
} )
}
if _ , err := s . submitOrders ( ctx , orderExecutor , targetOrders ... ) ; err != nil {
log . WithError ( err ) . Error ( "submit profit target order error" )
2022-03-31 03:10:53 +00:00
s . Notify ( "submit %s profit trailing stop order error" , s . Symbol )
2022-01-24 03:34:57 +00:00
return
}
2021-02-10 16:21:06 +00:00
}
2022-01-12 06:33:31 +00:00
s . tradeCollector . Process ( )
2021-02-10 16:21:06 +00:00
} )
2021-05-30 17:02:35 +00:00
s . Graceful . OnShutdown ( func ( ctx context . Context , wg * sync . WaitGroup ) {
defer wg . Done ( )
2022-01-24 05:09:12 +00:00
// Cancel trailing stop order
2022-02-10 12:39:20 +00:00
if s . TrailingStopTarget . TrailingStopCallbackRatio . Sign ( ) > 0 {
2022-04-21 10:31:02 +00:00
// Cancel all orders
if err := s . activeOrders . GracefulCancel ( ctx , session . Exchange ) ; err != nil {
errMsg := "Not all {s.Symbol} orders are cancelled! Please check again."
log . WithError ( err ) . Errorf ( errMsg )
s . Notify ( errMsg )
} else {
s . Notify ( "All {s.Symbol} orders are cancelled." )
2022-01-24 05:09:12 +00:00
}
2022-04-21 10:31:02 +00:00
2022-01-24 05:09:12 +00:00
s . trailingStopControl . OrderID = 0
}
2021-05-30 17:02:35 +00:00
if err := s . SaveState ( ) ; err != nil {
log . WithError ( err ) . Errorf ( "can not save state: %+v" , s . state )
} else {
s . Notify ( "%s position is saved" , s . Symbol , s . state . Position )
}
} )
2021-02-10 16:21:06 +00:00
return nil
}