Merge pull request #453 from c9s/fix/ftx-ioc

fix: fix ftx ioc order
This commit is contained in:
Yo-An Lin 2022-02-18 15:40:04 +08:00 committed by GitHub
commit 9e8363773d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 127 additions and 81 deletions

View File

@ -17,7 +17,7 @@ func newLimitOrder(symbol string, side types.SideType, price, quantity float64)
Type: types.OrderTypeLimit,
Quantity: fixedpoint.NewFromFloat(quantity),
Price: fixedpoint.NewFromFloat(price),
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
}
}

View File

@ -324,7 +324,7 @@ func toGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, er
Type: toGlobalOrderType(binanceOrder.Type),
Quantity: fixedpoint.MustNewFromString(binanceOrder.OrigQuantity),
Price: fixedpoint.MustNewFromString(binanceOrder.Price),
TimeInForce: string(binanceOrder.TimeInForce),
TimeInForce: types.TimeInForce(binanceOrder.TimeInForce),
},
Exchange: types.ExchangeBinance,
IsWorking: binanceOrder.IsWorking,
@ -349,7 +349,7 @@ func toGlobalFuturesOrder(futuresOrder *futures.Order, isMargin bool) (*types.Or
ClosePosition: futuresOrder.ClosePosition,
Quantity: fixedpoint.MustNewFromString(futuresOrder.OrigQuantity),
Price: fixedpoint.MustNewFromString(futuresOrder.Price),
TimeInForce: string(futuresOrder.TimeInForce),
TimeInForce: types.TimeInForce(futuresOrder.TimeInForce),
},
Exchange: types.ExchangeBinance,
OrderID: uint64(futuresOrder.OrderID),

View File

@ -121,7 +121,7 @@ func (e *ExecutionReportEvent) Order() (*types.Order, error) {
Type: toGlobalOrderType(binance.OrderType(e.OrderType)),
Quantity: e.OrderQuantity,
Price: e.OrderPrice,
TimeInForce: e.TimeInForce,
TimeInForce: types.TimeInForce(e.TimeInForce),
},
OrderID: uint64(e.OrderID),
Status: toGlobalOrderStatus(binance.OrderStatusType(e.CurrentOrderStatus)),
@ -710,7 +710,7 @@ func (e *OrderTradeUpdateEvent) OrderFutures() (*types.Order, error) {
Type: toGlobalFuturesOrderType(futures.OrderType(e.OrderTrade.OrderType)),
Quantity: e.OrderTrade.OriginalQuantity,
Price: e.OrderTrade.OriginalPrice,
TimeInForce: e.OrderTrade.TimeInForce,
TimeInForce: types.TimeInForce(e.OrderTrade.TimeInForce),
},
OrderID: uint64(e.OrderTrade.OrderId),
Status: toGlobalFuturesOrderStatus(futures.OrderStatusType(e.OrderTrade.CurrentOrderStatus)),

View File

