Merge pull request #744 from c9s/refactor/support

refactor: refactor and update the support strategy
This commit is contained in:
Yo-An Lin 2022-06-20 02:08:32 +08:00 committed by GitHub
commit 4cb0b1c571
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 128 additions and 297 deletions

View File

@ -3,50 +3,26 @@ notifications:
slack:
defaultChannel: "dev-bbgo"
errorChannel: "bbgo-error"
# if you want to route channel by symbol
symbolChannels:
"^BTC": "btc"
"^ETH": "eth"
"^BNB": "bnb"
# object routing rules
routing:
trade: "$symbol"
order: "$symbol"
submitOrder: "$session" # not supported yet
pnL: "bbgo-pnl"
sessions:
binance:
exchange: binance
riskControls:
# This is the session-based risk controller, which let you configure different risk controller by session.
sessionBased:
max:
orderExecutor:
bySymbol:
BTCUSDT:
basic:
minQuoteBalance: 100.0
maxBaseAssetBalance: 3.0
minBaseAssetBalance: 0.0
maxOrderAmount: 1000.0
backtest:
# for testing max draw down (MDD) at 03-12
# see here for more details
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2020-09-04"
endTime: "2020-09-14"
startTime: "2021-09-01"
endTime: "2021-09-30"
sessions:
- binance
symbols:
- BTCUSDT
- ETHUSDT
- LINKUSDT
account:
binance:
balances:
BTC: 0.0
USDT: 10000.0
exchangeStrategies:
@ -54,8 +30,8 @@ exchangeStrategies:
- on: binance
support:
symbol: LINKUSDT
interval: 1m
minVolume: 1_000
interval: 5m
minVolume: 80_000
triggerMovingAverage:
interval: 5m
window: 99
@ -66,17 +42,16 @@ exchangeStrategies:
scaleQuantity:
byVolume:
exp:
domain: [ 1_000, 200_000 ]
domain: [ 10_000, 200_000 ]
range: [ 0.5, 1.0 ]
maxBaseAssetBalance: 1000.0
minQuoteAssetBalance: 2000.0
#trailingStopTarget:
# callbackRatio: 0.015
# minimumProfitPercentage: 0.02
trailingStopTarget:
callbackRatio: 1.5%
minimumProfitPercentage: 2%
targets:
- profitPercentage: 0.02
quantityPercentage: 0.5

View File

@ -85,10 +85,10 @@ func (e *GeneralOrderExecutor) Bind() {
e.tradeCollector.BindStream(e.session.UserDataStream)
}
func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) error {
func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) {
formattedOrders, err := e.session.FormatOrders(submitOrders)
if err != nil {
return err
return nil, err
}
createdOrders, err := e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
@ -99,7 +99,7 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ..
e.orderStore.Add(createdOrders...)
e.activeMakerOrders.Add(createdOrders...)
e.tradeCollector.Process()
return err
return createdOrders, err
}
func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
@ -118,10 +118,10 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix
return nil
}
return e.SubmitOrders(ctx, *submitOrder)
_, err := e.SubmitOrders(ctx, *submitOrder)
return err
}
func (e *GeneralOrderExecutor) TradeCollector() *TradeCollector {
return e.tradeCollector
}

View File

