2021-05-02 10:03:41 +00:00
package schedule
import (
"context"
2022-07-19 09:38:32 +00:00
"fmt"
2022-01-30 17:42:21 +00:00
2021-05-02 12:58:32 +00:00
"github.com/pkg/errors"
2021-05-02 10:03:41 +00:00
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo"
2022-08-19 08:48:43 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
2021-05-02 10:03:41 +00:00
"github.com/c9s/bbgo/pkg/types"
)
const ID = "schedule"
func init ( ) {
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
type Strategy struct {
Market types . Market
2021-05-02 12:58:32 +00:00
// StandardIndicatorSet contains the standard indicators of a market (symbol)
// This field will be injected automatically since we defined the Symbol field.
* bbgo . StandardIndicatorSet
2021-05-02 10:03:41 +00:00
// Interval is the period that you want to submit order
Interval types . Interval ` json:"interval" `
// Symbol is the symbol of the market
Symbol string ` json:"symbol" `
// Side is the order side type, which can be buy or sell
2022-01-30 17:42:21 +00:00
Side types . SideType ` json:"side,omitempty" `
2021-05-02 10:03:41 +00:00
2023-06-07 08:27:36 +00:00
UseLimitOrder bool ` json:"useLimitOrder" `
2022-01-30 17:42:21 +00:00
bbgo . QuantityOrAmount
2021-05-02 10:03:41 +00:00
2023-06-07 08:36:38 +00:00
MinBaseBalance fixedpoint . Value ` json:"minBaseBalance" `
2022-08-19 08:48:43 +00:00
MaxBaseBalance fixedpoint . Value ` json:"maxBaseBalance" `
2021-08-26 03:32:39 +00:00
BelowMovingAverage * bbgo . MovingAverageSettings ` json:"belowMovingAverage,omitempty" `
2021-05-02 12:58:32 +00:00
2021-08-26 03:32:39 +00:00
AboveMovingAverage * bbgo . MovingAverageSettings ` json:"aboveMovingAverage,omitempty" `
2022-07-19 09:38:32 +00:00
Position * types . Position ` persistence:"position" `
session * bbgo . ExchangeSession
orderExecutor * bbgo . GeneralOrderExecutor
2021-05-02 10:03:41 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
2022-07-19 09:38:32 +00:00
func ( s * Strategy ) InstanceID ( ) string {
return fmt . Sprintf ( "%s:%s" , ID , s . Symbol )
}
2021-05-02 10:03:41 +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-19 08:35:05 +00:00
if s . BelowMovingAverage != nil {
2022-05-19 01:48:36 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . BelowMovingAverage . Interval } )
2021-08-19 08:35:05 +00:00
}
if s . AboveMovingAverage != nil {
2022-05-19 01:48:36 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . AboveMovingAverage . Interval } )
2021-08-19 08:35:05 +00:00
}
2021-05-02 10:03:41 +00:00
}
2021-05-02 12:58:32 +00:00
func ( s * Strategy ) Validate ( ) error {
2022-01-30 17:42:21 +00:00
if err := s . QuantityOrAmount . Validate ( ) ; err != nil {
return err
2021-05-02 12:58:32 +00:00
}
return nil
}
2021-05-02 10:03:41 +00:00
func ( s * Strategy ) Run ( ctx context . Context , orderExecutor bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
2022-07-19 09:38:32 +00:00
s . session = session
2021-05-02 12:58:32 +00:00
if s . StandardIndicatorSet == nil {
return errors . New ( "StandardIndicatorSet can not be nil, injection failed?" )
}
2022-07-19 09:38:32 +00:00
if s . Position == nil {
s . Position = types . NewPositionFromMarket ( s . Market )
}
instanceID := s . InstanceID ( )
s . orderExecutor = bbgo . NewGeneralOrderExecutor ( session , s . Symbol , ID , instanceID , s . Position )
s . orderExecutor . TradeCollector ( ) . OnPositionUpdate ( func ( position * types . Position ) {
2022-10-03 10:45:24 +00:00
bbgo . Sync ( ctx , s )
2022-07-19 09:38:32 +00:00
} )
s . orderExecutor . Bind ( )
2021-08-26 03:46:02 +00:00
var belowMA types . Float64Indicator
var aboveMA types . Float64Indicator
2021-05-02 12:58:32 +00:00
var err error
if s . BelowMovingAverage != nil {
belowMA , err = s . BelowMovingAverage . Indicator ( s . StandardIndicatorSet )
if err != nil {
return err
}
}
if s . AboveMovingAverage != nil {
aboveMA , err = s . AboveMovingAverage . Indicator ( s . StandardIndicatorSet )
if err != nil {
return err
}
}
2021-05-27 19:15:29 +00:00
session . MarketDataStream . OnKLineClosed ( func ( kline types . KLine ) {
2021-05-02 10:03:41 +00:00
if kline . Symbol != s . Symbol {
return
}
2021-08-19 08:54:00 +00:00
if kline . Interval != s . Interval {
return
}
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
closePrice := kline . Close
closePriceF := closePrice . Float64 ( )
2022-01-30 17:42:21 +00:00
quantity := s . QuantityOrAmount . CalculateQuantity ( closePrice )
2021-05-02 12:58:32 +00:00
side := s . Side
if s . BelowMovingAverage != nil || s . AboveMovingAverage != nil {
match := false
// if any of the conditions satisfies then we execute order
2023-05-31 11:35:44 +00:00
if belowMA != nil && closePriceF < belowMA . Last ( 0 ) {
2021-05-02 12:58:32 +00:00
match = true
if s . BelowMovingAverage != nil {
if s . BelowMovingAverage . Side != nil {
side = * s . BelowMovingAverage . Side
}
// override the default quantity or amount
2022-01-30 17:42:21 +00:00
if s . BelowMovingAverage . QuantityOrAmount . IsSet ( ) {
quantity = s . BelowMovingAverage . QuantityOrAmount . CalculateQuantity ( closePrice )
2021-05-02 12:58:32 +00:00
}
}
2023-05-31 11:35:44 +00:00
} else if aboveMA != nil && closePriceF > aboveMA . Last ( 0 ) {
2021-05-02 12:58:32 +00:00
match = true
if s . AboveMovingAverage != nil {
if s . AboveMovingAverage . Side != nil {
side = * s . AboveMovingAverage . Side
}
2022-01-30 17:42:21 +00:00
if s . AboveMovingAverage . QuantityOrAmount . IsSet ( ) {
quantity = s . AboveMovingAverage . QuantityOrAmount . CalculateQuantity ( closePrice )
2021-05-02 12:58:32 +00:00
}
}
}
if ! match {
2022-06-19 04:29:36 +00:00
bbgo . Notify ( "skip, the %s closed price %v is below or above moving average" , s . Symbol , closePrice )
2021-05-02 12:58:32 +00:00
return
}
}
2021-05-02 10:03:41 +00:00
2021-05-02 12:58:32 +00:00
// calculate quote quantity for balance checking
quoteQuantity := quantity . Mul ( closePrice )
2023-06-07 08:36:38 +00:00
quoteBalance , ok := session . GetAccount ( ) . Balance ( s . Market . QuoteCurrency )
if ! ok {
log . Errorf ( "can not place scheduled %s order, quote balance %s is empty" , s . Symbol , s . Market . QuoteCurrency )
return
}
baseBalance , ok := session . GetAccount ( ) . Balance ( s . Market . BaseCurrency )
if ! ok {
log . Errorf ( "can not place scheduled %s order, base balance %s is empty" , s . Symbol , s . Market . BaseCurrency )
return
}
totalBase := baseBalance . Total ( )
2021-05-02 12:58:32 +00:00
// execute orders
switch side {
2021-05-02 10:03:41 +00:00
case types . SideTypeBuy :
2023-06-07 08:36:38 +00:00
2022-08-19 08:48:43 +00:00
if ! s . MaxBaseBalance . IsZero ( ) {
2023-06-07 08:36:38 +00:00
if totalBase . Add ( quantity ) . Compare ( s . MaxBaseBalance ) >= 0 {
quantity = s . MaxBaseBalance . Sub ( totalBase )
quoteQuantity = quantity . Mul ( closePrice )
2022-08-19 08:48:43 +00:00
}
}
2023-06-07 08:36:38 +00:00
// if min base balance is defined
if ! s . MinBaseBalance . IsZero ( ) && s . MinBaseBalance . Compare ( totalBase ) > 0 {
quantity = fixedpoint . Max ( quantity , s . MinBaseBalance . Sub ( totalBase ) )
quantity = fixedpoint . Max ( quantity , s . Market . MinQuantity )
2021-05-02 10:03:41 +00:00
}
2022-01-30 17:42:21 +00:00
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if quoteBalance . Available . Compare ( quoteQuantity ) < 0 {
log . Errorf ( "can not place scheduled %s order: quote balance %s is not enough: %v < %v" , s . Symbol , s . Market . QuoteCurrency , quoteBalance . Available , quoteQuantity )
2021-05-02 10:03:41 +00:00
return
}
case types . SideTypeSell :
2023-06-07 08:36:38 +00:00
quantity = fixedpoint . Min ( quantity , baseBalance . Available )
// skip sell if we hit the minBaseBalance line
if ! s . MinBaseBalance . IsZero ( ) {
if totalBase . Sub ( quantity ) . Compare ( s . MinBaseBalance ) < 0 {
return
}
2021-05-02 10:03:41 +00:00
}
2022-01-30 17:42:21 +00:00
2022-08-19 08:48:43 +00:00
quoteQuantity = quantity . Mul ( closePrice )
}
2021-05-02 10:03:41 +00:00
2023-06-07 08:27:36 +00:00
// truncate quantity by its step size
quantity = s . Market . TruncateQuantity ( quantity )
2022-08-19 08:48:43 +00:00
if s . Market . IsDustQuantity ( quantity , closePrice ) {
2023-06-07 08:27:36 +00:00
log . Warnf ( "%s: quantity %f is too small, skip order" , s . Symbol , quantity . Float64 ( ) )
2022-08-19 08:48:43 +00:00
return
2021-05-02 10:03:41 +00:00
}
2023-06-07 08:27:36 +00:00
submitOrder := types . SubmitOrder {
2021-05-02 10:03:41 +00:00
Symbol : s . Symbol ,
2021-05-02 12:58:32 +00:00
Side : side ,
2021-05-02 10:03:41 +00:00
Type : types . OrderTypeMarket ,
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
Quantity : quantity ,
2022-01-30 17:42:21 +00:00
Market : s . Market ,
2023-06-07 08:27:36 +00:00
}
if s . UseLimitOrder {
submitOrder . Type = types . OrderTypeLimit
submitOrder . Price = closePrice
}
2023-06-07 08:30:54 +00:00
if err := s . orderExecutor . GracefulCancel ( ctx ) ; err != nil {
log . WithError ( err ) . Errorf ( "cancel order error" )
}
2023-06-07 08:27:36 +00:00
bbgo . Notify ( "Submitting scheduled %s order with quantity %s at price %s" , s . Symbol , quantity . String ( ) , closePrice . String ( ) )
_ , err := s . orderExecutor . SubmitOrders ( ctx , submitOrder )
2021-05-02 10:03:41 +00:00
if err != nil {
2022-06-19 04:29:36 +00:00
bbgo . Notify ( "Can not place scheduled %s order: submit error %s" , s . Symbol , err . Error ( ) )
2022-01-30 17:42:21 +00:00
log . WithError ( err ) . Errorf ( "can not place scheduled %s order error" , s . Symbol )
2021-05-02 10:03:41 +00:00
}
} )
return nil
}