@ -38,16 +38,26 @@ var errUnsupportedOrderStatus = fmt.Errorf("unsupported order status")
func toGlobalOrder(r order) (types.Order, error) {
// In exchange/max/convert.go, it only parses these fields.
timeInForce := types.TimeInForceGTC
if r.Ioc {
timeInForce = types.TimeInForceIOC
}
// order type definition: https://github.com/ftexchange/ftx/blob/master/rest/client.py#L122
orderType := types.OrderType(TrimUpperString(r.Type))
if orderType == types.OrderTypeLimit && r.PostOnly {
orderType = types.OrderTypeLimitMaker
}
o := types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: r.ClientId,
Symbol: toGlobalSymbol(r.Market),
Side: types.SideType(TrimUpperString(r.Side)),
// order type definition: https://github.com/ftexchange/ftx/blob/master/rest/client.py#L122
Type: types.OrderType(TrimUpperString(r.Type)),
Type: orderType,
Quantity: r.Size,
Price: r.Price,
TimeInForce: "GTC",
TimeInForce: timeInForce,
},
Exchange: types.ExchangeFTX,
IsWorking: r.Status == "open",
@ -159,21 +169,19 @@ const (
OrderTypeMarket OrderType = "market"
)
func toLocalOrderType(orderType types.OrderType) (OrderType, bool, bool, error) {
func toLocalOrderType(orderType types.OrderType) (OrderType, error) {
switch orderType {
case types.OrderTypeLimitMaker:
return OrderTypeLimit, true, false, nil
return OrderTypeLimit, nil
case types.OrderTypeLimit:
return OrderTypeLimit, false, false, nil
return OrderTypeLimit, nil
case types.OrderTypeMarket:
return OrderTypeMarket, false, false, nil
return OrderTypeMarket, nil
case types.OrderTypeIOCLimit:
return OrderTypeLimit, false, true, nil
}
return "", false, false, fmt.Errorf("order type %s not supported", orderType)
return "", fmt.Errorf("order type %s not supported", orderType)
}

View File

@ -42,7 +42,7 @@ func Test_toGlobalOrderFromOpenOrder(t *testing.T) {
assert.Equal(t, types.OrderTypeLimit, o.Type)
assert.Equal(t, "31431", o.Quantity.String())
assert.Equal(t, "0.306525", o.Price.String())
assert.Equal(t, "GTC", o.TimeInForce)
assert.Equal(t, types.TimeInForceGTC, o.TimeInForce)
assert.Equal(t, types.ExchangeFTX, o.Exchange)
assert.True(t, o.IsWorking)
assert.Equal(t, uint64(9596912), o.OrderID)
@ -102,41 +102,20 @@ func Test_toGlobalSymbol(t *testing.T) {
}
func Test_toLocalOrderTypeWithLimitMaker(t *testing.T) {
orderType, postOnly, IOC, err := toLocalOrderType(types.OrderTypeLimitMaker)
orderType, err := toLocalOrderType(types.OrderTypeLimitMaker)
assert.NoError(t, err)
assert.Equal(t, orderType, OrderTypeLimit)
assert.Equal(t, postOnly, true)
assert.Equal(t, IOC, false)
}
func Test_toLocalOrderTypeWithLimit(t *testing.T) {
orderType, postOnly, IOC, err := toLocalOrderType(types.OrderTypeLimit)
orderType, err := toLocalOrderType(types.OrderTypeLimit)
assert.NoError(t, err)
assert.Equal(t, orderType, OrderTypeLimit)
assert.Equal(t, postOnly, false)
assert.Equal(t, IOC, false)
}
func Test_toLocalOrderTypeWithMarket(t *testing.T) {
orderType, postOnly, IOC, err := toLocalOrderType(types.OrderTypeMarket)
orderType, err := toLocalOrderType(types.OrderTypeMarket)
assert.NoError(t, err)
assert.Equal(t, orderType, OrderTypeMarket)
assert.Equal(t, postOnly, false)
assert.Equal(t, IOC, false)
}
func Test_toLocalOrderTypeWithIOCLimit(t *testing.T) {
orderType, postOnly, IOC, err := toLocalOrderType(types.OrderTypeIOCLimit)
assert.NoError(t, err)
assert.Equal(t, orderType, OrderTypeLimit)
assert.Equal(t, postOnly, false)
assert.Equal(t, IOC, true)
}

View File

@ -207,8 +207,8 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap,
return balances, nil
}
//resolution field in api
//window length in seconds. options: 15, 60, 300, 900, 3600, 14400, 86400, or any multiple of 86400 up to 30*86400
// resolution field in api
// window length in seconds. options: 15, 60, 300, 900, 3600, 14400, 86400, or any multiple of 86400 up to 30*86400
var supportedIntervals = map[types.Interval]int{
types.Interval1m: 1,
types.Interval5m: 5,
@ -242,7 +242,7 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type
for {
//the fetch result is from newest to oldest
// the fetch result is from newest to oldest
endTime := currentEnd.Add(interval.Duration())
options.EndTime = &endTime
lines, err := e._queryKLines(ctx, symbol, interval, types.KLineQueryOptions{
@ -449,16 +449,15 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
// TODO: currently only support limit and market order
// TODO: support time in force
for _, so := range orders {
if so.TimeInForce != "GTC" && so.TimeInForce != "" {
return createdOrders, fmt.Errorf("unsupported TimeInForce %s. only support GTC", so.TimeInForce)
}
if err := requestLimit.Wait(ctx); err != nil {
logrus.WithError(err).Error("rate limit error")
}
orderType, postOnly, IOC, err := toLocalOrderType(so.Type)
orderType, err := toLocalOrderType(so.Type)
if err != nil {
logrus.WithError(err).Error("type error")
}
or, err := e.newRest().PlaceOrder(ctx, PlaceOrderPayload{
Market: toLocalSymbol(TrimUpperString(so.Symbol)),
Side: TrimLowerString(string(so.Side)),
@ -466,20 +465,24 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
Type: string(orderType),
Size: so.Quantity,
ReduceOnly: false,
IOC: IOC,
PostOnly: postOnly,
IOC: so.TimeInForce == types.TimeInForceIOC,
PostOnly: so.Type == types.OrderTypeLimitMaker,
ClientID: newSpotClientOrderID(so.ClientOrderID),
})
if err != nil {
return createdOrders, fmt.Errorf("failed to place order %+v: %w", so, err)
}
if !or.Success {
return createdOrders, fmt.Errorf("ftx returns placing order failure")
}
globalOrder, err := toGlobalOrder(or.Result)
if err != nil {
return createdOrders, fmt.Errorf("failed to convert response to global order")
}
createdOrders = append(createdOrders, globalOrder)
}
return createdOrders, nil
@ -619,7 +622,7 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri
logrus.WithError(err).Errorf("order rate limiter wait error")
}
//ctx context.Context, market string, interval types.Interval, limit int64, start, end time.Time
// ctx context.Context, market string, interval types.Interval, limit int64, start, end time.Time
prices, err := rest.HistoricalPrices(ctx, v.Market.LocalSymbol, types.Interval1h, 1, time.Now().Add(time.Duration(-1)*time.Hour), time.Now())
if err != nil || !prices.Success || len(prices.Result) == 0 {
continue

View File

@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
@ -17,6 +18,46 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
func integrationTestConfigured() (key, secret string, ok bool) {
var hasKey, hasSecret bool
key, hasKey = os.LookupEnv("FTX_API_KEY")
secret, hasSecret = os.LookupEnv("FTX_API_SECRET")
ok = hasKey && hasSecret && os.Getenv("TEST_FTX") == "1"
return key, secret, ok
}
func TestExchange_IOCOrder(t *testing.T) {
key, secret, ok := integrationTestConfigured()
if !ok {
t.SkipNow()
return
}
ex := NewExchange(key, secret, "")
createdOrder, err := ex.SubmitOrders(context.Background(), types.SubmitOrder{
Symbol: "LTCUSDT",
Side: types.SideTypeBuy,
Type: types.OrderTypeLimitMaker,
Quantity: fixedpoint.NewFromFloat(1.0),
Price: fixedpoint.NewFromFloat(50.0),
Market: types.Market{
Symbol: "LTCUSDT",
LocalSymbol: "LTC/USDT",
PricePrecision: 3,
VolumePrecision: 2,
QuoteCurrency: "USDT",
BaseCurrency: "LTC",
MinQuantity: fixedpoint.NewFromFloat(0.01),
StepSize: fixedpoint.NewFromFloat(0.01),
TickSize: fixedpoint.NewFromFloat(0.01),
},
TimeInForce: "IOC",
})
assert.NoError(t, err)
assert.NotEmpty(t, createdOrder)
t.Logf("created orders: %+v", createdOrder)
}
func TestExchange_QueryAccountBalances(t *testing.T) {
successResp := `
{

View File

@ -6,6 +6,7 @@ import (
"time"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
@ -78,9 +79,10 @@ func (s *Stream) Connect(ctx context.Context) error {
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
logger.WithError(err).Errorf("websocket ping goroutine is terminated")
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
logger.WithError(err).Errorf("context returned error")
}
case <-tk.C:
if err := s.ws.Conn().WriteJSON(websocketRequest{
Operation: ping,

View File

@ -8,8 +8,8 @@ import (
"time"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
func toGlobalBalanceMap(accounts []kucoinapi.Account) types.BalanceMap {
@ -213,7 +213,7 @@ func toGlobalOrder(o kucoinapi.Order) types.Order {
Quantity: o.Size,
Price: o.Price,
StopPrice: o.StopPrice,
TimeInForce: string(o.TimeInForce),
TimeInForce: types.TimeInForce(o.TimeInForce),
},
Exchange: types.ExchangeKucoin,
OrderID: hashStringID(o.ID),

View File

@ -121,7 +121,7 @@ func toGlobalOrderType(orderType max.OrderType) types.OrderType {
return types.OrderTypeStopMarket
case max.OrderTypeIOCLimit:
return types.OrderTypeIOCLimit
return types.OrderTypeLimit
case max.OrderTypePostOnly:
return types.OrderTypeLimitMaker
@ -149,9 +149,6 @@ func toLocalOrderType(orderType types.OrderType) (max.OrderType, error) {
case types.OrderTypeMarket:
return max.OrderTypeMarket, nil
case types.OrderTypeIOCLimit:
return max.OrderTypeIOCLimit, nil
}
return "", fmt.Errorf("order type %s not supported", orderType)

View File

@ -418,6 +418,11 @@ func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.Order, error) {
return nil, err
}
// case IOC type
if orderType == maxapi.OrderTypeLimit && o.TimeInForce == types.TimeInForceIOC {
orderType = maxapi.OrderTypeIOCLimit
}
var quantityString string
if o.Market.Symbol != "" {
quantityString = o.Market.FormatQuantity(o.Quantity)
@ -443,7 +448,7 @@ func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.Order, error) {
}
switch o.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker, types.OrderTypeIOCLimit:
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker:
var priceInString string
if o.Market.Symbol != "" {
priceInString = o.Market.FormatPrice(o.Price)

View File

@ -5,10 +5,11 @@ import (
"strconv"
"strings"
"github.com/c9s/bbgo/pkg/exchange/okex/okexapi"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/exchange/okex/okexapi"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
func toGlobalSymbol(symbol string) string {
@ -173,12 +174,12 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error)
return orders, err
}
timeInForce := "GTC"
timeInForce := types.TimeInForceGTC
switch orderDetail.OrderType {
case okexapi.OrderTypeFOK:
timeInForce = "FOK"
timeInForce = types.TimeInForceFOK
case okexapi.OrderTypeIOC:
timeInForce = "IOC"
timeInForce = types.TimeInForceIOC
}

View File

@ -165,7 +165,7 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types
Market: s.Market,
Quantity: quantity,
Price: price,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
}
quoteQuantity := order.Quantity.Mul(price)
if quantity.Compare(s.MinQuantity) < 0 {
@ -232,7 +232,7 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type
Market: s.Market,
Quantity: quantity,
Price: price,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
}
baseQuantity := order.Quantity
if quantity.Compare(s.MinQuantity) < 0 {
@ -312,7 +312,7 @@ func (s *Strategy) submitReverseOrder(order types.Order, session *bbgo.ExchangeS
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
}
log.Infof("submitting reverse order: %s against %s", submitOrder.String(), order.String())

View File

@ -90,7 +90,7 @@ func (s *Strategy) updateBidOrders(orderExecutor bbgo.OrderExecutor, session *bb
Market: s.Market,
Quantity: s.BaseQuantity,
Price: startPrice,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
})
startPrice = startPrice.Mul(s.Percentage)

View File

@ -217,7 +217,7 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type
Market: s.Market,
Quantity: quantity,
Price: price.Add(s.ProfitSpread),
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID,
})
baseBalance.Available = baseBalance.Available.Sub(quantity)
@ -318,7 +318,7 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types
Market: s.Market,
Quantity: quantity,
Price: price,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID,
})
balance.Available = balance.Available.Sub(quoteQuantity)
@ -434,7 +434,7 @@ func (s *Strategy) handleFilledOrder(filledOrder types.Order) {
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID,
}

View File

@ -71,7 +71,7 @@ func (stop *PercentageTargetStop) GenerateOrders(market types.Market, pos *types
Price: targetPrice,
Quantity: targetQuantity,
MarginSideEffect: target.MarginOrderSideEffect,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
})
}
@ -111,7 +111,7 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value)
Type: types.OrderTypeStopLimit,
Quantity: quantity,
MarginSideEffect: control.marginSideEffect,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
Price: targetPrice,
StopPrice: targetPrice,
@ -613,7 +613,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
Quantity: targetQuantity,
MarginSideEffect: target.MarginOrderSideEffect,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
})
}

View File

@ -360,7 +360,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
Quantity: quantity,
Price: price,
Market: s.tradingMarket,
// TimeInForce: "GTC",
// TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID,
}, types.SubmitOrder{
Symbol: s.Symbol,
@ -369,7 +369,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
Quantity: quantity,
Price: price,
Market: s.tradingMarket,
// TimeInForce: "GTC",
// TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID,
})
if err != nil {

View File

@ -380,7 +380,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
Side: types.SideTypeBuy,
Price: bidPrice,
Quantity: bidQuantity,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID,
})
@ -434,7 +434,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
Side: types.SideTypeSell,
Price: askPrice,
Quantity: askQuantity,
TimeInForce: "GTC",
TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID,
})
makerQuota.Commit()

View File

@ -14,12 +14,22 @@ import (
"github.com/c9s/bbgo/pkg/util"
)
func init() {
// make sure we can cast Order to PlainText
_ = PlainText(Order{})
_ = PlainText(&Order{})
}
type TimeInForce string
var (
TimeInForceGTC TimeInForce = "GTC"
TimeInForceIOC TimeInForce = "IOC"
TimeInForceFOK TimeInForce = "FOK"
)
// MarginOrderSideEffectType define side effect type for orders
type MarginOrderSideEffectType string
@ -29,6 +39,7 @@ var (
SideEffectTypeAutoRepay MarginOrderSideEffectType = "AUTO_REPAY"
)
func (t *MarginOrderSideEffectType) UnmarshalJSON(data []byte) error {
var s string
var err = json.Unmarshal(data, &s)
@ -64,7 +75,6 @@ const (
OrderTypeMarket OrderType = "MARKET"
OrderTypeStopLimit OrderType = "STOP_LIMIT"
OrderTypeStopMarket OrderType = "STOP_MARKET"
OrderTypeIOCLimit OrderType = "IOC_LIMIT"
)
/*
@ -115,7 +125,7 @@ type SubmitOrder struct {
Market Market `json:"-" db:"-"`
TimeInForce string `json:"timeInForce,omitempty" db:"time_in_force"` // GTC, IOC, FOK
TimeInForce TimeInForce `json:"timeInForce,omitempty" db:"time_in_force"` // GTC, IOC, FOK
GroupID uint32 `json:"groupID,omitempty"`