@ -52,7 +52,7 @@ func (s *BacktestService) SyncKLineByInterval(ctx context.Context, exchange type
q := &batch.KLineBatchQuery{Exchange: exchange}
return q.Query(ctx, symbol, interval, startTime, endTime)
},
BatchInsertBuffer: 500,
BatchInsertBuffer: 1000,
BatchInsert: func(obj interface{}) error {
kLines := obj.([]types.KLine)
return s.BatchInsert(kLines)

View File

@ -130,7 +130,7 @@ func (sel SyncTask) execute(ctx context.Context, db *sqlx.DB, startTime time.Tim
}
if sel.BatchInsert != nil {
if batchBufferRefVal.Len() >= sel.BatchInsertBuffer-1 {
if batchBufferRefVal.Len() > sel.BatchInsertBuffer-1 {
if sel.LogInsert {
logrus.Infof("batch inserting %d %T", batchBufferRefVal.Len(), obj)
} else {

View File

@ -241,7 +241,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
bbgo.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder)
err := s.orderExecutor.SubmitOrders(ctx, submitOrder)
_, err := s.orderExecutor.SubmitOrders(ctx, submitOrder)
return err
}
@ -470,7 +470,7 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec
submitOrders[i] = adjustOrderQuantity(submitOrders[i], s.Market)
}
_ = s.orderExecutor.SubmitOrders(ctx, submitOrders...)
_, _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...)
}
func (s *Strategy) hasLongSet() bool {

View File

@ -143,7 +143,7 @@ func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoin
}
func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value) {
_ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Price: price,
Side: types.SideTypeSell,
@ -154,7 +154,7 @@ func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoin
}
func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value) {
_ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeMarket,
@ -174,7 +174,8 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
}
bbgo.Notify("Closing %s position by %f", s.Symbol, percentage.Float64())
return s.orderExecutor.SubmitOrders(ctx, *submitOrder)
_, err := s.orderExecutor.SubmitOrders(ctx, *submitOrder)
return err
}
func (s *Strategy) InstanceID() string {
@ -470,7 +471,7 @@ func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fi
}
func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quantity fixedpoint.Value) {
_ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,

View File

@ -7,11 +7,9 @@ import (
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
@ -92,7 +90,13 @@ type TrailingStopControl struct {
minimumProfitPercentage fixedpoint.Value
CurrentHighestPrice fixedpoint.Value
OrderID uint64
StopOrder *types.Order
}
func (control *TrailingStopControl) UpdateCurrentHighestPrice(p fixedpoint.Value) bool {
orig := control.CurrentHighestPrice
control.CurrentHighestPrice = fixedpoint.Max(control.CurrentHighestPrice, p)
return orig.Compare(control.CurrentHighestPrice) == 0
}
func (control *TrailingStopControl) IsHigherThanMin(minTargetPrice fixedpoint.Value) bool {
@ -130,8 +134,11 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value)
// }
type Strategy struct {
*bbgo.Persistence
*bbgo.Graceful `json:"-"`
*bbgo.Persistence `json:"-"`
*bbgo.Environment `json:"-"`
*bbgo.Graceful `json:"-"`
session *bbgo.ExchangeSession
Symbol string `json:"symbol"`
Market types.Market `json:"-"`
@ -165,13 +172,14 @@ type Strategy struct {
ScaleQuantity *bbgo.PriceVolumeScale `json:"scaleQuantity"`
orderExecutor bbgo.OrderExecutor
orderExecutor *bbgo.GeneralOrderExecutor
tradeCollector *bbgo.TradeCollector
Position *types.Position `persistence:"position"`
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
TradeStats *types.TradeStats `persistence:"trade_stats"`
CurrentHighestPrice fixedpoint.Value `persistence:"current_highest_price"`
orderStore *bbgo.OrderStore
activeOrders *bbgo.ActiveOrderBook
state *State
state *State
triggerEMA *indicator.EWMA
longTermEMA *indicator.EWMA
@ -217,13 +225,13 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
}
func (s *Strategy) CurrentPosition() *types.Position {
return s.state.Position
return s.Position
}
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
base := s.state.Position.GetBase()
base := s.Position.GetBase()
if base.IsZero() {
return fmt.Errorf("no opened %s position", s.state.Position.Symbol)
return fmt.Errorf("no opened %s position", s.Position.Symbol)
}
// make it negative
@ -246,85 +254,12 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
}
bbgo.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...)
s.activeOrders.Add(createdOrders...)
_, err := s.orderExecutor.SubmitOrders(ctx, submitOrder)
return err
}
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)
}
if s.state.Position == nil {
s.state.Position = types.NewPositionFromMarket(s.Market)
}
if s.trailingStopControl != nil {
if s.state.CurrentHighestPrice == nil {
s.trailingStopControl.CurrentHighestPrice = fixedpoint.Zero
} else {
s.trailingStopControl.CurrentHighestPrice = *s.state.CurrentHighestPrice
}
s.state.CurrentHighestPrice = &s.trailingStopControl.CurrentHighestPrice
}
return nil
}
func (s *Strategy) submitOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor, orderForms ...types.SubmitOrder) (types.OrderSlice, error) {
createdOrders, err := orderExecutor.SubmitOrders(ctx, orderForms...)
if err != nil {
return nil, err
}
s.orderStore.Add(createdOrders...)
s.activeOrders.Add(createdOrders...)
s.tradeCollector.Emit()
return createdOrders, nil
}
// Cancel order
func (s *Strategy) cancelOrder(orderID uint64, ctx context.Context, orderExecutor bbgo.OrderExecutor) error {
// Cancel the original order
order, ok := s.orderStore.Get(orderID)
if ok {
switch order.Status {
case types.OrderStatusCanceled, types.OrderStatusRejected, types.OrderStatusFilled:
// Do nothing
default:
if err := orderExecutor.CancelOrders(ctx, order); err != nil {
return err
}
}
}
return nil
return s.orderExecutor.SubmitOrders(ctx, orderForms...)
}
var slippageModifier = fixedpoint.NewFromFloat(1.003)
@ -389,27 +324,35 @@ func (s *Strategy) calculateQuantity(session *bbgo.ExchangeSession, side types.S
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
s.orderExecutor = orderExecutor
s.session = session
instanceID := s.InstanceID()
if s.Position == nil {
s.Position = types.NewPositionFromMarket(s.Market)
}
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
// trade stats
if s.TradeStats == nil {
s.TradeStats = &types.TradeStats{}
}
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.orderExecutor.BindEnvironment(s.Environment)
s.orderExecutor.BindProfitStats(s.ProfitStats)
s.orderExecutor.BindTradeStats(s.TradeStats)
s.orderExecutor.Bind()
// StrategyController
s.Status = types.StrategyStatusRunning
s.OnSuspend(func() {
// Cancel all order
if err := s.activeOrders.GracefulCancel(ctx, session.Exchange); err != nil {
errMsg := fmt.Sprintf("Not all %s orders are cancelled! Please check again.", s.Symbol)
log.WithError(err).Errorf(errMsg)
bbgo.Notify(errMsg)
} else {
bbgo.Notify("All %s orders are cancelled.", s.Symbol)
}
// Save state
if err := s.SaveState(); err != nil {
log.WithError(err).Errorf("can not save state: %+v", s.state)
} else {
log.Infof("%s state is saved.", s.Symbol)
}
_ = s.orderExecutor.GracefulCancel(ctx)
_ = s.Persistence.Sync(s)
})
s.OnEmergencyStop(func() {
@ -447,12 +390,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
log.Infof("adjusted minimal support volume to %s according to sensitivity %s", s.MinVolume.String(), s.Sensitivity.String())
}
market, ok := session.Market(s.Symbol)
if !ok {
return fmt.Errorf("market %s is not defined", s.Symbol)
}
s.Market = market
standardIndicatorSet, ok := session.StandardIndicatorSet(s.Symbol)
if !ok {
return fmt.Errorf("standardIndicatorSet is nil, symbol %s", s.Symbol)
@ -471,85 +408,33 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.longTermEMA = standardIndicatorSet.EWMA(s.LongTermMovingAverage)
}
s.orderStore = bbgo.NewOrderStore(s.Symbol)
s.orderStore.BindStream(session.UserDataStream)
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
s.activeOrders.BindStream(session.UserDataStream)
if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() {
s.trailingStopControl = &TrailingStopControl{
symbol: s.Symbol,
market: s.Market,
marginSideEffect: s.MarginOrderSideEffect,
CurrentHighestPrice: fixedpoint.Zero,
trailingStopCallbackRatio: s.TrailingStopTarget.TrailingStopCallbackRatio,
minimumProfitPercentage: s.TrailingStopTarget.MinimumProfitPercentage,
CurrentHighestPrice: s.CurrentHighestPrice,
}
}
if err := s.LoadState(); err != nil {
return err
} else {
bbgo.Notify("%s state is restored => %+v", s.Symbol, s.state)
}
s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore)
if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() {
// Update trailing stop when the position changes
s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
// StrategyController
if s.Status != types.StrategyStatusRunning {
return
}
if position.Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position
// Cancel the original order
if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil {
log.WithError(err).Errorf("Can not cancel the original trailing stop order!")
}
s.trailingStopControl.OrderID = 0
// Calculate minimum target price
var minTargetPrice = fixedpoint.Zero
if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 {
minTargetPrice = position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage))
}
// Place new order if the target price is higher than the minimum target price
if s.trailingStopControl.IsHigherThanMin(minTargetPrice) {
orderForm := s.trailingStopControl.GenerateStopOrder(position.Base)
orders, err := s.submitOrders(ctx, orderExecutor, orderForm)
if err != nil {
log.WithError(err).Error("submit profit trailing stop order error")
bbgo.Notify("submit %s profit trailing stop order error", s.Symbol)
} else {
orderIds := orders.IDs()
if len(orderIds) > 0 {
s.trailingStopControl.OrderID = orderIds[0]
} else {
log.Error("submit profit trailing stop order error. unknown error")
bbgo.Notify("submit %s profit trailing stop order error", s.Symbol)
s.trailingStopControl.OrderID = 0
}
}
}
}
// Save state
if err := s.SaveState(); err != nil {
log.WithError(err).Errorf("can not save state: %+v", s.state)
} else {
bbgo.Notify("%s position is saved", s.Symbol, s.state.Position)
if !position.IsLong() || position.IsDust(position.AverageCost) {
return
}
s.updateStopOrder(ctx)
})
}
s.tradeCollector.BindStream(session.UserDataStream)
// s.tradeCollector.BindStreamForBackground(session.UserDataStream)
// go s.tradeCollector.Run(ctx)
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
// StrategyController
if s.Status != types.StrategyStatusRunning {
@ -567,48 +452,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
closePrice := kline.GetClose()
highPrice := kline.GetHigh()
// check our trailing stop
if s.TrailingStopTarget.TrailingStopCallbackRatio.Sign() > 0 {
if s.state.Position.Base.Compare(s.Market.MinQuantity) <= 0 { // Without a position
// Update trailing orders with current high price
s.trailingStopControl.CurrentHighestPrice = highPrice
} else if s.trailingStopControl.CurrentHighestPrice.Compare(highPrice) < 0 || s.trailingStopControl.OrderID == 0 { // With a position or no trailing stop order yet
// Update trailing orders with current high price if it's higher
s.trailingStopControl.CurrentHighestPrice = highPrice
// Cancel the original order
if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil {
log.WithError(err).Errorf("Can not cancel the original trailing stop order!")
if s.Position.IsLong() && !s.Position.IsDust(closePrice) {
changed := s.trailingStopControl.UpdateCurrentHighestPrice(highPrice)
if changed {
// Cancel the original order
s.updateStopOrder(ctx)
}
s.trailingStopControl.OrderID = 0
// Calculate minimum target price
var minTargetPrice = fixedpoint.Zero
if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 {
minTargetPrice = s.state.Position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage))
}
// Place new order if the target price is higher than the minimum target price
if s.trailingStopControl.IsHigherThanMin(minTargetPrice) {
orderForm := s.trailingStopControl.GenerateStopOrder(s.state.Position.Base)
orders, err := s.submitOrders(ctx, orderExecutor, orderForm)
if err != nil || orders == nil {
log.WithError(err).Errorf("submit %s profit trailing stop order error", s.Symbol)
bbgo.Notify("submit %s profit trailing stop order error", s.Symbol)
} else {
orderIds := orders.IDs()
if len(orderIds) > 0 {
s.trailingStopControl.OrderID = orderIds[0]
} else {
log.Error("submit profit trailing stop order error. unknown error")
bbgo.Notify("submit %s profit trailing stop order error", s.Symbol)
s.trailingStopControl.OrderID = 0
}
}
}
}
// Save state
if err := s.SaveState(); err != nil {
log.WithError(err).Errorf("can not save state: %+v", s.state)
}
}
@ -679,7 +530,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
orderForm := types.SubmitOrder{
Symbol: s.Symbol,
Market: market,
Market: s.Market,
Side: types.SideTypeBuy,
Type: types.OrderTypeMarket,
Quantity: quantity,
@ -697,12 +548,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
log.WithError(err).Error("submit order error")
return
}
// Save state
if err := s.SaveState(); err != nil {
log.WithError(err).Errorf("can not save state: %+v", s.state)
} else {
bbgo.Notify("%s position is saved", s.Symbol, s.state.Position)
}
if s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { // submit fixed target orders
var targetOrders []types.SubmitOrder
@ -711,17 +556,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
targetQuantity := quantity.Mul(target.QuantityPercentage)
targetQuoteQuantity := targetPrice.Mul(targetQuantity)
if targetQuoteQuantity.Compare(market.MinNotional) <= 0 {
if targetQuoteQuantity.Compare(s.Market.MinNotional) <= 0 {
continue
}
if targetQuantity.Compare(market.MinQuantity) <= 0 {
if targetQuantity.Compare(s.Market.MinQuantity) <= 0 {
continue
}
targetOrders = append(targetOrders, types.SubmitOrder{
Symbol: kline.Symbol,
Market: market,
Market: s.Market,
Type: types.OrderTypeLimit,
Side: types.SideTypeSell,
Price: targetPrice,
@ -732,14 +577,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
})
}
if _, err := s.submitOrders(ctx, orderExecutor, targetOrders...); err != nil {
log.WithError(err).Error("submit profit target order error")
bbgo.Notify("submit %s profit trailing stop order error", s.Symbol)
return
_, err = s.orderExecutor.SubmitOrders(ctx, targetOrders...)
if err != nil {
bbgo.Notify("submit %s profit trailing stop order error: %s", s.Symbol, err.Error())
}
}
s.tradeCollector.Process()
})
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
@ -747,24 +589,43 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// Cancel trailing stop order
if s.TrailingStopTarget.TrailingStopCallbackRatio.Sign() > 0 {
// 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)
bbgo.Notify(errMsg)
} else {
bbgo.Notify("All {s.Symbol} orders are cancelled.")
}
s.trailingStopControl.OrderID = 0
}
if err := s.SaveState(); err != nil {
log.WithError(err).Errorf("can not save state: %+v", s.state)
} else {
bbgo.Notify("%s position is saved", s.Symbol, s.state.Position)
_ = s.orderExecutor.GracefulCancel(ctx)
}
})
return nil
}
func (s *Strategy) updateStopOrder(ctx context.Context) {
// cancel the original stop order
if s.trailingStopControl.StopOrder != nil {
if err := s.session.Exchange.CancelOrders(ctx, *s.trailingStopControl.StopOrder); err != nil {
log.WithError(err).Error("cancel order error")
}
s.trailingStopControl.StopOrder = nil
s.orderExecutor.TradeCollector().Process()
}
// Calculate minimum target price
var minTargetPrice = fixedpoint.Zero
if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 {
minTargetPrice = s.Position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage))
}
// Place new order if the target price is higher than the minimum target price
if s.trailingStopControl.IsHigherThanMin(minTargetPrice) {
orderForm := s.trailingStopControl.GenerateStopOrder(s.Position.Base)
orders, err := s.orderExecutor.SubmitOrders(ctx, orderForm)
if err != nil {
bbgo.Notify("failed to submit the trailing stop order on %s", s.Symbol)
log.WithError(err).Error("submit profit trailing stop order error")
}
if len(orders) == 0 {
log.Error("unexpected error: len(createdOrders) = 0")
return
}
s.trailingStopControl.StopOrder = &orders[0]
}
}

View File

@ -267,11 +267,12 @@ func (o Order) String() string {
orderID = strconv.FormatUint(o.OrderID, 10)
}
return fmt.Sprintf("ORDER %s | %s | %s | %s %-4s | %s/%s @ %s | %s",
return fmt.Sprintf("ORDER %s | %s | %s | %s | %s %-4s | %s/%s @ %s | %s",
o.Exchange.String(),
o.CreationTime.Time().Local().Format(time.RFC1123),
orderID,
o.Symbol,
o.Type,
o.Side,
o.ExecutedQuantity.String(),
o.Quantity.String(),

View File

@ -205,10 +205,3 @@ func (m *SyncOrderMap) Orders() (slice OrderSlice) {
}
type OrderSlice []Order
func (s OrderSlice) IDs() (ids []uint64) {
for _, o := range s {
ids = append(ids, o.OrderID)
}
return ids
}