mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
move order submit logics to order processor
This commit is contained in:
parent
e37932bace
commit
0b58033bfb
|
@ -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)
|
||||
|
|
|
@ -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
148
bbgo/order_processor.go
Normal 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
|
||||
}
|
13
bbgo/order_processor_test.go
Normal file
13
bbgo/order_processor_test.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package bbgo
|
||||
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOrderProcessor(t *testing.T) {
|
||||
processor := &OrderProcessor{}
|
||||
_ = processor
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in New Issue
Block a user