2022-06-18 08:31:53 +00:00
|
|
|
package bbgo
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-09-09 05:57:39 +00:00
|
|
|
"errors"
|
2022-07-04 12:13:54 +00:00
|
|
|
"fmt"
|
2022-06-27 10:17:57 +00:00
|
|
|
"strings"
|
2022-06-18 08:31:53 +00:00
|
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
|
|
|
type NotifyFunc func(obj interface{}, args ...interface{})
|
|
|
|
|
|
|
|
// GeneralOrderExecutor implements the general order executor for strategy
|
|
|
|
type GeneralOrderExecutor struct {
|
|
|
|
session *ExchangeSession
|
|
|
|
symbol string
|
|
|
|
strategy string
|
|
|
|
strategyInstanceID string
|
|
|
|
position *types.Position
|
|
|
|
activeMakerOrders *ActiveOrderBook
|
|
|
|
orderStore *OrderStore
|
|
|
|
tradeCollector *TradeCollector
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewGeneralOrderExecutor(session *ExchangeSession, symbol, strategy, strategyInstanceID string, position *types.Position) *GeneralOrderExecutor {
|
|
|
|
// Always update the position fields
|
|
|
|
position.Strategy = strategy
|
|
|
|
position.StrategyInstanceID = strategyInstanceID
|
|
|
|
|
|
|
|
orderStore := NewOrderStore(symbol)
|
|
|
|
return &GeneralOrderExecutor{
|
|
|
|
session: session,
|
|
|
|
symbol: symbol,
|
|
|
|
strategy: strategy,
|
|
|
|
strategyInstanceID: strategyInstanceID,
|
|
|
|
position: position,
|
|
|
|
activeMakerOrders: NewActiveOrderBook(symbol),
|
|
|
|
orderStore: orderStore,
|
|
|
|
tradeCollector: NewTradeCollector(symbol, position, orderStore),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-28 10:34:12 +00:00
|
|
|
func (e *GeneralOrderExecutor) ActiveMakerOrders() *ActiveOrderBook {
|
|
|
|
return e.activeMakerOrders
|
|
|
|
}
|
|
|
|
|
2022-06-18 08:31:53 +00:00
|
|
|
func (e *GeneralOrderExecutor) BindEnvironment(environ *Environment) {
|
|
|
|
e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) {
|
|
|
|
environ.RecordPosition(e.position, trade, profit)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *GeneralOrderExecutor) BindTradeStats(tradeStats *types.TradeStats) {
|
|
|
|
e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) {
|
|
|
|
if profit == nil {
|
|
|
|
return
|
|
|
|
}
|
2022-07-05 03:14:50 +00:00
|
|
|
|
|
|
|
tradeStats.Add(profit)
|
2022-06-18 08:31:53 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-19 04:29:36 +00:00
|
|
|
func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) {
|
2022-06-18 08:31:53 +00:00
|
|
|
e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) {
|
|
|
|
profitStats.AddTrade(trade)
|
|
|
|
if profit == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
profitStats.AddProfit(*profit)
|
2022-07-08 08:43:32 +00:00
|
|
|
|
|
|
|
Notify(profit)
|
2022-07-01 09:32:40 +00:00
|
|
|
Notify(profitStats)
|
2022-06-18 08:31:53 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-19 05:01:22 +00:00
|
|
|
func (e *GeneralOrderExecutor) Bind() {
|
2022-06-18 08:31:53 +00:00
|
|
|
e.activeMakerOrders.BindStream(e.session.UserDataStream)
|
|
|
|
e.orderStore.BindStream(e.session.UserDataStream)
|
|
|
|
|
|
|
|
// trade notify
|
|
|
|
e.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) {
|
2022-06-19 05:01:22 +00:00
|
|
|
Notify(trade)
|
2022-06-18 08:31:53 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
e.tradeCollector.OnPositionUpdate(func(position *types.Position) {
|
|
|
|
log.Infof("position changed: %s", position)
|
2022-06-19 05:01:22 +00:00
|
|
|
Notify(position)
|
2022-06-18 08:31:53 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
e.tradeCollector.BindStream(e.session.UserDataStream)
|
|
|
|
}
|
|
|
|
|
2022-06-30 16:57:19 +00:00
|
|
|
// CancelOrders cancels the given order objects directly
|
2022-06-26 08:13:58 +00:00
|
|
|
func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error {
|
2022-08-11 05:49:16 +00:00
|
|
|
err := e.session.Exchange.CancelOrders(ctx, orders...)
|
|
|
|
if err != nil { // Retry once
|
|
|
|
err = e.session.Exchange.CancelOrders(ctx, orders...)
|
|
|
|
}
|
|
|
|
return err
|
2022-06-26 08:13:58 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 07:57:59 +00:00
|
|
|
func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) {
|
2022-06-18 08:31:53 +00:00
|
|
|
formattedOrders, err := e.session.FormatOrders(submitOrders)
|
|
|
|
if err != nil {
|
2022-06-19 07:57:59 +00:00
|
|
|
return nil, err
|
2022-06-18 08:31:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
createdOrders, err := e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
|
|
|
|
if err != nil {
|
2022-08-11 05:49:16 +00:00
|
|
|
// Retry once
|
|
|
|
createdOrders, err = e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("can not place orders: %w", err)
|
|
|
|
}
|
2022-06-18 08:31:53 +00:00
|
|
|
}
|
2022-08-10 11:36:30 +00:00
|
|
|
// FIXME: map by price and volume
|
|
|
|
for i := 0; i < len(createdOrders); i++ {
|
|
|
|
createdOrders[i].Tag = formattedOrders[i].Tag
|
|
|
|
}
|
2022-06-18 08:31:53 +00:00
|
|
|
|
|
|
|
e.orderStore.Add(createdOrders...)
|
|
|
|
e.activeMakerOrders.Add(createdOrders...)
|
|
|
|
e.tradeCollector.Process()
|
2022-06-19 07:57:59 +00:00
|
|
|
return createdOrders, err
|
2022-06-18 08:31:53 +00:00
|
|
|
}
|
|
|
|
|
2022-09-09 05:57:39 +00:00
|
|
|
type OpenPositionOptions struct {
|
|
|
|
// Long is for open a long position
|
|
|
|
// Long or Short must be set
|
2022-09-09 09:50:21 +00:00
|
|
|
Long bool `json:"long"`
|
2022-09-09 05:57:39 +00:00
|
|
|
|
|
|
|
// Short is for open a short position
|
|
|
|
// Long or Short must be set
|
2022-09-09 09:50:21 +00:00
|
|
|
Short bool `json:"short"`
|
2022-09-09 05:57:39 +00:00
|
|
|
|
|
|
|
// Leverage is used for leveraged position and account
|
2022-09-09 09:50:21 +00:00
|
|
|
Leverage fixedpoint.Value `json:"leverage,omitempty"`
|
2022-09-09 05:57:39 +00:00
|
|
|
|
2022-09-09 09:50:21 +00:00
|
|
|
// Quantity will be used first, it will override the leverage if it's given.
|
|
|
|
Quantity fixedpoint.Value `json:"quantity,omitempty"`
|
|
|
|
|
|
|
|
// MarketOrder set to true to open a position with a market order
|
|
|
|
MarketOrder bool
|
|
|
|
|
|
|
|
// LimitOrder set to true to open a position with a limit order
|
|
|
|
LimitOrder bool
|
|
|
|
|
|
|
|
// LimitTakerRatio is used when LimitOrder = true, it adjusts the price of the limit order with a ratio.
|
|
|
|
// So you can ensure that the limit order can be a taker order. Higher the ratio, higher the chance it could be a taker order.
|
2022-09-09 05:57:39 +00:00
|
|
|
LimitTakerRatio fixedpoint.Value
|
|
|
|
CurrentPrice fixedpoint.Value
|
|
|
|
Tag string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) error {
|
2022-09-09 09:50:21 +00:00
|
|
|
log.Infof("opening %s position: %+v", e.position.Symbol, options)
|
|
|
|
|
2022-09-09 05:57:39 +00:00
|
|
|
price := options.CurrentPrice
|
|
|
|
submitOrder := types.SubmitOrder{
|
|
|
|
Symbol: e.position.Symbol,
|
|
|
|
Type: types.OrderTypeMarket,
|
|
|
|
MarginSideEffect: types.SideEffectTypeMarginBuy,
|
|
|
|
Tag: options.Tag,
|
|
|
|
}
|
|
|
|
|
|
|
|
if !options.LimitTakerRatio.IsZero() {
|
|
|
|
if options.Long {
|
|
|
|
// use higher price to buy (this ensures that our order will be filled)
|
|
|
|
price = price.Mul(one.Add(options.LimitTakerRatio))
|
|
|
|
} else if options.Short {
|
|
|
|
// use lower price to sell (this ensures that our order will be filled)
|
|
|
|
price = price.Mul(one.Sub(options.LimitTakerRatio))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if options.MarketOrder {
|
|
|
|
submitOrder.Type = types.OrderTypeMarket
|
|
|
|
} else if options.LimitOrder {
|
|
|
|
submitOrder.Type = types.OrderTypeLimit
|
|
|
|
submitOrder.Price = price
|
|
|
|
}
|
|
|
|
|
|
|
|
quantity := options.Quantity
|
|
|
|
|
|
|
|
if options.Long {
|
|
|
|
if quantity.IsZero() {
|
2022-09-09 09:40:17 +00:00
|
|
|
quoteQuantity, err := CalculateQuoteQuantity(ctx, e.session, e.position.QuoteCurrency, options.Leverage)
|
2022-09-09 05:57:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
quantity = quoteQuantity.Div(price)
|
|
|
|
}
|
|
|
|
|
|
|
|
submitOrder.Side = types.SideTypeBuy
|
|
|
|
submitOrder.Quantity = quantity
|
|
|
|
|
|
|
|
createdOrder, err2 := e.SubmitOrders(ctx, submitOrder)
|
|
|
|
if err2 != nil {
|
|
|
|
return err2
|
|
|
|
}
|
|
|
|
_ = createdOrder
|
|
|
|
return nil
|
|
|
|
} else if options.Short {
|
|
|
|
if quantity.IsZero() {
|
|
|
|
var err error
|
2022-09-09 09:40:17 +00:00
|
|
|
quantity, err = CalculateBaseQuantity(e.session, e.position.Market, price, quantity, options.Leverage)
|
2022-09-09 05:57:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
submitOrder.Side = types.SideTypeSell
|
|
|
|
submitOrder.Quantity = quantity
|
|
|
|
|
|
|
|
createdOrder, err2 := e.SubmitOrders(ctx, submitOrder)
|
|
|
|
if err2 != nil {
|
|
|
|
return err2
|
|
|
|
}
|
|
|
|
_ = createdOrder
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return errors.New("options Long or Short must be set")
|
|
|
|
}
|
|
|
|
|
2022-06-30 16:57:19 +00:00
|
|
|
// GracefulCancelActiveOrderBook cancels the orders from the active orderbook.
|
|
|
|
func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook) error {
|
2022-07-14 03:46:19 +00:00
|
|
|
if activeOrders.NumOfOrders() == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2022-06-30 16:57:19 +00:00
|
|
|
if err := activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil {
|
2022-08-11 05:49:16 +00:00
|
|
|
// Retry once
|
|
|
|
if err = activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil {
|
|
|
|
return fmt.Errorf("graceful cancel order error: %w", err)
|
|
|
|
}
|
2022-06-18 08:31:53 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 05:40:10 +00:00
|
|
|
e.tradeCollector.Process()
|
2022-06-18 08:31:53 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-30 16:57:19 +00:00
|
|
|
// GracefulCancel cancels all active maker orders
|
|
|
|
func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
|
|
|
|
return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders)
|
|
|
|
}
|
|
|
|
|
2022-06-27 10:17:57 +00:00
|
|
|
func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error {
|
2022-06-19 03:20:29 +00:00
|
|
|
submitOrder := e.position.NewMarketCloseOrder(percentage)
|
2022-06-18 08:31:53 +00:00
|
|
|
if submitOrder == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-07-28 02:27:04 +00:00
|
|
|
log.Infof("closing %s position with tags: %v", e.symbol, tags)
|
2022-06-27 10:17:57 +00:00
|
|
|
submitOrder.Tag = strings.Join(tags, ",")
|
2022-06-19 07:57:59 +00:00
|
|
|
_, err := e.SubmitOrders(ctx, *submitOrder)
|
|
|
|
return err
|
2022-06-18 08:31:53 +00:00
|
|
|
}
|
2022-06-19 05:40:10 +00:00
|
|
|
|
|
|
|
func (e *GeneralOrderExecutor) TradeCollector() *TradeCollector {
|
|
|
|
return e.tradeCollector
|
|
|
|
}
|
2022-06-26 08:13:58 +00:00
|
|
|
|
|
|
|
func (e *GeneralOrderExecutor) Session() *ExchangeSession {
|
|
|
|
return e.session
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *GeneralOrderExecutor) Position() *types.Position {
|
|
|
|
return e.position
|
|
|
|
}
|