2021-02-27 10:41:46 +00:00
|
|
|
package ftx
|
|
|
|
|
2021-03-01 00:13:21 +00:00
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"math"
|
2021-03-02 14:18:41 +00:00
|
|
|
"strings"
|
2021-03-01 00:13:21 +00:00
|
|
|
"time"
|
2021-03-02 14:18:41 +00:00
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
2021-03-01 00:13:21 +00:00
|
|
|
)
|
2021-02-27 11:27:26 +00:00
|
|
|
|
2021-02-27 10:41:46 +00:00
|
|
|
type operation string
|
|
|
|
|
|
|
|
const subscribe operation = "subscribe"
|
|
|
|
const unsubscribe operation = "unsubscribe"
|
|
|
|
|
|
|
|
type channel string
|
|
|
|
|
|
|
|
const orderbook channel = "orderbook"
|
|
|
|
const trades channel = "trades"
|
|
|
|
const ticker channel = "ticker"
|
|
|
|
|
|
|
|
// {'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'}
|
|
|
|
type SubscribeRequest struct {
|
|
|
|
Operation operation `json:"op"`
|
|
|
|
Channel channel `json:"channel"`
|
|
|
|
Market string `json:"market"`
|
|
|
|
}
|
2021-02-27 11:27:26 +00:00
|
|
|
|
|
|
|
type respType string
|
|
|
|
|
|
|
|
const errRespType respType = "error"
|
|
|
|
const subscribedRespType respType = "subscribed"
|
|
|
|
const unsubscribedRespType respType = "unsubscribed"
|
|
|
|
const infoRespType respType = "info"
|
|
|
|
const partialRespType respType = "partial"
|
|
|
|
const updateRespType respType = "update"
|
|
|
|
|
|
|
|
type mandatoryFields struct {
|
|
|
|
Type respType `json:"type"`
|
|
|
|
|
|
|
|
// Channel is mandatory
|
|
|
|
Channel channel `json:"channel"`
|
|
|
|
|
|
|
|
// Market is mandatory
|
|
|
|
Market string `json:"market"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// doc: https://docs.ftx.com/#response-format
|
|
|
|
type rawResponse struct {
|
|
|
|
mandatoryFields
|
|
|
|
|
|
|
|
// The following fields are optional.
|
|
|
|
// Example 1: {"type": "error", "code": 404, "msg": "No such market: BTCUSDT"}
|
|
|
|
Code int64 `json:"code"`
|
|
|
|
Message string `json:"msg"`
|
|
|
|
Data map[string]json.RawMessage `json:"data"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r rawResponse) toSubscribedResp() subscribedResponse {
|
|
|
|
return subscribedResponse{
|
|
|
|
mandatoryFields: r.mandatoryFields,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-01 00:13:21 +00:00
|
|
|
func (r rawResponse) toSnapshotResp() (snapshotResponse, error) {
|
|
|
|
o := snapshotResponse{
|
|
|
|
mandatoryFields: r.mandatoryFields,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.Unmarshal(r.Data["action"], &o.Action); err != nil {
|
|
|
|
return snapshotResponse{}, fmt.Errorf("failed to unmarshal data.action field: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var t float64
|
|
|
|
if err := json.Unmarshal(r.Data["time"], &t); err != nil {
|
|
|
|
return snapshotResponse{}, fmt.Errorf("failed to unmarshal data.time field: %w", err)
|
|
|
|
}
|
|
|
|
sec, dec := math.Modf(t)
|
|
|
|
o.Time = time.Unix(int64(sec), int64(dec*1e9))
|
|
|
|
|
|
|
|
if err := json.Unmarshal(r.Data["checksum"], &o.Checksum); err != nil {
|
|
|
|
return snapshotResponse{}, fmt.Errorf("failed to unmarshal data.checksum field: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.Unmarshal(r.Data["bids"], &o.Bids); err != nil {
|
|
|
|
return snapshotResponse{}, fmt.Errorf("failed to unmarshal data.bids field: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.Unmarshal(r.Data["asks"], &o.Asks); err != nil {
|
|
|
|
return snapshotResponse{}, fmt.Errorf("failed to unmarshal data.asks field: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return o, nil
|
|
|
|
}
|
|
|
|
|
2021-02-27 11:27:26 +00:00
|
|
|
// {"type": "subscribed", "channel": "orderbook", "market": "BTC/USDT"}
|
|
|
|
type subscribedResponse struct {
|
|
|
|
mandatoryFields
|
|
|
|
}
|
2021-03-01 00:13:21 +00:00
|
|
|
|
|
|
|
type snapshotResponse struct {
|
|
|
|
mandatoryFields
|
|
|
|
|
|
|
|
Action string
|
|
|
|
|
|
|
|
Time time.Time
|
|
|
|
|
|
|
|
Checksum int64
|
|
|
|
|
|
|
|
// Best 100 orders
|
|
|
|
Bids [][]float64
|
|
|
|
|
|
|
|
// Best 100 orders
|
|
|
|
Asks [][]float64
|
|
|
|
}
|
2021-03-02 14:18:41 +00:00
|
|
|
|
|
|
|
func (r snapshotResponse) toGlobalOrderBook() types.OrderBook {
|
|
|
|
return types.OrderBook{
|
|
|
|
// ex. BTC/USDT
|
|
|
|
Symbol: strings.ToUpper(r.Market),
|
|
|
|
Bids: toPriceVolumeSlice(r.Bids),
|
|
|
|
Asks: toPriceVolumeSlice(r.Asks),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func toPriceVolumeSlice(orders [][]float64) types.PriceVolumeSlice {
|
|
|
|
var pv types.PriceVolumeSlice
|
|
|
|
for _, o := range orders {
|
|
|
|
pv = append(pv, types.PriceVolume{
|
|
|
|
Price: fixedpoint.NewFromFloat(o[0]),
|
|
|
|
Volume: fixedpoint.NewFromFloat(o[1]),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return pv
|
|
|
|
}
|