move order submit logics to order processor

This commit is contained in:
c9s 2020-09-16 14:05:03 +08:00
parent e37932bace
commit 0b58033bfb
7 changed files with 184 additions and 13 deletions

View File

@ -250,10 +250,10 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order *types.SubmitOrder) er
Symbol(order.Symbol).
Side(binance.SideType(order.Side)).
Type(orderType).
Quantity(order.Quantity)
Quantity(order.QuantityString)
if len(order.Price) > 0 {
req.Price(order.Price)
if len(order.PriceString) > 0 {
req.Price(order.PriceString)
}
if len(order.TimeInForce) > 0 {
req.TimeInForce(order.TimeInForce)

View File

@ -65,12 +65,12 @@ func (trader *KLineRegressionTrader) RunStrategy(ctx context.Context, strategy S
var price float64
if order.Type == types.OrderTypeLimit {
price = util.MustParseFloat(order.Price)
price = util.MustParseFloat(order.PriceString)
} else {
price = kline.GetClose()
}
volume := util.MustParseFloat(order.Quantity)
volume := util.MustParseFloat(order.QuantityString)
fee := 0.0
feeCurrency := ""

148
bbgo/order_processor.go Normal file
View File

@ -0,0 +1,148 @@
package bbgo
import (
"context"
"fmt"
"math"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/bbgo/types"
"github.com/c9s/bbgo/pkg/util"
)
var (
ErrQuoteBalanceLevelTooLow = errors.New("quote balance level is too low")
ErrInsufficientQuoteBalance = errors.New("insufficient quote balance")
ErrAssetBalanceLevelTooLow = errors.New("asset balance level too low")
ErrInsufficientAssetBalance = errors.New("insufficient asset balance")
ErrAssetBalanceLevelTooHigh = errors.New("asset balance level too high")
)
// OrderProcessor does:
// - Check quote balance
// - Check and control the order amount
// - Adjust order amount due to the minAmount configuration and maxAmount configuration
// - Canonicalize the volume precision base on the given exchange
type OrderProcessor struct {
// balance control
MinQuoteBalance float64 `json:"minQuoteBalance"`
MaxAssetBalance float64 `json:"maxBaseAssetBalance"`
MinAssetBalance float64 `json:"minBaseAssetBalance"`
// MinProfitSpread is used when submitting sell orders, it check if there the selling can make the profit.
MinProfitSpread float64 `json:"minProfitSpread"`
MaxOrderAmount float64 `json:"maxOrderAmount"`
Exchange types.Exchange
Trader *Trader
}
func (p *OrderProcessor) Submit(ctx context.Context, order *types.SubmitOrder) error {
tradingCtx := p.Trader.Context
currentPrice := tradingCtx.CurrentPrice
market := order.Market
quantity := order.Quantity
switch order.Side {
case types.SideTypeBuy:
if balance, ok := tradingCtx.Balances[market.QuoteCurrency]; ok {
if balance.Available < p.MinQuoteBalance {
return errors.Wrapf(ErrQuoteBalanceLevelTooLow, "quote balance level is too low: %s < %s",
USD.FormatMoneyFloat64(balance.Available),
USD.FormatMoneyFloat64(p.MinQuoteBalance))
}
if baseBalance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
if util.NotZero(p.MaxAssetBalance) && baseBalance.Available > p.MaxAssetBalance {
return errors.Wrapf(ErrAssetBalanceLevelTooHigh, "asset balance level is too high: %f > %f", baseBalance.Available, p.MaxAssetBalance)
}
}
available := math.Max(0.0, balance.Available-p.MinQuoteBalance)
if available < market.MinAmount {
return errors.Wrapf(ErrInsufficientQuoteBalance, "insufficient quote balance: %f < min amount %f", available, market.MinAmount)
}
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinAmount*1.01)
quantity = adjustQuantityByMaxAmount(quantity, currentPrice, available)
amount := quantity * currentPrice
if amount < market.MinAmount {
return fmt.Errorf("amount too small: %f < min amount %f", amount, market.MinAmount)
}
}
case types.SideTypeSell:
if balance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
if util.NotZero(p.MinAssetBalance) && balance.Available < p.MinAssetBalance {
return errors.Wrapf(ErrAssetBalanceLevelTooLow, "asset balance level is too low: %f > %f", balance.Available, p.MinAssetBalance)
}
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinNotional*1.01)
available := balance.Available
quantity = math.Min(quantity, available)
if quantity < market.MinQuantity {
return errors.Wrapf(ErrInsufficientAssetBalance, "insufficient asset balance: %f > minimal quantity %f", available, market.MinQuantity)
}
notional := quantity * currentPrice
if notional < tradingCtx.Market.MinNotional {
return fmt.Errorf("notional %f < min notional: %f", notional, market.MinNotional)
}
// price tick10
// 2 -> 0.01 -> 0.1
// 4 -> 0.0001 -> 0.001
tick10 := math.Pow10(-market.PricePrecision + 1)
minProfitSpread := math.Max(p.MinProfitSpread, tick10)
estimatedFee := currentPrice * 0.0015 * 2 // double the fee
targetPrice := currentPrice - estimatedFee - minProfitSpread
stockQuantity := tradingCtx.StockManager.Stocks.QuantityBelowPrice(targetPrice)
if math.Round(stockQuantity*1e8) == 0.0 {
return fmt.Errorf("profitable stock not found: target price %f, profit spread: %f", targetPrice, minProfitSpread)
}
quantity = math.Min(quantity, stockQuantity)
if quantity < market.MinLot {
return fmt.Errorf("quantity %f less than min lot %f", quantity, market.MinLot)
}
notional = quantity * currentPrice
if notional < tradingCtx.Market.MinNotional {
return fmt.Errorf("notional %f < min notional: %f", notional, market.MinNotional)
}
}
}
order.Quantity = quantity
order.QuantityString = market.FormatVolume(quantity)
return p.Exchange.SubmitOrder(ctx, order)
}
func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount float64) float64 {
// modify quantity for the min amount
amount := currentPrice * quantity
if amount < minAmount {
ratio := minAmount / amount
quantity *= ratio
}
return quantity
}
func adjustQuantityByMaxAmount(quantity float64, currentPrice float64, maxAmount float64) float64 {
amount := currentPrice * quantity
if amount > maxAmount {
ratio := maxAmount / amount
quantity *= ratio
}
return quantity
}

