2021-02-08 10:59:01 +00:00
|
|
|
package ftx
|
|
|
|
|
2021-03-07 04:46:26 +00:00
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
2021-03-21 12:17:41 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
2021-03-07 04:46:26 +00:00
|
|
|
|
2022-03-02 06:15:02 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi"
|
2021-03-07 04:46:26 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
2021-02-08 10:59:01 +00:00
|
|
|
|
2021-02-08 14:29:50 +00:00
|
|
|
func toGlobalCurrency(original string) string {
|
2021-03-07 04:46:26 +00:00
|
|
|
return TrimUpperString(original)
|
|
|
|
}
|
|
|
|
|
|
|
|
func toGlobalSymbol(original string) string {
|
2021-04-01 03:54:16 +00:00
|
|
|
return strings.ReplaceAll(TrimUpperString(original), "/", "")
|
2021-03-07 04:46:26 +00:00
|
|
|
}
|
|
|
|
|
2021-05-17 10:32:29 +00:00
|
|
|
func toLocalSymbol(original string) string {
|
2021-05-25 15:29:50 +00:00
|
|
|
if symbolMap[original] == "" {
|
|
|
|
return original
|
|
|
|
}
|
|
|
|
|
2021-05-17 10:32:29 +00:00
|
|
|
return symbolMap[original]
|
|
|
|
}
|
|
|
|
|
2021-03-07 04:46:26 +00:00
|
|
|
func TrimUpperString(original string) string {
|
2021-02-08 14:29:50 +00:00
|
|
|
return strings.ToUpper(strings.TrimSpace(original))
|
2021-02-08 10:59:01 +00:00
|
|
|
}
|
2021-03-07 04:46:26 +00:00
|
|
|
|
2021-03-13 01:51:16 +00:00
|
|
|
func TrimLowerString(original string) string {
|
|
|
|
return strings.ToLower(strings.TrimSpace(original))
|
|
|
|
}
|
|
|
|
|
2021-03-07 04:46:26 +00:00
|
|
|
var errUnsupportedOrderStatus = fmt.Errorf("unsupported order status")
|
|
|
|
|
2022-03-02 06:49:19 +00:00
|
|
|
func toGlobalOrderNew(r ftxapi.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(string(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(string(r.Side))),
|
|
|
|
Type: orderType,
|
|
|
|
Quantity: r.Size,
|
|
|
|
Price: r.Price,
|
|
|
|
TimeInForce: timeInForce,
|
|
|
|
},
|
|
|
|
Exchange: types.ExchangeFTX,
|
2022-03-02 06:55:22 +00:00
|
|
|
IsWorking: r.Status == ftxapi.OrderStatusOpen || r.Status == ftxapi.OrderStatusNew,
|
2022-03-02 06:49:19 +00:00
|
|
|
OrderID: uint64(r.Id),
|
|
|
|
Status: "",
|
|
|
|
ExecutedQuantity: r.FilledSize,
|
|
|
|
CreationTime: types.Time(r.CreatedAt),
|
|
|
|
UpdateTime: types.Time(r.CreatedAt),
|
|
|
|
}
|
|
|
|
|
|
|
|
s, err := toGlobalOrderStatus(r, r.Status)
|
|
|
|
o.Status = s
|
|
|
|
return o, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func toGlobalOrderStatus(o ftxapi.Order, s ftxapi.OrderStatus) (types.OrderStatus, error) {
|
|
|
|
switch s {
|
|
|
|
case ftxapi.OrderStatusNew:
|
|
|
|
return types.OrderStatusNew, nil
|
|
|
|
|
|
|
|
case ftxapi.OrderStatusOpen:
|
|
|
|
if !o.FilledSize.IsZero() {
|
|
|
|
return types.OrderStatusPartiallyFilled, nil
|
|
|
|
} else {
|
|
|
|
return types.OrderStatusNew, nil
|
|
|
|
}
|
|
|
|
case ftxapi.OrderStatusClosed:
|
|
|
|
// filled or canceled
|
|
|
|
if o.FilledSize == o.Size {
|
|
|
|
return types.OrderStatusFilled, nil
|
|
|
|
} else {
|
|
|
|
// can't distinguish it's canceled or rejected from order response, so always set to canceled
|
|
|
|
return types.OrderStatusCanceled, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", fmt.Errorf("unsupported ftx order status %s: %w", s, errUnsupportedOrderStatus)
|
|
|
|
}
|
|
|
|
|
2021-03-13 01:51:31 +00:00
|
|
|
func toGlobalOrder(r order) (types.Order, error) {
|
|
|
|
// In exchange/max/convert.go, it only parses these fields.
|
2022-02-18 06:07:29 +00:00
|
|
|
timeInForce := types.TimeInForceGTC
|
|
|
|
if r.Ioc {
|
|
|
|
timeInForce = types.TimeInForceIOC
|
|
|
|
}
|
2022-02-18 06:10:21 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-03-07 04:46:26 +00:00
|
|
|
o := types.Order{
|
|
|
|
SubmitOrder: types.SubmitOrder{
|
|
|
|
ClientOrderID: r.ClientId,
|
|
|
|
Symbol: toGlobalSymbol(r.Market),
|
|
|
|
Side: types.SideType(TrimUpperString(r.Side)),
|
2022-03-02 06:15:02 +00:00
|
|
|
Type: orderType,
|
|
|
|
Quantity: r.Size,
|
|
|
|
Price: r.Price,
|
|
|
|
TimeInForce: timeInForce,
|
2021-03-07 04:46:26 +00:00
|
|
|
},
|
2021-05-16 09:02:23 +00:00
|
|
|
Exchange: types.ExchangeFTX,
|
2021-03-07 04:46:26 +00:00
|
|
|
IsWorking: r.Status == "open",
|
|
|
|
OrderID: uint64(r.ID),
|
|
|
|
Status: "",
|
|
|
|
ExecutedQuantity: r.FilledSize,
|
2021-05-19 17:32:26 +00:00
|
|
|
CreationTime: types.Time(r.CreatedAt.Time),
|
|
|
|
UpdateTime: types.Time(r.CreatedAt.Time),
|
2021-03-07 04:46:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// `new` (accepted but not processed yet), `open`, or `closed` (filled or cancelled)
|
|
|
|
switch r.Status {
|
|
|
|
case "new":
|
|
|
|
o.Status = types.OrderStatusNew
|
|
|
|
case "open":
|
2022-02-03 04:55:25 +00:00
|
|
|
if !o.ExecutedQuantity.IsZero() {
|
2021-03-07 04:46:26 +00:00
|
|
|
o.Status = types.OrderStatusPartiallyFilled
|
|
|
|
} else {
|
|
|
|
o.Status = types.OrderStatusNew
|
|
|
|
}
|
|
|
|
case "closed":
|
|
|
|
// filled or canceled
|
2022-02-03 04:55:25 +00:00
|
|
|
if o.Quantity == o.ExecutedQuantity {
|
2021-03-07 04:46:26 +00:00
|
|
|
o.Status = types.OrderStatusFilled
|
|
|
|
} else {
|
|
|
|
// can't distinguish it's canceled or rejected from order response, so always set to canceled
|
|
|
|
o.Status = types.OrderStatusCanceled
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return types.Order{}, fmt.Errorf("unsupported status %s: %w", r.Status, errUnsupportedOrderStatus)
|
|
|
|
}
|
|
|
|
|
|
|
|
return o, nil
|
|
|
|
}
|
2021-03-21 12:17:41 +00:00
|
|
|
|
|
|
|
func toGlobalDeposit(input depositHistory) (types.Deposit, error) {
|
|
|
|
s, err := toGlobalDepositStatus(input.Status)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Warnf("assign empty string to the deposit status")
|
|
|
|
}
|
|
|
|
t := input.Time
|
2021-03-25 08:57:54 +00:00
|
|
|
if input.ConfirmedTime.Time != (time.Time{}) {
|
2021-03-21 12:17:41 +00:00
|
|
|
t = input.ConfirmedTime
|
|
|
|
}
|
|
|
|
d := types.Deposit{
|
|
|
|
GID: 0,
|
|
|
|
Exchange: types.ExchangeFTX,
|
2021-05-19 17:32:26 +00:00
|
|
|
Time: types.Time(t.Time),
|
2021-03-21 12:17:41 +00:00
|
|
|
Amount: input.Size,
|
|
|
|
Asset: toGlobalCurrency(input.Coin),
|
|
|
|
TransactionID: input.TxID,
|
|
|
|
Status: s,
|
|
|
|
Address: input.Address.Address,
|
|
|
|
AddressTag: input.Address.Tag,
|
|
|
|
}
|
|
|
|
return d, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func toGlobalDepositStatus(input string) (types.DepositStatus, error) {
|
|
|
|
// The document only list `confirmed` status
|
|
|
|
switch input {
|
|
|
|
case "confirmed", "complete":
|
|
|
|
return types.DepositSuccess, nil
|
|
|
|
}
|
|
|
|
return "", fmt.Errorf("unsupported status %s", input)
|
|
|
|
}
|
2021-03-25 08:57:54 +00:00
|
|
|
|
2022-03-02 06:15:02 +00:00
|
|
|
func toGlobalTrade(f ftxapi.Fill) (types.Trade, error) {
|
2021-03-25 08:57:54 +00:00
|
|
|
return types.Trade{
|
2022-03-02 06:15:02 +00:00
|
|
|
ID: f.Id,
|
2021-03-25 08:57:54 +00:00
|
|
|
OrderID: f.OrderId,
|
2021-05-16 09:02:23 +00:00
|
|
|
Exchange: types.ExchangeFTX,
|
2021-03-25 08:57:54 +00:00
|
|
|
Price: f.Price,
|
|
|
|
Quantity: f.Size,
|
2022-02-03 04:55:25 +00:00
|
|
|
QuoteQuantity: f.Price.Mul(f.Size),
|
2021-03-25 08:57:54 +00:00
|
|
|
Symbol: toGlobalSymbol(f.Market),
|
2022-03-02 06:15:02 +00:00
|
|
|
Side: types.SideType(strings.ToUpper(string(f.Side))),
|
|
|
|
IsBuyer: f.Side == ftxapi.SideBuy,
|
|
|
|
IsMaker: f.Liquidity == ftxapi.LiquidityMaker,
|
|
|
|
Time: types.Time(f.Time),
|
2021-03-25 08:57:54 +00:00
|
|
|
Fee: f.Fee,
|
|
|
|
FeeCurrency: f.FeeCurrency,
|
|
|
|
IsMargin: false,
|
|
|
|
IsIsolated: false,
|
2022-03-02 06:15:02 +00:00
|
|
|
IsFutures: f.Future != "",
|
2021-03-25 08:57:54 +00:00
|
|
|
}, nil
|
|
|
|
}
|
2021-03-31 10:09:13 +00:00
|
|
|
|
|
|
|
func toGlobalKLine(symbol string, interval types.Interval, h Candle) (types.KLine, error) {
|
|
|
|
return types.KLine{
|
2021-05-28 12:51:10 +00:00
|
|
|
Exchange: types.ExchangeFTX,
|
2021-04-01 03:54:16 +00:00
|
|
|
Symbol: toGlobalSymbol(symbol),
|
2021-12-15 05:04:01 +00:00
|
|
|
StartTime: types.Time(h.StartTime.Time),
|
|
|
|
EndTime: types.Time(h.StartTime.Add(interval.Duration())),
|
2021-03-31 10:09:13 +00:00
|
|
|
Interval: interval,
|
|
|
|
Open: h.Open,
|
|
|
|
Close: h.Close,
|
|
|
|
High: h.High,
|
|
|
|
Low: h.Low,
|
|
|
|
Volume: h.Volume,
|
|
|
|
Closed: true,
|
|
|
|
}, nil
|
|
|
|
}
|
2022-01-11 19:47:12 +00:00
|
|
|
|
|
|
|
type OrderType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
OrderTypeLimit OrderType = "limit"
|
|
|
|
OrderTypeMarket OrderType = "market"
|
|
|
|
)
|
|
|
|
|
2022-02-18 06:01:47 +00:00
|
|
|
func toLocalOrderType(orderType types.OrderType) (OrderType, error) {
|
2022-01-11 19:47:12 +00:00
|
|
|
switch orderType {
|
|
|
|
|
|
|
|
case types.OrderTypeLimitMaker:
|
2022-02-18 06:01:47 +00:00
|
|
|
return OrderTypeLimit, nil
|
2022-01-11 19:47:12 +00:00
|
|
|
|
|
|
|
case types.OrderTypeLimit:
|
2022-02-18 06:01:47 +00:00
|
|
|
return OrderTypeLimit, nil
|
2022-01-11 19:47:12 +00:00
|
|
|
|
|
|
|
case types.OrderTypeMarket:
|
2022-02-18 06:01:47 +00:00
|
|
|
return OrderTypeMarket, nil
|
2022-01-11 19:47:12 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-02-18 06:01:47 +00:00
|
|
|
return "", fmt.Errorf("order type %s not supported", orderType)
|
2022-01-11 19:47:12 +00:00
|
|
|
}
|