Files
gocryptotrader/exchanges/subscription/template.go
Gareth Kirwan 4c7f48ae0e GateIO: Fix GetFuturesContractDetails for delivery futures and minor other fixes (#1766)
* GateIO: Fix GetFuturesContractDetails for Deliveries

Was returning the product of all the contracts, so 1444 instead of 38
contracts.

* GateIO: Fix GetOpenInterest returning asset.ErrNotEnabled

Using wrong error for pair not enabled

* GateIO: Rename GetSingleContract and GetSingleDeliveryContracts

Especially fixes GetSingleContract, which seems misleading to not say
Futures.
There's a load of `GetSingle*` here that should probably also be fixed,
but these two justified a dyno

* GateIO: Rename GateIOGetPersonalTradingHistory to GetMySpotTradingHistory

* GateIO: Rename GetMyPersonalTradingHistory to GetMyFuturesTradingHistory

* GateIO: Remove duplicate DeliveryTradingHistory

* GateIO: Rename Get*PersonalTradingHistory to GetMy*TradingHistory

* Linter: Disable shadow linting for err

It's been a year, and I'm still getting caught out by govet demanding I
don't shadow a var I was deliberately shadowing.
Made worse by an increase in clashes with stylecheck when they both want
opposite things on the same line.

* GateIO: Add missing Futures and tradinghistory fields

* GateIO: Improve WS Header parsing

This unifies handling for time_ms and time in response headers, since
options and delivery have only time, but spot has time_ms as well.
We use the better of the two results.

Also [improves performance 2x](https://gist.github.com/gbjk/7cacb63b9a256e745534bb05ca853c48)

* GateIO: Use time_ms WS fields where available

Removes the deprecated _time json fields and populates our Time fields
with the time_ms values
2025-01-14 15:19:17 +11:00

203 lines
6.0 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 (
errInvalidAssetExpandPairs = errors.New("subscription template containing PairSeparator with must contain either specific Asset or AssetSeparator")
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")
errAssetTemplateWithoutAll = errors.New("sub.Asset must be set to All if AssetSeparator is used in Channel template")
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
}