mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +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).
|
Symbol(order.Symbol).
|
||||||
Side(binance.SideType(order.Side)).
|
Side(binance.SideType(order.Side)).
|
||||||
Type(orderType).
|
Type(orderType).
|
||||||
Quantity(order.Quantity)
|
Quantity(order.QuantityString)
|
||||||
|
|
||||||
if len(order.Price) > 0 {
|
if len(order.PriceString) > 0 {
|
||||||
req.Price(order.Price)
|
req.Price(order.PriceString)
|
||||||
}
|
}
|
||||||
if len(order.TimeInForce) > 0 {
|
if len(order.TimeInForce) > 0 {
|
||||||
req.TimeInForce(order.TimeInForce)
|
req.TimeInForce(order.TimeInForce)
|
||||||
|
|
|
@ -65,12 +65,12 @@ func (trader *KLineRegressionTrader) RunStrategy(ctx context.Context, strategy S
|
||||||
|
|
||||||
var price float64
|
var price float64
|
||||||
if order.Type == types.OrderTypeLimit {
|
if order.Type == types.OrderTypeLimit {
|
||||||
price = util.MustParseFloat(order.Price)
|
price = util.MustParseFloat(order.PriceString)
|
||||||
} else {
|
} else {
|
||||||
price = kline.GetClose()
|
price = kline.GetClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
volume := util.MustParseFloat(order.Quantity)
|
volume := util.MustParseFloat(order.QuantityString)
|
||||||
fee := 0.0
|
fee := 0.0
|
||||||
feeCurrency := ""
|
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) {
|
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)
|
err := trader.Exchange.SubmitOrder(ctx, order)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type Exchange interface {
|
type Exchange interface {
|
||||||
QueryKLines(interval string, startFrom time.Time, endTo time.Time) []KLineOrWindow
|
QueryKLines(interval string, startFrom time.Time, endTo time.Time) []KLineOrWindow
|
||||||
QueryTrades(symbol string, startFrom time.Time) []Trade
|
QueryTrades(symbol string, startFrom time.Time) []Trade
|
||||||
|
|
||||||
|
SubmitOrder(ctx context.Context, order *SubmitOrder) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,13 @@ type SubmitOrder struct {
|
||||||
Symbol string
|
Symbol string
|
||||||
Side SideType
|
Side SideType
|
||||||
Type OrderType
|
Type OrderType
|
||||||
Quantity string
|
Quantity float64
|
||||||
Price string
|
Price float64
|
||||||
|
|
||||||
|
Market Market
|
||||||
|
|
||||||
|
PriceString string
|
||||||
|
QuantityString string
|
||||||
|
|
||||||
TimeInForce binance.TimeInForceType
|
TimeInForce binance.TimeInForceType
|
||||||
}
|
}
|
||||||
|
@ -27,11 +32,11 @@ func (o *SubmitOrder) SlackAttachment() slack.Attachment {
|
||||||
var fields = []slack.AttachmentField{
|
var fields = []slack.AttachmentField{
|
||||||
{Title: "Symbol", Value: o.Symbol, Short: true},
|
{Title: "Symbol", Value: o.Symbol, Short: true},
|
||||||
{Title: "Side", Value: string(o.Side), 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 {
|
if len(o.PriceString) > 0 {
|
||||||
fields = append(fields, slack.AttachmentField{Title: "Price", Value: o.Price, Short: true})
|
fields = append(fields, slack.AttachmentField{Title: "Price", Value: o.PriceString, Short: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
return slack.Attachment{
|
return slack.Attachment{
|
||||||
|
|
Loading…
Reference in New Issue
Block a user