bollmaker: check dust order for stop

This commit is contained in:
c9s 2022-01-29 17:44:42 +08:00
parent 99af5d3971
commit 2255f3ed0a
5 changed files with 63 additions and 38 deletions

View File

@ -109,7 +109,6 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
}
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *types.Order, trades *types.Trade, err error) {
// price for checking account balance
price := o.Price
switch o.Type {
@ -119,16 +118,23 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ
price = o.Price
}
if o.Quantity < m.Market.MinQuantity {
return nil, nil, fmt.Errorf("order quantity %f is less than minQuantity %f, order: %+v", o.Quantity, m.Market.MinQuantity, o)
}
quoteQuantity := o.Quantity * price
if quoteQuantity < m.Market.MinNotional {
return nil, nil, fmt.Errorf("order amount %f is less than minNotional %f, order: %+v", quoteQuantity, m.Market.MinNotional, o)
}
switch o.Side {
case types.SideTypeBuy:
quote := price * o.Quantity
if err := m.Account.LockBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(quote)); err != nil {
if err := m.Account.LockBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(quoteQuantity)); err != nil {
return nil, nil, err
}
case types.SideTypeSell:
baseQuantity := o.Quantity
if err := m.Account.LockBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(baseQuantity)); err != nil {
if err := m.Account.LockBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(o.Quantity)); err != nil {
return nil, nil, err
}
}
@ -428,7 +434,7 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) {
m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close))
}
default: // no trade up or down
if (m.LastPrice == 0) {
if m.LastPrice == 0 {
m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close))
}

View File

@ -175,19 +175,17 @@ func (trader *Trader) SetRiskControls(riskControls *RiskControls) {
trader.riskControls = riskControls
}
func (trader *Trader) Subscribe() {
// pre-subscribe the data
for sessionName, strategies := range trader.exchangeStrategies {
session := trader.environment.sessions[sessionName]
for _, strategy := range strategies {
for _, strategy := range strategies {
if initializer, ok := strategy.(StrategyInitializer); ok {
initializer.Initialize()
if err := initializer.Initialize(); err != nil {
panic(err)
}
}
if subscriber, ok := strategy.(ExchangeSessionSubscriber); ok {
subscriber.Subscribe(session)
} else {
@ -197,9 +195,9 @@ func (trader *Trader) Subscribe() {
}
for _, strategy := range trader.crossExchangeStrategies {
for _, strategy := range strategies {
if initializer, ok := strategy.(StrategyInitializer); ok {
initializer.Initialize()
if err := initializer.Initialize(); err != nil {
panic(err)
}
}

View File

@ -90,9 +90,10 @@ func (c *TrailingStopController) Subscribe(session *bbgo.ExchangeSession) {
})
}
func (c *TrailingStopController) Setup(ctx context.Context, session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) {
func (c *TrailingStopController) Run(ctx context.Context, session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) {
// store the position
c.position = tradeCollector.Position()
c.averageCost = c.position.AverageCost
// Use trade collector to get the position update event
tradeCollector.OnPositionUpdate(func(position *types.Position) {
@ -101,11 +102,7 @@ func (c *TrailingStopController) Setup(ctx context.Context, session *bbgo.Exchan
})
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if kline.Symbol != c.Symbol {
return
}
if kline.Interval != c.Interval {
if kline.Symbol != c.Symbol || kline.Interval != c.Interval {
return
}
@ -115,7 +112,13 @@ func (c *TrailingStopController) Setup(ctx context.Context, session *bbgo.Exchan
c.latestHigh = math.Max(closePrice, c.latestHigh)
if c.Virtual {
if c.position == nil {
// if average cost is updated, we can check min profit
if c.averageCost == 0 {
return
}
// skip dust position
if c.position.Base.Abs().Float64() < c.position.Market.MinQuantity || c.position.Base.Abs().Float64()*closePrice < c.position.Market.MinNotional {
return
}
@ -124,24 +127,21 @@ func (c *TrailingStopController) Setup(ctx context.Context, session *bbgo.Exchan
return
}
// if average cost is updated, we can check min profit
if c.averageCost == 0 {
return
}
// if it's below the average cost, we skip stop
if closePrice < c.averageCost.Float64() {
return
}
// if the profit rate is defined, and it is less than our minimum profit rate, we skip stop
if c.MinProfit > 0 && changeRate(closePrice, c.averageCost.Float64()) < c.MinProfit.Float64() {
if c.MinProfit > 0 &&
(closePrice < c.averageCost.Float64() ||
changeRate(closePrice, c.averageCost.Float64()) < c.MinProfit.Float64()) {
return
}
log.Infof("trailing stop event emitted, submitting market order to stop...")
marketOrder := c.position.NewClosePositionOrder(c.ClosePosition.Float64())
if marketOrder != nil {
// skip dust order
if marketOrder.Quantity*closePrice < c.position.Market.MinNotional {
return
}
createdOrders, err := session.Exchange.SubmitOrders(ctx, *marketOrder)
if err != nil {
log.WithError(err).Errorf("stop order place error")
@ -176,12 +176,16 @@ func (c *TrailingStopController) GenerateStopOrder(stopPrice, price float64) *ty
}
quantity := math.Abs(base.Float64())
quoteQuantity := price * quantity
if c.ClosePosition > 0 {
quantity = quantity * c.ClosePosition.Float64()
}
quantity = math.Min(quantity, c.position.Market.MinQuantity)
// skip dust orders
if quantity < c.position.Market.MinQuantity || quoteQuantity < c.position.Market.MinNotional {
return nil
}
side := types.SideTypeSell
if base < 0 {
@ -317,10 +321,11 @@ func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) Initialize(session *bbgo.ExchangeSession) error {
func (s *Strategy) Initialize() error {
for _, stop := range s.Stops {
s.stopControllers = append(s.stopControllers,
NewTrailingStopController(s.Symbol, stop.TrailingStop))
NewTrailingStopController(s.Symbol, stop.TrailingStop),
)
}
return nil
}
@ -714,6 +719,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.tradeCollector.BindStream(session.UserDataStream)
for _, stopController := range s.stopControllers {
stopController.Run(ctx, session, s.tradeCollector)
}
session.UserDataStream.OnStart(func() {
if s.UseTickerPrice {
ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol)

View File

@ -127,10 +127,20 @@ type SubmitOrder struct {
}
func (o SubmitOrder) String() string {
switch o.Type {
case OrderTypeMarket:
return fmt.Sprintf("SubmitOrder %s %s %s %f", o.Symbol, o.Type, o.Side, o.Quantity)
}
return fmt.Sprintf("SubmitOrder %s %s %s %f @ %f", o.Symbol, o.Type, o.Side, o.Quantity, o.Price)
}
func (o SubmitOrder) PlainText() string {
switch o.Type {
case OrderTypeMarket:
return fmt.Sprintf("SubmitOrder %s %s %s %f", o.Symbol, o.Type, o.Side, o.Quantity)
}
return fmt.Sprintf("SubmitOrder %s %s %s %f @ %f", o.Symbol, o.Type, o.Side, o.Quantity, o.Price)
}

View File

@ -2,7 +2,6 @@ package types
import (
"fmt"
"math"
"sync"
"time"
@ -58,7 +57,10 @@ func (p *Position) NewClosePositionOrder(percentage float64) *SubmitOrder {
base := p.GetBase()
quantity := base.Float64()
quantity = quantity * percentage
quantity = math.Min(quantity, p.Market.MinQuantity)
if quantity < p.Market.MinQuantity {
return nil
}
side := SideTypeSell
if base == 0 {
return nil