Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REFACTOR: [okx] refactor order trade event by json.Unmarshal #1502

Merged
merged 1 commit into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion pkg/exchange/okex/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,25 @@ func tradeToGlobal(trade okexapi.Trade) types.Trade {
}
}

func processMarketBuySize(o *okexapi.OrderDetail) (fixedpoint.Value, error) {
switch o.State {
case okexapi.OrderStateLive, okexapi.OrderStateCanceled:
return fixedpoint.Zero, nil

case okexapi.OrderStatePartiallyFilled:
if o.FillPrice.IsZero() {
return fixedpoint.Zero, fmt.Errorf("fillPrice for a partialFilled should not be zero")
}
return o.Size.Div(o.FillPrice), nil

case okexapi.OrderStateFilled:
return o.AccumulatedFillSize, nil

default:
return fixedpoint.Zero, fmt.Errorf("unexpected status: %s", o.State)
}
}

func orderDetailToGlobal(order *okexapi.OrderDetail) (*types.Order, error) {
side := toGlobalSide(order.Side)

Expand All @@ -196,14 +215,26 @@ func orderDetailToGlobal(order *okexapi.OrderDetail) (*types.Order, error) {
return nil, err
}

size := order.Size
if order.Side == okexapi.SideTypeBuy &&
order.OrderType == okexapi.OrderTypeMarket &&
order.TargetCurrency == okexapi.TargetCurrencyQuote {

size, err = processMarketBuySize(order)
if err != nil {
return nil, err
}
}

return &types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: order.ClientOrderId,
Symbol: toGlobalSymbol(order.InstrumentID),
Side: side,
Type: orderType,
Price: order.Price,
Quantity: order.Size,
Quantity: size,
AveragePrice: order.AvgPrice,
TimeInForce: timeInForce,
},
Exchange: types.ExchangeOKEx,
Expand Down
71 changes: 71 additions & 0 deletions pkg/exchange/okex/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ func Test_orderDetailToGlobal(t *testing.T) {
assert.Equal(expOrder, order)
})

t.Run("succeeds with market/buy/targetQuoteCurrency", func(t *testing.T) {
newOrder := *openOrder
newOrder.OrderType = okexapi.OrderTypeMarket
newOrder.Side = okexapi.SideTypeBuy
newOrder.TargetCurrency = okexapi.TargetCurrencyQuote
newOrder.FillPrice = fixedpoint.NewFromFloat(100)
newOrder.Size = fixedpoint.NewFromFloat(10000)
newOrder.State = okexapi.OrderStatePartiallyFilled

newExpOrder := *expOrder
newExpOrder.Side = types.SideTypeBuy
newExpOrder.Type = types.OrderTypeMarket
newExpOrder.Quantity = fixedpoint.NewFromFloat(100)
newExpOrder.Status = types.OrderStatusPartiallyFilled
newExpOrder.OriginalStatus = string(okexapi.OrderStatePartiallyFilled)
order, err := orderDetailToGlobal(&newOrder)
assert.NoError(err)
assert.Equal(&newExpOrder, order)
})

t.Run("unexpected order status", func(t *testing.T) {
newOrder := *openOrder
newOrder.State = "xxx"
Expand Down Expand Up @@ -172,3 +192,54 @@ func Test_tradeToGlobal(t *testing.T) {
})
})
}

