2020-10-25 10:26:10 +00:00
|
|
|
package max
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/exchange/max/maxapi"
|
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
|
|
|
func toGlobalCurrency(currency string) string {
|
|
|
|
return strings.ToUpper(currency)
|
|
|
|
}
|
|
|
|
|
|
|
|
func toLocalCurrency(currency string) string {
|
|
|
|
return strings.ToLower(currency)
|
|
|
|
}
|
|
|
|
|
|
|
|
func toLocalSymbol(symbol string) string {
|
|
|
|
return strings.ToLower(symbol)
|
|
|
|
}
|
|
|
|
|
|
|
|
func toGlobalSymbol(symbol string) string {
|
|
|
|
return strings.ToUpper(symbol)
|
|
|
|
}
|
|
|
|
|
|
|
|
func toLocalSideType(side types.SideType) string {
|
|
|
|
return strings.ToLower(string(side))
|
|
|
|
}
|
|
|
|
|
|
|
|
func toGlobalSideType(v string) types.SideType {
|
|
|
|
switch strings.ToLower(v) {
|
|
|
|
case "bid", "buy":
|
|
|
|
return types.SideTypeBuy
|
|
|
|
|
|
|
|
case "ask", "sell":
|
|
|
|
return types.SideTypeSell
|
|
|
|
|
|
|
|
case "self-trade":
|
|
|
|
return types.SideTypeSelf
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return types.SideType(v)
|
|
|
|
}
|
|
|
|
|
2021-02-23 08:39:48 +00:00
|
|
|
func toGlobalRewards(maxRewards []max.Reward) ([]types.Reward, error) {
|
|
|
|
// convert to global reward
|
|
|
|
var rewards []types.Reward
|
|
|
|
for _, r := range maxRewards {
|
|
|
|
// ignore "accepted"
|
|
|
|
if r.State != "done" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
reward, err := r.Reward()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
rewards = append(rewards, *reward)
|
|
|
|
}
|
|
|
|
|
|
|
|
return rewards, nil
|
|
|
|
}
|
|
|
|
|
2021-12-26 07:58:12 +00:00
|
|
|
func toGlobalOrderStatus(orderState max.OrderState, executedVolume, remainingVolume fixedpoint.Value) types.OrderStatus {
|
|
|
|
switch orderState {
|
2020-10-25 10:26:10 +00:00
|
|
|
|
2021-12-26 16:21:52 +00:00
|
|
|
case max.OrderStateCancel:
|
|
|
|
return types.OrderStatusCanceled
|
|
|
|
|
|
|
|
case max.OrderStateFinalizing, max.OrderStateDone:
|
2022-02-03 04:55:25 +00:00
|
|
|
if executedVolume.IsZero() {
|
2021-12-26 16:21:52 +00:00
|
|
|
return types.OrderStatusCanceled
|
2022-02-03 04:55:25 +00:00
|
|
|
} else if remainingVolume.IsZero() {
|
2020-10-25 10:26:10 +00:00
|
|
|
return types.OrderStatusFilled
|
|
|
|
}
|
|
|
|
|
|
|
|
return types.OrderStatusFilled
|
|
|
|
|
|
|
|
case max.OrderStateWait:
|
2022-02-03 04:55:25 +00:00
|
|
|
if executedVolume.Sign() > 0 && remainingVolume.Sign() > 0 {
|
2020-10-25 10:26:10 +00:00
|
|
|
return types.OrderStatusPartiallyFilled
|
|
|
|
}
|
|
|
|
|
|
|
|
return types.OrderStatusNew
|
|
|
|
|
|
|
|
case max.OrderStateConvert:
|
2022-02-03 04:55:25 +00:00
|
|
|
if executedVolume.Sign() > 0 && remainingVolume.Sign() > 0 {
|
2020-10-25 10:26:10 +00:00
|
|
|
return types.OrderStatusPartiallyFilled
|
|
|
|
}
|
|
|
|
|
|
|
|
return types.OrderStatusNew
|
|
|
|
|
|
|
|
case max.OrderStateFailed:
|
|
|
|
return types.OrderStatusRejected
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
log.Errorf("unknown order status: %v", orderState)
|
2021-12-26 07:58:12 +00:00
|
|
|
return types.OrderStatus(orderState)
|
2020-10-25 10:26:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func toGlobalOrderType(orderType max.OrderType) types.OrderType {
|
|
|
|
switch orderType {
|
|
|
|
case max.OrderTypeLimit:
|
|
|
|
return types.OrderTypeLimit
|
|
|
|
|
|
|
|
case max.OrderTypeMarket:
|
|
|
|
return types.OrderTypeMarket
|
|
|
|
|
|
|
|
case max.OrderTypeStopLimit:
|
|
|
|
return types.OrderTypeStopLimit
|
|
|
|
|
|
|
|
case max.OrderTypeStopMarket:
|
|
|
|
return types.OrderTypeStopMarket
|
|
|
|
|
2021-04-11 12:07:05 +00:00
|
|
|
case max.OrderTypeIOCLimit:
|
2022-02-18 05:57:47 +00:00
|
|
|
return types.OrderTypeLimit
|
2021-04-11 12:07:05 +00:00
|
|
|
|
2021-05-14 03:53:07 +00:00
|
|
|
case max.OrderTypePostOnly:
|
|
|
|
return types.OrderTypeLimitMaker
|
|
|
|
|
2020-10-25 10:26:10 +00:00
|
|
|
}
|
|
|
|
|
2022-01-02 04:00:06 +00:00
|
|
|
log.Errorf("order convert error, unknown order type: %v", orderType)
|
2020-10-25 10:26:10 +00:00
|
|
|
return types.OrderType(orderType)
|
|
|
|
}
|
|
|
|
|
|
|
|
func toLocalOrderType(orderType types.OrderType) (max.OrderType, error) {
|
|
|
|
switch orderType {
|
|
|
|
|
|
|
|
case types.OrderTypeStopLimit:
|
|
|
|
return max.OrderTypeStopLimit, nil
|
|
|
|
|
|
|
|
case types.OrderTypeStopMarket:
|
|
|
|
return max.OrderTypeStopMarket, nil
|
|
|
|
|
2021-03-22 09:25:25 +00:00
|
|
|
case types.OrderTypeLimitMaker:
|
|
|
|
return max.OrderTypePostOnly, nil
|
|
|
|
|
2020-10-25 10:26:10 +00:00
|
|
|
case types.OrderTypeLimit:
|
|
|
|
return max.OrderTypeLimit, nil
|
|
|
|
|
|
|
|
case types.OrderTypeMarket:
|
|
|
|
return max.OrderTypeMarket, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", fmt.Errorf("order type %s not supported", orderType)
|
|
|
|
}
|
|
|
|
|
2020-12-29 08:00:03 +00:00
|
|
|
func toGlobalOrders(maxOrders []max.Order) (orders []types.Order, err error) {
|
|
|
|
for _, localOrder := range maxOrders {
|
|
|
|
o, err := toGlobalOrder(localOrder)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("order convert error")
|
|
|
|
}
|
|
|
|
|
|
|
|
orders = append(orders, *o)
|
|
|
|
}
|
|
|
|
|
|
|
|
return orders, err
|
|
|
|
}
|
|
|
|
|
2020-10-25 10:26:10 +00:00
|
|
|
func toGlobalOrder(maxOrder max.Order) (*types.Order, error) {
|
2022-05-26 11:52:38 +00:00
|
|
|
executedVolume := maxOrder.ExecutedVolume
|
|
|
|
remainingVolume := maxOrder.RemainingVolume
|
2020-10-25 10:26:10 +00:00
|
|
|
|
|
|
|
return &types.Order{
|
|
|
|
SubmitOrder: types.SubmitOrder{
|
2020-10-25 16:26:17 +00:00
|
|
|
ClientOrderID: maxOrder.ClientOID,
|
2020-11-03 09:55:20 +00:00
|
|
|
Symbol: toGlobalSymbol(maxOrder.Market),
|
|
|
|
Side: toGlobalSideType(maxOrder.Side),
|
|
|
|
Type: toGlobalOrderType(maxOrder.OrderType),
|
2022-05-26 11:52:38 +00:00
|
|
|
Quantity: maxOrder.Volume,
|
|
|
|
Price: maxOrder.Price,
|
|
|
|
TimeInForce: types.TimeInForceGTC, // MAX only supports GTC
|
2021-01-23 09:17:46 +00:00
|
|
|
GroupID: maxOrder.GroupID,
|
2020-10-25 10:26:10 +00:00
|
|
|
},
|
2021-05-16 09:02:23 +00:00
|
|
|
Exchange: types.ExchangeMax,
|
2022-05-26 11:52:38 +00:00
|
|
|
IsWorking: maxOrder.State == max.OrderStateWait,
|
2020-10-25 10:26:10 +00:00
|
|
|
OrderID: maxOrder.ID,
|
|
|
|
Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume),
|
2022-02-03 04:55:25 +00:00
|
|
|
ExecutedQuantity: executedVolume,
|
2022-01-24 15:18:52 +00:00
|
|
|
CreationTime: types.Time(maxOrder.CreatedAtMs.Time()),
|
|
|
|
UpdateTime: types.Time(maxOrder.CreatedAtMs.Time()),
|
2020-10-25 10:26:10 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func toGlobalTrade(t max.Trade) (*types.Trade, error) {
|
|
|
|
// skip trade ID that is the same. however this should not happen
|
|
|
|
var side = toGlobalSideType(t.Side)
|
|
|
|
|
|
|
|
// trade time
|
2022-04-21 06:52:44 +00:00
|
|
|
mts := t.CreatedAtMilliSeconds
|
2020-10-25 10:26:10 +00:00
|
|
|
|
2022-02-03 04:55:25 +00:00
|
|
|
price, err := fixedpoint.NewFromString(t.Price)
|
2020-10-25 10:26:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-02-03 04:55:25 +00:00
|
|
|
quantity, err := fixedpoint.NewFromString(t.Volume)
|
2020-10-25 10:26:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-02-03 04:55:25 +00:00
|
|
|
quoteQuantity, err := fixedpoint.NewFromString(t.Funds)
|
2020-10-25 10:26:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-02-03 04:55:25 +00:00
|
|
|
fee, err := fixedpoint.NewFromString(t.Fee)
|
2020-10-25 10:26:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &types.Trade{
|
2021-12-23 05:15:27 +00:00
|
|
|
ID: t.ID,
|
2020-11-03 09:55:20 +00:00
|
|
|
OrderID: t.OrderID,
|
2020-10-25 10:26:10 +00:00
|
|
|
Price: price,
|
|
|
|
Symbol: toGlobalSymbol(t.Market),
|
|
|
|
Exchange: "max",
|
|
|
|
Quantity: quantity,
|
|
|
|
Side: side,
|
|
|
|
IsBuyer: t.IsBuyer(),
|
|
|
|
IsMaker: t.IsMaker(),
|
|
|
|
Fee: fee,
|
|
|
|
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
|
|
|
|
QuoteQuantity: quoteQuantity,
|
2021-05-19 17:32:26 +00:00
|
|
|
Time: types.Time(mts),
|
2020-10-25 10:26:10 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func toGlobalDepositStatus(a string) types.DepositStatus {
|
|
|
|
switch a {
|
|
|
|
case "submitting", "submitted", "checking":
|
|
|
|
return types.DepositPending
|
|
|
|
|
|
|
|
case "accepted":
|
|
|
|
return types.DepositSuccess
|
|
|
|
|
|
|
|
case "rejected":
|
|
|
|
return types.DepositRejected
|
|
|
|
|
|
|
|
case "canceled":
|
|
|
|
return types.DepositCancelled
|
|
|
|
|
|
|
|
case "suspect", "refunded":
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return types.DepositStatus(a)
|
|
|
|
}
|
2022-01-02 04:20:38 +00:00
|
|
|
|
|
|
|
func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) {
|
|
|
|
// skip trade ID that is the same. however this should not happen
|
|
|
|
var side = toGlobalSideType(t.Side)
|
|
|
|
|
|
|
|
// trade time
|
|
|
|
mts := time.Unix(0, t.Timestamp*int64(time.Millisecond))
|
|
|
|
|
2022-02-03 04:55:25 +00:00
|
|
|
price, err := fixedpoint.NewFromString(t.Price)
|
2022-01-02 04:20:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-02-03 04:55:25 +00:00
|
|
|
quantity, err := fixedpoint.NewFromString(t.Volume)
|
2022-01-02 04:20:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-02-03 04:55:25 +00:00
|
|
|
quoteQuantity := price.Mul(quantity)
|
2022-01-02 04:20:38 +00:00
|
|
|
|
2022-02-03 04:55:25 +00:00
|
|
|
fee, err := fixedpoint.NewFromString(t.Fee)
|
2022-01-02 04:20:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &types.Trade{
|
|
|
|
ID: t.ID,
|
|
|
|
OrderID: t.OrderID,
|
|
|
|
Symbol: toGlobalSymbol(t.Market),
|
|
|
|
Exchange: types.ExchangeMax,
|
|
|
|
Price: price,
|
|
|
|
Quantity: quantity,
|
|
|
|
Side: side,
|
|
|
|
IsBuyer: side == types.SideTypeBuy,
|
|
|
|
IsMaker: t.Maker,
|
|
|
|
Fee: fee,
|
|
|
|
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
|
|
|
|
QuoteQuantity: quoteQuantity,
|
|
|
|
Time: types.Time(mts),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
|
|
|
|
executedVolume, err := fixedpoint.NewFromString(u.ExecutedVolume)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
remainingVolume, err := fixedpoint.NewFromString(u.RemainingVolume)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-03-06 10:37:34 +00:00
|
|
|
timeInForce := types.TimeInForceGTC
|
|
|
|
if u.OrderType == max.OrderTypeIOCLimit {
|
|
|
|
timeInForce = types.TimeInForceIOC
|
|
|
|
}
|
|
|
|
|
2022-01-02 04:20:38 +00:00
|
|
|
return &types.Order{
|
|
|
|
SubmitOrder: types.SubmitOrder{
|
|
|
|
ClientOrderID: u.ClientOID,
|
|
|
|
Symbol: toGlobalSymbol(u.Market),
|
|
|
|
Side: toGlobalSideType(u.Side),
|
|
|
|
Type: toGlobalOrderType(u.OrderType),
|
2022-02-03 04:55:25 +00:00
|
|
|
Quantity: fixedpoint.MustNewFromString(u.Volume),
|
|
|
|
Price: fixedpoint.MustNewFromString(u.Price),
|
|
|
|
StopPrice: fixedpoint.MustNewFromString(u.StopPrice),
|
2022-03-06 10:37:34 +00:00
|
|
|
TimeInForce: timeInForce, // MAX only supports GTC
|
2022-01-02 04:20:38 +00:00
|
|
|
GroupID: u.GroupID,
|
|
|
|
},
|
|
|
|
Exchange: types.ExchangeMax,
|
|
|
|
OrderID: u.ID,
|
|
|
|
Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume),
|
2022-02-03 04:55:25 +00:00
|
|
|
ExecutedQuantity: executedVolume,
|
2022-01-02 04:20:38 +00:00
|
|
|
CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
|
2022-03-06 10:33:21 +00:00
|
|
|
UpdateTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
|
2022-01-02 04:20:38 +00:00
|
|
|
}, nil
|
|
|
|
}
|