Files
gocryptotrader/exchanges/bithumb/bithumb_websocket.go
Samuael A. 3f534a15f1 cmd/exchange_template, exchanges: Update templates and propogate to exchanges (#1777)
* Added TimeInForce type and updated related files

* Linter issue fix and minor coinbasepro type update

* Bitrex consts update

* added unit test and minor changes in bittrex

* Unit tests update

* Fix minor linter issues

* Update TestStringToTimeInForce unit test

* Exchange test template change

* A different approach

* fix conflict with gateio timeInForce

* minor exchange template update

* Minor fix to test_files template

* Update order tests

* Complete updating the order unit tests

* Updating exchange wrapper and test template files

* update kucoin and deribit wrapper to match the time in force change

* minor comment update

* fix time-in-force related test errors

* linter issue fix

* ADD_NEW_EXCHANGE documentation update

* time in force constants, functions and unit tests update

* shift tif policies to TimeInForce

* Update time-in-force, related functions, and unit tests

* fix linter issue and time-in-force processing

* added a good till crossing tif value

* order type fix and fix related tim-in-force entries

* update time-in-force unmarshaling and unit test

* consistency guideline added

* fix time-in-force error in gateio

* linter issue fix

* update based on review comments

* add unit test and fix missing issues

* minor fix and added benchmark unit test

* change GTT to GTC for limit

* fix linter issue

* added time-in-force value to place order param

* fix minor issues based on review comment and move tif code to separate files

* update on exchanges linked to time-in-force

* resolve missing review comments

* minor linter issues fix

* added time-in-force handler and update timeInForce parametered endpoint

* minor fixes based on review

* nits fix

* update based on review

* linter fix

* rm getTimeInForce func and minor change to time-in-force

* minor change

* update based on review comments

* wrappers and time-in-force calling approach

* minor change

* update gateio string to timeInForce conversion and unit test

* update exchange template

* update wrapper template file

* policy comments, and template files update

* rename all exchange types name to Exchange

* update on template files and template generation

* templates and generation code and other updates

* linter issue fix

* added subscriptions and websocket templates

* update ADD_NEW_EXCHANGE.md with recent binance functions and implementations

* rename template files and update unit tests

* minor template and unit test fix

* rename templates and fix on unit tests

* update on template files and documentation

* removed unnecessary tag fix and update templates

* fix Add_NEW_EXCHANGE.md doc file

* formatting, comments, and error checks update on template files

* rename exchange receivers to e and ex for consistency

* rename unit test exchange receiver and minor updates

* linter issues fix

* fix deribit issue and minor style update

* fix test issues caused by receiver change

* raname local variables exchange declaration variables

* update templates comments

* update templates and related comments

* renamed ex to e

* update template comments

* toggle WS to false to improve coverage

* template comments update

* added test coverage to Ws enabled and minor changes

---------

Co-authored-by: Samuel Reid <43227667+cranktakular@users.noreply.github.com>
2025-07-17 10:46:36 +10:00

227 lines
5.9 KiB
Go

package bithumb
import (
"context"
"fmt"
"net/http"
"strings"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
gws "github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
)
const (
wsEndpoint = "wss://pubwss.bithumb.com/pub/ws"
tickerTimeLayout = "20060102150405"
tradeTimeLayout = time.DateTime + ".000000"
)
var defaultSubscriptions = subscription.List{
{Enabled: true, Asset: asset.Spot, Channel: subscription.TickerChannel, Interval: kline.ThirtyMin}, // alternatives "1H", "12H", "24H"
{Enabled: true, Asset: asset.Spot, Channel: subscription.OrderbookChannel},
{Enabled: true, Asset: asset.Spot, Channel: subscription.AllTradesChannel},
}
// WsConnect initiates a websocket connection
func (e *Exchange) WsConnect() error {
ctx := context.TODO()
if !e.Websocket.IsEnabled() || !e.IsEnabled() {
return websocket.ErrWebsocketNotEnabled
}
var dialer gws.Dialer
dialer.HandshakeTimeout = e.Config.HTTPTimeout
dialer.Proxy = http.ProxyFromEnvironment
err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
if err != nil {
return fmt.Errorf("%v - Unable to connect to Websocket. Error: %w", e.Name, err)
}
e.Websocket.Wg.Add(1)
go e.wsReadData()
e.setupOrderbookManager(ctx)
return nil
}
// wsReadData receives and passes on websocket messages for processing
func (e *Exchange) wsReadData() {
defer e.Websocket.Wg.Done()
for {
select {
case <-e.Websocket.ShutdownC:
return
default:
resp := e.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
err := e.wsHandleData(resp.Raw)
if err != nil {
e.Websocket.DataHandler <- err
}
}
}
}
func (e *Exchange) wsHandleData(respRaw []byte) error {
var resp WsResponse
err := json.Unmarshal(respRaw, &resp)
if err != nil {
return err
}
if resp.Status != "" {
if resp.Status == "0000" {
return nil
}
return fmt.Errorf("%s: %w",
resp.ResponseMessage,
websocket.ErrSubscriptionFailure)
}
switch resp.Type {
case "ticker":
var tick WsTicker
err = json.Unmarshal(resp.Content, &tick)
if err != nil {
return err
}
var lu time.Time
lu, err = time.ParseInLocation(tickerTimeLayout, tick.Date+tick.Time, e.location)
if err != nil {
return err
}
e.Websocket.DataHandler <- &ticker.Price{
ExchangeName: e.Name,
AssetType: asset.Spot,
Last: tick.PreviousClosePrice,
Pair: tick.Symbol,
Open: tick.OpenPrice,
Close: tick.ClosePrice,
Low: tick.LowPrice,
High: tick.HighPrice,
QuoteVolume: tick.Value,
Volume: tick.Volume,
LastUpdated: lu,
}
case "transaction":
if !e.IsSaveTradeDataEnabled() {
return nil
}
var trades WsTransactions
err = json.Unmarshal(resp.Content, &trades)
if err != nil {
return err
}
toBuffer := make([]trade.Data, len(trades.List))
var lu time.Time
for x := range trades.List {
lu, err = time.ParseInLocation(tradeTimeLayout, trades.List[x].ContractTime, e.location)
if err != nil {
return err
}
toBuffer[x] = trade.Data{
Exchange: e.Name,
AssetType: asset.Spot,
CurrencyPair: trades.List[x].Symbol,
Timestamp: lu,
Price: trades.List[x].ContractPrice,
Amount: trades.List[x].ContractAmount,
}
}
err = e.AddTradesToBuffer(toBuffer...)
if err != nil {
return err
}
case "orderbookdepth":
var orderbooks WsOrderbooks
err = json.Unmarshal(resp.Content, &orderbooks)
if err != nil {
return err
}
init, err := e.UpdateLocalBuffer(&orderbooks)
if err != nil && !init {
return fmt.Errorf("%v - UpdateLocalCache error: %s", e.Name, err)
}
return nil
default:
return fmt.Errorf("unhandled response type %s", resp.Type)
}
return nil
}
// generateSubscriptions generates the default subscription set
func (e *Exchange) generateSubscriptions() (subscription.List, error) {
return e.Features.Subscriptions.ExpandTemplates(e)
}
// GetSubscriptionTemplate returns a subscription channel template
func (e *Exchange) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) {
return template.New("master.tmpl").Funcs(sprig.FuncMap()).Funcs(template.FuncMap{"subToReq": subToReq}).Parse(subTplText)
}
// Subscribe subscribes to a set of channels
func (e *Exchange) Subscribe(subs subscription.List) error {
ctx := context.TODO()
var errs error
for _, s := range subs {
err := e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, json.RawMessage(s.QualifiedChannel))
if err == nil {
err = e.Websocket.AddSuccessfulSubscriptions(e.Websocket.Conn, s)
}
if err != nil {
errs = common.AppendError(errs, err)
}
}
return errs
}
// subToReq returns the subscription as a map to populate WsSubscribe
func subToReq(s *subscription.Subscription, p currency.Pairs) *WsSubscribe {
req := &WsSubscribe{
Type: s.Channel,
Symbols: common.SortStrings(p),
}
switch s.Channel {
case subscription.TickerChannel:
// As-is
case subscription.OrderbookChannel:
req.Type = "orderbookdepth"
case subscription.AllTradesChannel:
req.Type = "transaction"
default:
panic(fmt.Errorf("%w: %s", subscription.ErrNotSupported, s.Channel))
}
if s.Interval > 0 {
req.TickTypes = []string{strings.ToUpper(s.Interval.Short())}
}
return req
}
const subTplText = `
{{ range $asset, $pairs := $.AssetPairs }}
{{- subToReq $.S $pairs | mustToJson }}
{{- $.AssetSeparator }}
{{- end }}
`