mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* modernise: Run new gopls modernise tool against codebase
* Address shazbert's nits
* apichecker, gctcli: Simplify HTML scraping functions and improve depth limit handling
* refactor: Create minSyncInterval const and update order book limit handling for binance and binanceUS
* refactor: Various slice usage improvements and rename TODO
* tranches: Revert deleteByID changes due to performance decrease
Shazbert was a F1 driver in a past lifetime 🏎️
* tranches: Simply retrieve copy
Thanks to shazbert
* documentation: Sort contributors list by contributions
* tranches: Remove deadcode in deleteByID
201 lines
5.7 KiB
Go
201 lines
5.7 KiB
Go
package subscription
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
)
|
|
|
|
const (
|
|
deviceControl = "\x11"
|
|
groupSeparator = "\x1D"
|
|
recordSeparator = "\x1E"
|
|
)
|
|
|
|
var (
|
|
errAssetRecords = errors.New("subscription template did not generate the expected number of asset records")
|
|
errPairRecords = errors.New("subscription template did not generate the expected number of pair records")
|
|
errTooManyBatchSizePerAsset = errors.New("more than one BatchSize directive inside an AssetSeparator")
|
|
errNoTemplateContent = errors.New("subscription template did not generate content")
|
|
errInvalidTemplate = errors.New("GetSubscriptionTemplate did not return a template")
|
|
)
|
|
|
|
type tplCtx struct {
|
|
S *Subscription
|
|
AssetPairs assetPairs
|
|
PairSeparator string
|
|
AssetSeparator string
|
|
BatchSize string
|
|
}
|
|
|
|
// ExpandTemplates returns a list of Subscriptions with Template expanded
|
|
// May be called on already expanded subscriptions: Passes $s through unprocessed if QualifiedChannel is already populated
|
|
// Calls e.GetSubscriptionTemplate to find a template for each subscription
|
|
// Filters out Authenticated subscriptions if !e.CanUseAuthenticatedEndpoints
|
|
// See README.md for more details
|
|
func (l List) ExpandTemplates(e IExchange) (List, error) {
|
|
if !slices.ContainsFunc(l, func(s *Subscription) bool { return s.QualifiedChannel == "" }) {
|
|
// Empty list, or already processed
|
|
return slices.Clone(l), nil
|
|
}
|
|
|
|
if !e.CanUseAuthenticatedWebsocketEndpoints() {
|
|
n := List{}
|
|
for _, s := range l {
|
|
if !s.Authenticated {
|
|
n = append(n, s)
|
|
}
|
|
}
|
|
l = n
|
|
}
|
|
|
|
ap, err := l.assetPairs(e)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
assets := make(asset.Items, 0, len(ap))
|
|
for k := range ap {
|
|
assets = append(assets, k)
|
|
}
|
|
slices.Sort(assets) // text/template ranges maps in sorted order
|
|
|
|
subs := List{}
|
|
for _, s := range l {
|
|
expanded, err2 := expandTemplate(e, s, maps.Clone(ap), assets)
|
|
if err2 != nil {
|
|
err = common.AppendError(err, fmt.Errorf("%s: %w", s, err2))
|
|
} else {
|
|
subs = append(subs, expanded...)
|
|
}
|
|
}
|
|
|
|
return subs, err
|
|
}
|
|
|
|
func expandTemplate(e IExchange, s *Subscription, ap assetPairs, assets asset.Items) (List, error) {
|
|
if s.QualifiedChannel != "" {
|
|
return List{s}, nil
|
|
}
|
|
|
|
t, err := e.GetSubscriptionTemplate(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if t == nil {
|
|
return nil, errInvalidTemplate
|
|
}
|
|
|
|
subCtx := &tplCtx{
|
|
S: s,
|
|
PairSeparator: recordSeparator,
|
|
AssetSeparator: groupSeparator,
|
|
BatchSize: deviceControl + "BS",
|
|
}
|
|
|
|
subs := List{}
|
|
|
|
if len(s.Pairs) != 0 {
|
|
// We deliberately do not check Availability of sub Pairs because users have edge cases to subscribe to non-existent pairs
|
|
for a := range ap {
|
|
ap[a] = s.Pairs
|
|
}
|
|
}
|
|
|
|
switch s.Asset {
|
|
case asset.All:
|
|
if len(ap) == 0 {
|
|
return List{}, nil // No assets enabled; only asset.Empty subs may continue
|
|
}
|
|
subCtx.AssetPairs = ap
|
|
default:
|
|
if s.Asset != asset.Empty && len(ap[s.Asset]) == 0 {
|
|
return List{}, nil // No pairs enabled for this sub asset
|
|
}
|
|
// This deliberately includes asset.Empty to harmonise handling
|
|
subCtx.AssetPairs = assetPairs{
|
|
s.Asset: ap[s.Asset],
|
|
}
|
|
assets = asset.Items{s.Asset}
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
if err := t.Execute(buf, subCtx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := strings.TrimSpace(buf.String())
|
|
|
|
// Remove a single trailing AssetSeparator; don't use a cutset to avoid removing 2 or more
|
|
out = strings.TrimSpace(strings.TrimSuffix(out, subCtx.AssetSeparator))
|
|
|
|
assetRecords := strings.Split(out, subCtx.AssetSeparator)
|
|
if len(assetRecords) != len(assets) {
|
|
return nil, fmt.Errorf("%w: Got %d; Expected %d", errAssetRecords, len(assetRecords), len(assets))
|
|
}
|
|
|
|
for i, assetChannels := range assetRecords {
|
|
a := assets[i]
|
|
pairs := subCtx.AssetPairs[a]
|
|
|
|
xpandPairs := strings.Contains(assetChannels, subCtx.PairSeparator)
|
|
|
|
/* Batching:
|
|
- We start by assuming we'll get 1 batch sized to contain all pairs. Maybe a comma-separated list, or just the asset name
|
|
- If a BatchSize directive is found, we expect it to come right at the end, and be followed by the batch size as a number
|
|
- We'll then split into N batches of that size
|
|
- If no batchSize was declared, but we saw a PairSeparator, then we expect to see one line per pair, so batchSize is 1
|
|
*/
|
|
batchSize := len(pairs)
|
|
if b := strings.Split(assetChannels, subCtx.BatchSize); len(b) > 2 {
|
|
return nil, fmt.Errorf("%w for %s", errTooManyBatchSizePerAsset, a)
|
|
} else if len(b) == 2 {
|
|
assetChannels = b[0]
|
|
if batchSize, err = strconv.Atoi(strings.TrimSpace(b[1])); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", s, common.GetTypeAssertError("int", b[1], "batchSize"))
|
|
}
|
|
} else if xpandPairs {
|
|
batchSize = 1
|
|
}
|
|
|
|
// Trim space, then only one pair separator, then any more space.
|
|
assetChannels = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(assetChannels), subCtx.PairSeparator))
|
|
|
|
if assetChannels == "" {
|
|
continue
|
|
}
|
|
|
|
batches := common.Batch(pairs, batchSize)
|
|
|
|
pairLines := strings.Split(assetChannels, subCtx.PairSeparator)
|
|
|
|
if s.Asset != asset.Empty && len(pairLines) != len(batches) {
|
|
// The number of lines we get generated must match the number of pair batches we expect
|
|
return nil, fmt.Errorf("%w for %s: Got %d; Expected %d", errPairRecords, a, len(pairLines), len(batches))
|
|
}
|
|
|
|
for j, channel := range pairLines {
|
|
c := s.Clone()
|
|
c.Asset = a
|
|
channel = strings.TrimSpace(channel)
|
|
if channel == "" {
|
|
return nil, fmt.Errorf("%w for %s: %s", errNoTemplateContent, a, s)
|
|
}
|
|
c.QualifiedChannel = channel
|
|
if s.Asset != asset.Empty {
|
|
c.Pairs = batches[j]
|
|
}
|
|
subs = append(subs, c)
|
|
}
|
|
}
|
|
|
|
return subs, nil
|
|
}
|