mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
Bitfinex: Add subscription configuration and templating (#1597)
* Bitfinex: Correct comment about R0 OB * Bitfinex: Test config updates * Bitfinex: Add missing assets to configtest * Bitfinex: Rename GenerateDefaultSubscriptions * Bitfinex: Add Subscription configuration * Subscriptions: Document panic in templates
This commit is contained in:
@@ -104,9 +104,7 @@ const (
|
||||
bitfinexChecksumFlag = 131072
|
||||
bitfinexWsSequenceFlag = 65536
|
||||
|
||||
// CandlesTimeframeKey configures the timeframe in subscription.Subscription.Params
|
||||
CandlesTimeframeKey = "_timeframe"
|
||||
// CandlesPeriodKey configures the aggregated period in subscription.Subscription.Params
|
||||
// CandlesPeriodKey configures the Candles aggregated period for MarginFunding in subscription.Subscription.Params
|
||||
CandlesPeriodKey = "_period"
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,8 +11,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
@@ -20,6 +22,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
@@ -30,6 +33,15 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
var defaultSubscriptions = subscription.List{
|
||||
{Enabled: true, Channel: subscription.TickerChannel, Asset: asset.All},
|
||||
{Enabled: true, Channel: subscription.AllTradesChannel, Asset: asset.All},
|
||||
{Enabled: true, Channel: subscription.CandlesChannel, Asset: asset.Spot, Interval: kline.OneMin},
|
||||
{Enabled: true, Channel: subscription.CandlesChannel, Asset: asset.Margin, Interval: kline.OneMin},
|
||||
{Enabled: true, Channel: subscription.CandlesChannel, Asset: asset.MarginFunding, Interval: kline.OneMin, Params: map[string]any{CandlesPeriodKey: "p30"}},
|
||||
{Enabled: true, Channel: subscription.OrderbookChannel, Asset: asset.All, Levels: 100, Params: map[string]any{"prec": "R0"}},
|
||||
}
|
||||
|
||||
var comms = make(chan stream.Response)
|
||||
|
||||
type checksum struct {
|
||||
@@ -41,6 +53,13 @@ type checksum struct {
|
||||
var checksumStore = make(map[int]*checksum)
|
||||
var cMtx sync.Mutex
|
||||
|
||||
var subscriptionNames = map[string]string{
|
||||
subscription.TickerChannel: wsTicker,
|
||||
subscription.OrderbookChannel: wsBook,
|
||||
subscription.CandlesChannel: wsCandles,
|
||||
subscription.AllTradesChannel: wsTrades,
|
||||
}
|
||||
|
||||
// WsConnect starts a new websocket connection
|
||||
func (b *Bitfinex) WsConnect() error {
|
||||
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
||||
@@ -525,35 +544,35 @@ func (b *Bitfinex) handleWSSubscribed(respRaw []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bitfinex) handleWSChannelUpdate(c *subscription.Subscription, eventType string, d []interface{}) error {
|
||||
if c == nil {
|
||||
func (b *Bitfinex) handleWSChannelUpdate(s *subscription.Subscription, eventType string, d []interface{}) error {
|
||||
if s == nil {
|
||||
return fmt.Errorf("%w: Subscription param", common.ErrNilPointer)
|
||||
}
|
||||
|
||||
if eventType == wsChecksum {
|
||||
return b.handleWSChecksum(c, d)
|
||||
return b.handleWSChecksum(s, d)
|
||||
}
|
||||
|
||||
if eventType == wsHeartbeat {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(c.Pairs) != 1 {
|
||||
if len(s.Pairs) != 1 {
|
||||
return subscription.ErrNotSinglePair
|
||||
}
|
||||
|
||||
switch c.Channel {
|
||||
case wsBook:
|
||||
return b.handleWSBookUpdate(c, d)
|
||||
case wsCandles:
|
||||
return b.handleWSCandleUpdate(c, d)
|
||||
case wsTicker:
|
||||
return b.handleWSTickerUpdate(c, d)
|
||||
case wsTrades:
|
||||
return b.handleWSTradesUpdate(c, eventType, d)
|
||||
switch s.Channel {
|
||||
case subscription.OrderbookChannel:
|
||||
return b.handleWSBookUpdate(s, d)
|
||||
case subscription.CandlesChannel:
|
||||
return b.handleWSCandleUpdate(s, d)
|
||||
case subscription.TickerChannel:
|
||||
return b.handleWSTickerUpdate(s, d)
|
||||
case subscription.AllTradesChannel:
|
||||
return b.handleWSTradesUpdate(s, eventType, d)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s unhandled channel update: %s", b.Name, c.Channel)
|
||||
return fmt.Errorf("%s unhandled channel update: %s", b.Name, s.Channel)
|
||||
}
|
||||
|
||||
func (b *Bitfinex) handleWSChecksum(c *subscription.Subscription, d []interface{}) error {
|
||||
@@ -1669,44 +1688,20 @@ func (b *Bitfinex) resubOrderbook(c *subscription.Subscription) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
||||
func (b *Bitfinex) GenerateDefaultSubscriptions() (subscription.List, error) {
|
||||
var channels = []string{wsBook, wsTrades, wsTicker, wsCandles}
|
||||
// generateSubscriptions returns a list of subscriptions from the configured subscriptions feature
|
||||
func (b *Bitfinex) generateSubscriptions() (subscription.List, error) {
|
||||
return b.Features.Subscriptions.ExpandTemplates(b)
|
||||
}
|
||||
|
||||
var subscriptions subscription.List
|
||||
assets := b.GetAssetTypes(true)
|
||||
for i := range assets {
|
||||
if !b.IsAssetWebsocketSupported(assets[i]) {
|
||||
continue
|
||||
}
|
||||
enabledPairs, err := b.GetEnabledPairs(assets[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for j := range channels {
|
||||
for k := range enabledPairs {
|
||||
params := make(map[string]interface{})
|
||||
if channels[j] == wsBook {
|
||||
params["prec"] = "R0"
|
||||
params["len"] = "100"
|
||||
}
|
||||
|
||||
if channels[j] == wsCandles && assets[i] == asset.MarginFunding {
|
||||
params[CandlesPeriodKey] = "30"
|
||||
}
|
||||
|
||||
subscriptions = append(subscriptions, &subscription.Subscription{
|
||||
Channel: channels[j],
|
||||
Pairs: currency.Pairs{enabledPairs[k]},
|
||||
Params: params,
|
||||
Asset: assets[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return subscriptions, nil
|
||||
// GetSubscriptionTemplate returns a subscription channel template
|
||||
func (b *Bitfinex) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) {
|
||||
return template.New("master.tmpl").Funcs(sprig.FuncMap()).Funcs(template.FuncMap{
|
||||
"subToMap": subToMap,
|
||||
"removeSpotFromMargin": func(ap map[asset.Item]currency.Pairs) string {
|
||||
spotPairs, _ := b.GetEnabledPairs(asset.Spot)
|
||||
return removeSpotFromMargin(ap, spotPairs)
|
||||
},
|
||||
}).Parse(subTplText)
|
||||
}
|
||||
|
||||
// ConfigureWS to send checksums and sequence numbers
|
||||
@@ -1718,26 +1713,36 @@ func (b *Bitfinex) ConfigureWS() error {
|
||||
}
|
||||
|
||||
// Subscribe sends a websocket message to receive data from channels
|
||||
func (b *Bitfinex) Subscribe(channels subscription.List) error {
|
||||
return b.ParallelChanOp(channels, b.subscribeToChan, 1)
|
||||
func (b *Bitfinex) Subscribe(subs subscription.List) error {
|
||||
var err error
|
||||
if subs, err = subs.ExpandTemplates(b); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.ParallelChanOp(subs, b.subscribeToChan, 1)
|
||||
}
|
||||
|
||||
// Unsubscribe sends a websocket message to stop receiving data from channels
|
||||
func (b *Bitfinex) Unsubscribe(channels subscription.List) error {
|
||||
return b.ParallelChanOp(channels, b.unsubscribeFromChan, 1)
|
||||
func (b *Bitfinex) Unsubscribe(subs subscription.List) error {
|
||||
var err error
|
||||
if subs, err = subs.ExpandTemplates(b); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.ParallelChanOp(subs, b.unsubscribeFromChan, 1)
|
||||
}
|
||||
|
||||
// subscribeToChan handles a single subscription and parses the result
|
||||
// on success it adds the subscription to the websocket
|
||||
func (b *Bitfinex) subscribeToChan(chans subscription.List) error {
|
||||
if len(chans) != 1 {
|
||||
return errors.New("subscription batching limited to 1")
|
||||
func (b *Bitfinex) subscribeToChan(subs subscription.List) error {
|
||||
if len(subs) != 1 {
|
||||
return subscription.ErrNotSinglePair
|
||||
}
|
||||
|
||||
c := chans[0]
|
||||
req, err := subscribeReq(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Pairs)
|
||||
s := subs[0]
|
||||
req := map[string]any{
|
||||
"event": "subscribe",
|
||||
}
|
||||
if err := json.Unmarshal([]byte(s.QualifiedChannel), &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// subId is a single round-trip identifier that provides linking sub requests to chanIDs
|
||||
@@ -1747,23 +1752,23 @@ func (b *Bitfinex) subscribeToChan(chans subscription.List) error {
|
||||
|
||||
// Add a temporary Key so we can find this Sub when we get the resp without delay or context switch
|
||||
// Otherwise we might drop the first messages after the subscribed resp
|
||||
c.Key = subID // Note subID string type avoids conflicts with later chanID key
|
||||
if err = b.Websocket.AddSubscriptions(b.Websocket.Conn, c); err != nil {
|
||||
return fmt.Errorf("%w Channel: %s Pair: %s Error: %w", stream.ErrSubscriptionFailure, c.Channel, c.Pairs, err)
|
||||
s.Key = subID // Note subID string type avoids conflicts with later chanID key
|
||||
if err := b.Websocket.AddSubscriptions(b.Websocket.Conn, s); err != nil {
|
||||
return fmt.Errorf("%w Channel: %s Pair: %s", err, s.Channel, s.Pairs)
|
||||
}
|
||||
|
||||
// Always remove the temporary subscription keyed by subID
|
||||
defer func() {
|
||||
_ = b.Websocket.RemoveSubscriptions(b.Websocket.Conn, c)
|
||||
_ = b.Websocket.RemoveSubscriptions(b.Websocket.Conn, s)
|
||||
}()
|
||||
|
||||
respRaw, err := b.Websocket.Conn.SendMessageReturnResponse(context.TODO(), request.Unset, "subscribe:"+subID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Pairs)
|
||||
return fmt.Errorf("%w: Channel: %s Pair: %s", err, s.Channel, s.Pairs)
|
||||
}
|
||||
|
||||
if err = b.getErrResp(respRaw); err != nil {
|
||||
wErr := fmt.Errorf("%w: %w; Channel: %s Pair: %s", stream.ErrSubscriptionFailure, err, c.Channel, c.Pairs)
|
||||
wErr := fmt.Errorf("%w: Channel: %s Pair: %s", err, s.Channel, s.Pairs)
|
||||
b.Websocket.DataHandler <- wErr
|
||||
return wErr
|
||||
}
|
||||
@@ -1771,78 +1776,15 @@ func (b *Bitfinex) subscribeToChan(chans subscription.List) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// subscribeReq returns a map of request params for subscriptions
|
||||
func subscribeReq(c *subscription.Subscription) (map[string]interface{}, error) {
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("%w: Subscription param", common.ErrNilPointer)
|
||||
}
|
||||
if len(c.Pairs) != 1 {
|
||||
return nil, subscription.ErrNotSinglePair
|
||||
}
|
||||
pair := c.Pairs[0]
|
||||
req := map[string]interface{}{
|
||||
"event": "subscribe",
|
||||
"channel": c.Channel,
|
||||
}
|
||||
|
||||
for k, v := range c.Params {
|
||||
switch k {
|
||||
case CandlesPeriodKey, CandlesTimeframeKey:
|
||||
// Skip these internal Params
|
||||
case "key", "symbol":
|
||||
// Ensure user's Params aren't silently overwritten
|
||||
return nil, fmt.Errorf("%s %w", k, errParamNotAllowed)
|
||||
default:
|
||||
req[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
prefix := "t"
|
||||
if c.Asset == asset.MarginFunding {
|
||||
prefix = "f"
|
||||
}
|
||||
|
||||
needsDelimiter := pair.Len() > 6
|
||||
|
||||
var formattedPair string
|
||||
if needsDelimiter {
|
||||
formattedPair = pair.Format(currency.PairFormat{Uppercase: true, Delimiter: ":"}).String()
|
||||
} else {
|
||||
formattedPair = currency.PairFormat{Uppercase: true}.Format(pair)
|
||||
}
|
||||
|
||||
if c.Channel == wsCandles {
|
||||
timeframe := "1m"
|
||||
if t, ok := c.Params[CandlesTimeframeKey]; ok {
|
||||
if timeframe, ok = t.(string); !ok {
|
||||
return nil, common.GetTypeAssertError("string", t, "Subscription.CandlesTimeframeKey")
|
||||
}
|
||||
}
|
||||
fundingPeriod := ""
|
||||
if p, ok := c.Params[CandlesPeriodKey]; ok {
|
||||
s, cOk := p.(string)
|
||||
if !cOk {
|
||||
return nil, common.GetTypeAssertError("string", p, "Subscription.CandlesPeriodKey")
|
||||
}
|
||||
fundingPeriod = ":p" + s
|
||||
}
|
||||
req["key"] = "trade:" + timeframe + ":" + prefix + formattedPair + fundingPeriod
|
||||
} else {
|
||||
req["symbol"] = prefix + formattedPair
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// unsubscribeFromChan sends a websocket message to stop receiving data from a channel
|
||||
func (b *Bitfinex) unsubscribeFromChan(chans subscription.List) error {
|
||||
if len(chans) != 1 {
|
||||
func (b *Bitfinex) unsubscribeFromChan(subs subscription.List) error {
|
||||
if len(subs) != 1 {
|
||||
return errors.New("subscription batching limited to 1")
|
||||
}
|
||||
c := chans[0]
|
||||
chanID, ok := c.Key.(int)
|
||||
s := subs[0]
|
||||
chanID, ok := s.Key.(int)
|
||||
if !ok {
|
||||
return common.GetTypeAssertError("int", c.Key, "chanID")
|
||||
return common.GetTypeAssertError("int", s.Key, "subscription.Key")
|
||||
}
|
||||
|
||||
req := map[string]interface{}{
|
||||
@@ -1856,12 +1798,12 @@ func (b *Bitfinex) unsubscribeFromChan(chans subscription.List) error {
|
||||
}
|
||||
|
||||
if err := b.getErrResp(respRaw); err != nil {
|
||||
wErr := fmt.Errorf("%w from ChanId: %v; %w", stream.ErrUnsubscribeFailure, chanID, err)
|
||||
wErr := fmt.Errorf("%w: ChanId: %v", err, chanID)
|
||||
b.Websocket.DataHandler <- wErr
|
||||
return wErr
|
||||
}
|
||||
|
||||
return b.Websocket.RemoveSubscriptions(b.Websocket.Conn, c)
|
||||
return b.Websocket.RemoveSubscriptions(b.Websocket.Conn, s)
|
||||
}
|
||||
|
||||
// getErrResp takes a json response string and looks for an error event type
|
||||
@@ -2143,7 +2085,7 @@ func validateCRC32(book *orderbook.Base, token int) error {
|
||||
reOrderByID(book.Bids)
|
||||
reOrderByID(book.Asks)
|
||||
|
||||
// RO precision calculation is based on order ID's and amount values
|
||||
// R0 precision calculation is based on order ID's and amount values
|
||||
var bids, asks []orderbook.Tranche
|
||||
for i := range 25 {
|
||||
if i < len(book.Bids) {
|
||||
@@ -2233,3 +2175,71 @@ subSort:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// subToMap returns a json object of request params for subscriptions
|
||||
func subToMap(s *subscription.Subscription, a asset.Item, p currency.Pair) map[string]any {
|
||||
c := s.Channel
|
||||
if name, ok := subscriptionNames[s.Channel]; ok {
|
||||
c = name
|
||||
}
|
||||
req := map[string]interface{}{
|
||||
"channel": c,
|
||||
}
|
||||
|
||||
var fundingPeriod string
|
||||
for k, v := range s.Params {
|
||||
switch k {
|
||||
case CandlesPeriodKey:
|
||||
if s, ok := v.(string); !ok {
|
||||
panic(common.GetTypeAssertError("string", v, "subscription.CandlesPeriodKey"))
|
||||
} else {
|
||||
fundingPeriod = ":" + s
|
||||
}
|
||||
case "key", "symbol", "len":
|
||||
panic(fmt.Errorf("%w: %s", errParamNotAllowed, k)) // Ensure user's Params aren't silently overwritten
|
||||
default:
|
||||
req[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if s.Levels != 0 {
|
||||
req["len"] = s.Levels
|
||||
}
|
||||
|
||||
prefix := "t"
|
||||
if a == asset.MarginFunding {
|
||||
prefix = "f"
|
||||
}
|
||||
|
||||
pairFmt := currency.PairFormat{Uppercase: true}
|
||||
if needsDelimiter := p.Len() > 6; needsDelimiter {
|
||||
pairFmt.Delimiter = ":"
|
||||
}
|
||||
symbol := p.Format(pairFmt).String()
|
||||
if c == wsCandles {
|
||||
req["key"] = "trade:" + s.Interval.Short() + ":" + prefix + symbol + fundingPeriod
|
||||
} else {
|
||||
req["symbol"] = prefix + symbol
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// removeSpotFromMargin removes spot pairs from margin pairs in the supplied AssetPairs map to avoid duplicate subscriptions
|
||||
func removeSpotFromMargin(ap map[asset.Item]currency.Pairs, spotPairs currency.Pairs) string {
|
||||
if p, ok := ap[asset.Margin]; ok {
|
||||
ap[asset.Margin] = p.Remove(spotPairs...)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const subTplText = `
|
||||
{{- removeSpotFromMargin $.AssetPairs -}}
|
||||
{{ range $asset, $pairs := $.AssetPairs }}
|
||||
{{- range $p := $pairs -}}
|
||||
{{- subToMap $.S $asset $p | mustToJson }}
|
||||
{{- $.PairSeparator }}
|
||||
{{- end -}}
|
||||
{{ $.AssetSeparator }}
|
||||
{{- end -}}
|
||||
`
|
||||
|
||||
@@ -159,6 +159,7 @@ func (b *Bitfinex) SetDefaults() {
|
||||
GlobalResultLimit: 10000,
|
||||
},
|
||||
},
|
||||
Subscriptions: defaultSubscriptions.Clone(),
|
||||
}
|
||||
|
||||
b.Requester, err = request.New(b.Name,
|
||||
@@ -208,7 +209,7 @@ func (b *Bitfinex) Setup(exch *config.Exchange) error {
|
||||
Connector: b.WsConnect,
|
||||
Subscriber: b.Subscribe,
|
||||
Unsubscriber: b.Unsubscribe,
|
||||
GenerateSubscriptions: b.GenerateDefaultSubscriptions,
|
||||
GenerateSubscriptions: b.generateSubscriptions,
|
||||
Features: &b.Features.Supports.WebsocketCapabilities,
|
||||
OrderbookBufferConfig: buffer.Config{
|
||||
UpdateEntriesByID: true,
|
||||
|
||||
@@ -61,14 +61,15 @@ Example:
|
||||
|
||||
Assets and pairs should be output in the sequence in AssetPairs since text/template range function uses an sorted order for map keys.
|
||||
|
||||
Template functions may modify AssetPairs to update the subscription's pairs, e.g. Filtering out margin pairs already in spot subscription
|
||||
Template functions may modify AssetPairs to update the subscription's pairs, e.g. Filtering out margin pairs already in spot subscription.
|
||||
|
||||
We use separators like this because it allows mono-templates to decide at runtime whether to fan out.
|
||||
|
||||
See exchanges/subscription/testdata/subscriptions.tmpl for an example mono-template showcasing various features
|
||||
See exchanges/subscription/testdata/subscriptions.tmpl for an example mono-template showcasing various features.
|
||||
|
||||
Templates do not need to worry about joining around separators; Trailing separators will be stripped automatically.
|
||||
|
||||
Template functions should panic to handle errors. They are caught by text/template and turned into errors for use in `subscription.expandTemplate`.
|
||||
|
||||
## Contribution
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package subscriptionstest
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
40
testdata/configtest.json
vendored
40
testdata/configtest.json
vendored
@@ -579,19 +579,41 @@
|
||||
"uppercase": true
|
||||
},
|
||||
"useGlobalFormat": true,
|
||||
"assetTypes": [
|
||||
"spot"
|
||||
],
|
||||
"pairs": {
|
||||
"spot": {
|
||||
"assetEnabled": true,
|
||||
"enabled": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC",
|
||||
"available": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BTCEUR,BTCJPY,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,NEOUSD,NEOBTC,NEOETH,ETPUSD,ETPBTC,ETPETH,QTMUSD,QTMBTC,QTMETH,AVTUSD,AVTBTC,AVTETH,EDOUSD,EDOBTC,EDOETH,BTGUSD,BTGBTC,DATUSD,DATBTC,DATETH,QSHUSD,QSHBTC,QSHETH,YYWUSD,YYWBTC,YYWETH,GNTUSD,GNTBTC,GNTETH,SNTUSD,SNTBTC,SNTETH,IOTEUR,BATUSD,BATBTC,BATETH,MNAUSD,MNABTC,MNAETH,FUNUSD,FUNBTC,FUNETH,ZRXUSD,ZRXBTC,ZRXETH,TNBUSD,TNBBTC,TNBETH,SPKUSD,SPKBTC,SPKETH,TRXUSD,TRXBTC,TRXETH,RCNUSD,RCNBTC,RCNETH,RLCUSD,RLCBTC,RLCETH,AIDUSD,AIDBTC,AIDETH,SNGUSD,SNGBTC,SNGETH,REPUSD,REPBTC,REPETH,ELFUSD,ELFBTC,ELFETH,NECUSD,NECBTC,NECETH,BTCGBP,ETHEUR,ETHJPY,ETHGBP,NEOEUR,NEOJPY,NEOGBP,EOSEUR,EOSJPY,EOSGBP,IOTJPY,IOTGBP,IOSUSD,IOSBTC,IOSETH,AIOUSD,AIOBTC,AIOETH,REQUSD,REQBTC,REQETH,RDNUSD,RDNBTC,RDNETH,LRCUSD,LRCBTC,LRCETH,WAXUSD,WAXBTC,WAXETH,DAIUSD,DAIBTC,DAIETH,AGIUSD,AGIBTC,AGIETH,BFTUSD,BFTBTC,BFTETH,MTNUSD,MTNBTC,MTNETH,ODEUSD,ODEBTC,ODEETH,ANTUSD,ANTBTC,ANTETH,DTHUSD,DTHBTC,DTHETH,MITUSD,MITBTC,MITETH,STJUSD,STJBTC,STJETH,XLMUSD,XLMEUR,XLMJPY,XLMGBP,XLMBTC,XLMETH,XVGUSD,XVGEUR,XVGJPY,XVGGBP,XVGBTC,XVGETH,BCIUSD,BCIBTC,MKRUSD,MKRBTC,MKRETH,KNCUSD,KNCBTC,KNCETH,POAUSD,POABTC,POAETH,EVTUSD,LYMUSD,LYMBTC,LYMETH,UTKUSD,UTKBTC,UTKETH,VEEUSD,VEEBTC,VEEETH,DADUSD,DADBTC,DADETH,ORSUSD,ORSBTC,ORSETH,AUCUSD,AUCBTC,AUCETH,POYUSD,POYBTC,POYETH,FSNUSD,FSNBTC,FSNETH,CBTUSD,CBTBTC,CBTETH,ZCNUSD,ZCNBTC,ZCNETH,SENUSD,SENBTC,SENETH,NCAUSD,NCABTC,NCAETH,CNDUSD,CNDBTC,CNDETH,CTXUSD,CTXBTC,CTXETH,PAIUSD,PAIBTC,SEEUSD,SEEBTC,SEEETH,ESSUSD,ESSBTC,ESSETH,ATMUSD,ATMBTC,ATMETH,HOTUSD,HOTBTC,HOTETH,DTAUSD,DTABTC,DTAETH,IQXUSD,IQXBTC,IQXEOS,WPRUSD,WPRBTC,WPRETH,ZILUSD,ZILBTC,ZILETH,BNTUSD,BNTBTC,BNTETH,ABSUSD,ABSETH,XRAUSD,XRAETH,MANUSD,MANETH,BBNUSD,BBNETH,NIOUSD,NIOETH,DGXUSD,DGXETH,VETUSD,VETBTC,VETETH,UTNUSD,UTNETH,TKNUSD,TKNETH,GOTUSD,GOTEUR,GOTETH,XTZUSD,XTZBTC,CNNUSD,CNNETH,BOXUSD,BOXETH,TRXEUR,TRXGBP,TRXJPY,MGOUSD,MGOETH,RTEUSD,RTEETH,YGGUSD,YGGETH,MLNUSD,MLNETH,WTCUSD,WTCETH,CSXUSD,CSXETH,OMNUSD,OMNBTC,INTUSD,INTETH,DRNUSD,DRNETH,PNKUSD,PNKETH,DGBUSD,DGBBTC,BSVUSD,BSVBTC,BABUSD,BABBTC,WLOUSD,WLOXLM,VLDUSD,VLDETH,ENJUSD,ENJETH,ONLUSD,ONLETH,RBTUSD,RBTBTC,USTUSD,EUTEUR,EUTUSD,GSDUSD,UDCUSD,TSDUSD,PAXUSD,RIFUSD,RIFBTC,PASUSD,PASETH,VSYUSD,VSYBTC,ZRXDAI,MKRDAI,OMGDAI,BTTUSD,BTTBTC,BTCUST,ETHUST,CLOUSD,CLOBTC,IMPUSD,IMPETH,LTCUST,EOSUST,BABUST,SCRUSD,SCRETH,GNOUSD,GNOETH,GENUSD,GENETH,ATOUSD,ATOBTC,ATOETH,WBTUSD,XCHUSD,EUSUSD,WBTETH,XCHETH,EUSETH,LEOUSD,LEOBTC,LEOUST,LEOEOS,LEOETH,ASTUSD,ASTETH,FOAUSD,FOAETH,UFRUSD,UFRETH,ZBTUSD,ZBTUST,OKBUSD,USKUSD,GTXUSD,KANUSD,OKBUST,OKBETH,OKBBTC,USKUST,USKETH,USKBTC,USKEOS,GTXUST,KANUST,AMPUSD,ALGUSD,ALGBTC,ALGUST,BTCXCH,SWMUSD,SWMETH,TRIUSD,TRIETH,LOOUSD,LOOETH,AMPUST,DUSK:USD,DUSK:BTC,UOSUSD,UOSBTC,RRBUSD,RRBUST,DTXUSD,DTXUST,AMPBTC,FTTUSD,FTTUST,PAXUST,UDCUST,TSDUST,BTC:CNHT,UST:CNHT,CNH:CNHT,CHZUSD,CHZUST,BTCF0:USTF0,ETHF0:USTF0"
|
||||
},
|
||||
"margin": {
|
||||
"assetEnabled": false,
|
||||
"enabled": "ADA:BTC,FTM:USD",
|
||||
"available": "ADA:BTC,ADA:USD,ADA:UST,ALG:USD,ALG:UST,APE:USD,APE:UST,APT:USD,APT:UST,ATO:USD,ATO:UST,AVAX:BTC,AVAX:USD,AVAX:UST,AXS:USD,AXS:UST,BCHN:USD,BTC:EUR,BTC:EUT,BTC:GBP,BTC:JPY,BTC:USD,BTC:UST,COMP:USD,COMP:UST,DAI:USD,DOGE:BTC,DOGE:USD,DOGE:UST,DOT:BTC,DOT:USD,DOT:UST,DSH:BTC,DSH:USD,EGLD:USD,EGLD:UST,EOS:BTC,EOS:ETH,EOS:USD,EOS:UST,ETC:BTC,ETC:USD,ETC:UST,ETH:BTC,ETH:EUR,ETH:EUT,ETH:GBP,ETH:JPY,ETH:USD,ETH:UST,ETHW:USD,ETHW:UST,FIL:USD,FIL:UST,FTM:USD,FTM:UST,IOT:BTC,IOT:USD,LEO:USD,LEO:UST,LINK:USD,LINK:UST,LTC:BTC,LTC:USD,LTC:UST,MATIC:USD,MATIC:UST,MKR:USD,NEO:BTC,NEO:USD,NEO:UST,SHIB:USD,SHIB:UST,SOL:BTC,SOL:USD,SOL:UST,SUSHI:USD,SUSHI:UST,TRX:USD,TRX:UST,UNI:USD,UNI:UST,UST:USD,XAUT:BTC,XAUT:USD,XAUT:UST,XLM:BTC,XLM:USD,XMR:BTC,XMR:USD,XMR:UST,XRP:BTC,XRP:USD,XRP:UST,XTZ:BTC,XTZ:USD,XTZ:UST,YFI:USD,YFI:UST,ZEC:BTC,ZEC:USD,ZRX:USD",
|
||||
"requestFormat": {
|
||||
"uppercase": true
|
||||
},
|
||||
"configFormat": {
|
||||
"uppercase": true,
|
||||
"delimiter": ":"
|
||||
}
|
||||
},
|
||||
"marginfunding": {
|
||||
"assetEnabled": false,
|
||||
"enabled": "MKR-,AVAX-",
|
||||
"available": "MKR-,DAI-,USD-,XMR-,DOT-,UNI-,DSH-,ZEC-,ZRX-,UST-,IOT-,SOL-,SHIB-,FTM-,MATIC-,LINK-,BCHN-,EUT-,ADA-,LTC-,APE-,NEO-,APT-,LEO-,YFI-,FIL-,DOGE-,ALG-,SUSHI-,ETC-,TRX-,XTZ-,ETHW-,XRP-,EOS-,XLM-,AVAX-,XAUT-,GBP-,ETH-,BTC-,ATO-,JPY-,EGLD-,EUR-,AXS-,COMP-",
|
||||
"requestFormat": {
|
||||
"uppercase": true
|
||||
},
|
||||
"configFormat": {
|
||||
"uppercase": true,
|
||||
"delimiter": "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"authenticatedSupport": true,
|
||||
"authenticatedWebsocketApiSupport": true,
|
||||
"authenticatedSupport": false,
|
||||
"authenticatedWebsocketApiSupport": false,
|
||||
"endpoints": {
|
||||
"url": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
|
||||
"urlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
|
||||
@@ -648,7 +670,13 @@
|
||||
"iban": "DE78660700240057016801",
|
||||
"supportedCurrencies": "JPY,GBP"
|
||||
}
|
||||
]
|
||||
],
|
||||
"orderbook": {
|
||||
"verificationBypass": false,
|
||||
"websocketBufferLimit": 5,
|
||||
"websocketBufferEnabled": false,
|
||||
"publishPeriod": 10000000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Bitflyer",
|
||||
|
||||
Reference in New Issue
Block a user