View File

@ -0,0 +1,13 @@
package bbgo
import (
"testing"
)
func TestOrderProcessor(t *testing.T) {
processor := &OrderProcessor{}
_ = processor
}

View File

@ -302,11 +302,11 @@ func (trader *Trader) reportPnL() {
}
func (trader *Trader) SubmitOrder(ctx context.Context, order *types.SubmitOrder) {
trader.Notifier.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.Quantity, order)
trader.Notifier.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order)
err := trader.Exchange.SubmitOrder(ctx, order)
if err != nil {
log.WithError(err).Errorf("order create error: side %s quantity: %s", order.Side, order.Quantity)
log.WithError(err).Errorf("order create error: side %s quantity: %s", order.Side, order.QuantityString)
return
}
}

View File

@ -1,8 +1,13 @@
package types
import "time"
import (
"context"
"time"
)
type Exchange interface {
QueryKLines(interval string, startFrom time.Time, endTo time.Time) []KLineOrWindow
QueryTrades(symbol string, startFrom time.Time) []Trade
SubmitOrder(ctx context.Context, order *SubmitOrder) error
}

View File

@ -17,8 +17,13 @@ type SubmitOrder struct {
Symbol string
Side SideType
Type OrderType
Quantity string
Price string
Quantity float64
Price float64
Market Market
PriceString string
QuantityString string
TimeInForce binance.TimeInForceType
}
@ -27,11 +32,11 @@ func (o *SubmitOrder) SlackAttachment() slack.Attachment {
var fields = []slack.AttachmentField{
{Title: "Symbol", Value: o.Symbol, Short: true},
{Title: "Side", Value: string(o.Side), Short: true},
{Title: "Volume", Value: o.Quantity, Short: true},
{Title: "Volume", Value: o.QuantityString, Short: true},
}
if len(o.Price) > 0 {
fields = append(fields, slack.AttachmentField{Title: "Price", Value: o.Price, Short: true})
if len(o.PriceString) > 0 {
fields = append(fields, slack.AttachmentField{Title: "Price", Value: o.PriceString, Short: true})
}
return slack.Attachment{