func Test_processMarketBuyQuantity(t *testing.T) {
var (
assert = assert.New(t)
)

t.Run("zero", func(t *testing.T) {
size, err := processMarketBuySize(&okexapi.OrderDetail{State: okexapi.OrderStateLive})
assert.NoError(err)
assert.Equal(fixedpoint.Zero, size)

size, err = processMarketBuySize(&okexapi.OrderDetail{State: okexapi.OrderStateCanceled})
assert.NoError(err)
assert.Equal(fixedpoint.Zero, size)
})

t.Run("estimated size", func(t *testing.T) {
size, err := processMarketBuySize(&okexapi.OrderDetail{
FillPrice: fixedpoint.NewFromFloat(2),
Size: fixedpoint.NewFromFloat(4),
State: okexapi.OrderStatePartiallyFilled,
})
assert.NoError(err)
assert.Equal(fixedpoint.NewFromFloat(2), size)
})

t.Run("unexpected fill price", func(t *testing.T) {
_, err := processMarketBuySize(&okexapi.OrderDetail{
FillPrice: fixedpoint.Zero,
Size: fixedpoint.NewFromFloat(4),
State: okexapi.OrderStatePartiallyFilled,
})
assert.ErrorContains(err, "fillPrice")
})

t.Run("accumulatedFillsize", func(t *testing.T) {
size, err := processMarketBuySize(&okexapi.OrderDetail{
AccumulatedFillSize: fixedpoint.NewFromFloat(1000),
State: okexapi.OrderStateFilled,
})
assert.NoError(err)
assert.Equal(fixedpoint.NewFromFloat(1000), size)
})

t.Run("unexpected status", func(t *testing.T) {
_, err := processMarketBuySize(&okexapi.OrderDetail{
State: "XXXXXXX",
})
assert.ErrorContains(err, "unexpected")
})
}
4 changes: 2 additions & 2 deletions pkg/exchange/okex/okexapi/get_order_history_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type OrderDetail struct {
Side SideType `json:"side"`
State OrderState `json:"state"`
Size fixedpoint.Value `json:"sz"`
TargetCurrency string `json:"tgtCcy"`
TargetCurrency TargetCurrency `json:"tgtCcy"`
UpdatedTime types.MillisecondTimestamp `json:"uTime"`

// Margin currency
Expand Down Expand Up @@ -69,7 +69,7 @@ type OrderDetail struct {
// Self trade prevention mode. Return "" if self trade prevention is not applicable
StpMode string `json:"stpMode"`
Tag string `json:"tag"`
TradeMode string `json:"tdMode"`
TradeMode TradeMode `json:"tdMode"`
TpOrdPx fixedpoint.Value `json:"tpOrdPx"`
TpTriggerPx fixedpoint.Value `json:"tpTriggerPx"`
TpTriggerPxType string `json:"tpTriggerPxType"`
Expand Down
88 changes: 69 additions & 19 deletions pkg/exchange/okex/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"

"github.com/valyala/fastjson"

"github.com/c9s/bbgo/pkg/exchange/okex/okexapi"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
Expand All @@ -22,7 +21,7 @@ const (
ChannelCandlePrefix Channel = "candle"
ChannelAccount Channel = "account"
ChannelMarketTrades Channel = "trades"
ChannelOrders Channel = "orders"
ChannelOrderTrades Channel = "orders"
)

type ActionType string
Expand All @@ -33,13 +32,8 @@ const (
)

func parseWebSocketEvent(in []byte) (interface{}, error) {
v, err := fastjson.ParseBytes(in)
if err != nil {
return nil, err
}

var event WebSocketEvent
err = json.Unmarshal(in, &event)
err := json.Unmarshal(in, &event)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -73,9 +67,14 @@ func parseWebSocketEvent(in []byte) (interface{}, error) {
}
return trade, nil

case ChannelOrders:
// TODO: remove fastjson
return parseOrder(v)
case ChannelOrderTrades:
var orderTrade []OrderTradeEvent
err := json.Unmarshal(event.Data, &orderTrade)
if err != nil {
return nil, err
}

return orderTrade, nil

default:
if strings.HasPrefix(string(event.Arg.Channel), string(ChannelCandlePrefix)) {
Expand Down Expand Up @@ -391,16 +390,67 @@ func parseAccount(v []byte) (*okexapi.Account, error) {
return &accounts[0], nil
}

func parseOrder(v *fastjson.Value) ([]okexapi.OrderDetails, error) {
data := v.Get("data").MarshalTo(nil)
type OrderTradeEvent struct {
okexapi.OrderDetail

Code types.StrInt64 `json:"code"`
Msg string `json:"msg"`
AmendResult string `json:"amendResult"`
ExecutionType okexapi.LiquidityType `json:"execType"`
// FillFee last filled fee amount or rebate amount:
// Negative number represents the user transaction fee charged by the platform;
// Positive number represents rebate
FillFee fixedpoint.Value `json:"fillFee"`
// FillFeeCurrency last filled fee currency or rebate currency.
// It is fee currency when fillFee is less than 0; It is rebate currency when fillFee>=0.
FillFeeCurrency string `json:"fillFeeCcy"`
// FillNotionalUsd Filled notional value in USD of order
FillNotionalUsd fixedpoint.Value `json:"fillNotionalUsd"`
FillPnl fixedpoint.Value `json:"fillPnl"`
// NotionalUsd Estimated national value in USD of order
NotionalUsd fixedpoint.Value `json:"notionalUsd"`
// ReqId Client Request ID as assigned by the client for order amendment. "" will be returned if there is no order amendment.
ReqId string `json:"reqId"`
LastPrice fixedpoint.Value `json:"lastPx"`
// QuickMgnType Quick Margin type, Only applicable to Quick Margin Mode of isolated margin
// manual, auto_borrow, auto_repay
QuickMgnType string `json:"quickMgnType"`
// AmendSource Source of the order amendation.
AmendSource string `json:"amendSource"`
// CancelSource Source of the order cancellation.
CancelSource string `json:"cancelSource"`

// Only applicable to options; return "" for other instrument types
FillPriceVolume string `json:"fillPxVol"`
FillPriceUsd string `json:"fillPxUsd"`
FillMarkVolume string `json:"fillMarkVol"`
FillFwdPrice string `json:"fillFwdPx"`
FillMarkPrice string `json:"fillMarkPx"`
}

var orderDetails []okexapi.OrderDetails
err := json.Unmarshal(data, &orderDetails)
func (o *OrderTradeEvent) toGlobalTrade() (types.Trade, error) {
side := toGlobalSide(o.Side)
tradeId, err := strconv.ParseUint(o.TradeId, 10, 64)
if err != nil {
return nil, err
return types.Trade{}, fmt.Errorf("unexpected trade id [%s] format: %w", o.TradeId, err)
}

return orderDetails, nil
return types.Trade{
ID: tradeId,
OrderID: uint64(o.OrderId),
Exchange: types.ExchangeOKEx,
Price: o.FillPrice,
Quantity: o.FillSize,
QuoteQuantity: o.FillPrice.Mul(o.FillSize),
Symbol: toGlobalSymbol(o.InstrumentID),
Side: side,
IsBuyer: side == types.SideTypeBuy,
IsMaker: o.ExecutionType == okexapi.LiquidityTypeMaker,
Time: types.Time(o.FillTime.Time()),
// charged by the platform is positive in our design, so added the `Neg()`.
Fee: o.FillFee.Neg(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fillFee default is currently negative because it is charged by the platform. However, in our design, the value should be positive, so I added the neg.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a rebate, then the result will be the opposite.

FeeCurrency: o.FeeCurrency,
FeeDiscounted: false,
}, nil
}

func toGlobalSideType(side okexapi.SideType) (types.SideType, error) {
Expand Down
Loading
Loading