package deribit

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/encoding/json"
	"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
	"github.com/thrasher-corp/gocryptotrader/exchanges/request"
	"github.com/thrasher-corp/gocryptotrader/log"
)

// WSRetrieveBookBySummary retrieves book summary data for currency requested through websocket connection.
func (e *Exchange) WSRetrieveBookBySummary(ctx context.Context, ccy currency.Code, kind string) ([]BookSummaryData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		Kind     string        `json:"kind,omitempty"`
	}{
		Currency: ccy,
	}
	if kind != "" {
		input.Kind = kind
	}
	var resp []BookSummaryData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getBookByCurrency, input, &resp, false)
}

// WSRetrieveBookSummaryByInstrument retrieves book summary data for instrument requested through the websocket connection.
func (e *Exchange) WSRetrieveBookSummaryByInstrument(ctx context.Context, instrument string) ([]BookSummaryData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	input := &struct {
		Instrument string `json:"instrument_name,omitempty"`
	}{
		Instrument: instrument,
	}
	var resp []BookSummaryData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getBookByInstrument, input, &resp, false)
}

// WSRetrieveContractSize retrieves contract size for instrument requested through the websocket connection.
func (e *Exchange) WSRetrieveContractSize(ctx context.Context, instrument string) (*ContractSizeData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	input := &struct {
		Instrument string `json:"instrument_name"`
	}{
		Instrument: instrument,
	}
	var resp *ContractSizeData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getContractSize, input, &resp, false)
}

// WSRetrieveCurrencies retrieves all cryptocurrencies supported by the API through the websocket connection.
func (e *Exchange) WSRetrieveCurrencies(ctx context.Context) ([]CurrencyData, error) {
	var resp []CurrencyData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getCurrencies, nil, &resp, false)
}

// WSRetrieveDeliveryPrices retrieves delivery prices using index name through the websocket connection.
func (e *Exchange) WSRetrieveDeliveryPrices(ctx context.Context, indexName string, offset, count int64) (*IndexDeliveryPrice, error) {
	if indexName == "" {
		return nil, errUnsupportedIndexName
	}
	input := &struct {
		IndexName string `json:"index_name"`
		Offset    int64  `json:"offset,omitempty"`
		Count     int64  `json:"count,omitempty"`
	}{
		IndexName: indexName,
		Offset:    offset,
		Count:     count,
	}
	var resp *IndexDeliveryPrice
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getDeliveryPrices, input, &resp, false)
}

// WSRetrieveFundingChartData retrieves funding chart data for the requested instrument and time length through the websocket connection.
// supported lengths: 8h, 24h, 1m <-(1month)
func (e *Exchange) WSRetrieveFundingChartData(ctx context.Context, instrument, length string) (*FundingChartData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	if length == "" {
		return nil, errIntervalNotSupported
	}
	input := &struct {
		InstrumentName string `json:"instrument_name"`
		Length         string `json:"length"`
	}{
		InstrumentName: instrument,
		Length:         length,
	}
	var resp *FundingChartData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getFundingChartData, input, &resp, false)
}

// WSRetrieveFundingRateHistory retrieves hourly historical interest rate for requested PERPETUAL instrument through the websocket connection.
func (e *Exchange) WSRetrieveFundingRateHistory(ctx context.Context, instrumentName string, startTime, endTime time.Time) ([]FundingRateHistory, error) {
	if instrumentName == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	err := common.StartEndTimeCheck(startTime, endTime)
	if err != nil {
		return nil, err
	}
	input := &struct {
		InstrumentName string `json:"instrument_name"`
		StartTime      int64  `json:"start_timestamp"`
		EndTime        int64  `json:"end_timestamp"`
	}{
		InstrumentName: instrumentName,
		StartTime:      startTime.UnixMilli(),
		EndTime:        endTime.UnixMilli(),
	}
	var resp []FundingRateHistory
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getFundingRateHistory, input, &resp, false)
}

// WSRetrieveFundingRateValue retrieves funding rate value data through the websocket connection.
func (e *Exchange) WSRetrieveFundingRateValue(ctx context.Context, instrument string, startTime, endTime time.Time) (float64, error) {
	if instrument == "" {
		return 0, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	if err := common.StartEndTimeCheck(startTime, endTime); err != nil {
		return 0, err
	}
	input := &struct {
		Instrument     string `json:"instrument_name"`
		StartTimestamp int64  `json:"start_timestamp"`
		EndTimestamp   int64  `json:"end_timestamp"`
	}{
		Instrument:     instrument,
		StartTimestamp: startTime.UnixMilli(),
		EndTimestamp:   endTime.UnixMilli(),
	}
	var resp float64
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getFundingRateValue, input, &resp, false)
}

// WSRetrieveHistoricalVolatility retrieves historical volatility data
func (e *Exchange) WSRetrieveHistoricalVolatility(ctx context.Context, ccy currency.Code) ([]HistoricalVolatilityData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
	}{
		Currency: ccy,
	}
	var data []HistoricalVolatilityData
	return data, e.SendWSRequest(ctx, nonMatchingEPL, getHistoricalVolatility, input, &data, false)
}

// WSRetrieveCurrencyIndexPrice the current index price for the instruments, for the selected currency through the websocket connection.
func (e *Exchange) WSRetrieveCurrencyIndexPrice(ctx context.Context, ccy currency.Code) (map[string]float64, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
	}{
		Currency: ccy,
	}
	var resp map[string]float64
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getCurrencyIndexPrice, input, &resp, false)
}

// WSRetrieveIndexPrice retrieves price data for the requested index through the websocket connection.
func (e *Exchange) WSRetrieveIndexPrice(ctx context.Context, index string) (*IndexPriceData, error) {
	if index == "" {
		return nil, fmt.Errorf("%w index can not be empty", errUnsupportedIndexName)
	}
	input := &struct {
		IndexName string `json:"index_name"`
	}{
		IndexName: index,
	}
	var resp *IndexPriceData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getIndexPrice, input, &resp, false)
}

// WSRetrieveIndexPriceNames names of indexes through the websocket connection.
func (e *Exchange) WSRetrieveIndexPriceNames(ctx context.Context) ([]string, error) {
	var resp []string
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getIndexPriceNames, nil, &resp, false)
}

// WSRetrieveInstrumentData retrieves data for a requested instrument through the websocket connection.
func (e *Exchange) WSRetrieveInstrumentData(ctx context.Context, instrument string) (*InstrumentData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	input := &struct {
		Instrument string `json:"instrument_name"`
	}{
		Instrument: instrument,
	}
	var resp *InstrumentData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getInstrument, input, &resp, false)
}

// WSRetrieveInstrumentsData gets data for all available instruments
func (e *Exchange) WSRetrieveInstrumentsData(ctx context.Context, ccy currency.Code, kind string, expired bool) ([]*InstrumentData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		Expired  bool          `json:"expired"`
		Kind     string        `json:"kind,omitempty"`
	}{
		Currency: ccy,
		Expired:  expired,
		Kind:     kind,
	}
	var resp []*InstrumentData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getInstruments, input, &resp, false)
}

// WSRetrieveLastSettlementsByCurrency retrieves last settlement data by currency through the websocket connection.
func (e *Exchange) WSRetrieveLastSettlementsByCurrency(ctx context.Context, ccy currency.Code, settlementType, continuation string, count int64, startTime time.Time) (*SettlementsData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency             string `json:"currency,omitempty"`
		Type                 string `json:"type,omitempty"`
		Continuation         string `json:"continuation,omitempty"`
		Count                int64  `json:"count,omitempty"`
		SearchStartTimestamp int64  `json:"search_start_timestamp,omitempty"`
	}{
		Currency:             ccy.String(),
		Type:                 settlementType,
		Continuation:         continuation,
		Count:                count,
		SearchStartTimestamp: startTime.UnixMilli(),
	}
	var resp *SettlementsData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getLastSettlementsByCurrency, input, &resp, false)
}

// WSRetrieveLastSettlementsByInstrument retrieves last settlement data for requested instrument through the websocket connection.
func (e *Exchange) WSRetrieveLastSettlementsByInstrument(ctx context.Context, instrument, settlementType, continuation string, count int64, startTime time.Time) (*SettlementsData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	input := &struct {
		Instrument           string `json:"instrument_name"`
		SettlementType       string `json:"type,omitempty"`
		Continuation         string `json:"continuation,omitempty"`
		Count                int64  `json:"count,omitempty"`
		SearchStartTimestamp int64  `json:"search_start_timestamp,omitempty"`
	}{
		Instrument:     instrument,
		SettlementType: settlementType,
		Continuation:   continuation,
		Count:          count,
	}
	if !startTime.IsZero() {
		input.SearchStartTimestamp = startTime.UnixMilli()
	}
	var resp *SettlementsData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getLastSettlementsByInstrument, input, &resp, false)
}

// WSRetrieveLastTradesByCurrency retrieves last trades for requested currency through the websocket connection.
func (e *Exchange) WSRetrieveLastTradesByCurrency(ctx context.Context, ccy currency.Code, kind, startID, endID, sorting string, count int64, includeOld bool) (*PublicTradesData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency   currency.Code `json:"currency"`
		Kind       string        `json:"kind,omitempty"`
		StartID    string        `json:"start_id,omitempty"`
		EndID      string        `json:"end_id,omitempty"`
		Count      int64         `json:"count,omitempty"`
		IncludeOld bool          `json:"include_old,omitempty"`
		Sorting    string        `json:"sorting,omitempty"`
	}{
		Currency:   ccy,
		Kind:       kind,
		StartID:    startID,
		EndID:      endID,
		Count:      count,
		IncludeOld: includeOld,
		Sorting:    sorting,
	}
	var resp *PublicTradesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getLastTradesByCurrency, input, &resp, false)
}

// WSRetrieveLastTradesByCurrencyAndTime retrieves last trades for requested currency and time intervals through the websocket connection.
func (e *Exchange) WSRetrieveLastTradesByCurrencyAndTime(ctx context.Context, ccy currency.Code, kind, sorting string, count int64, includeOld bool, startTime, endTime time.Time) (*PublicTradesData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if err := common.StartEndTimeCheck(startTime, endTime); err != nil {
		return nil, err
	}
	input := &struct {
		Currency       currency.Code `json:"currency"`
		Kind           string        `json:"kind,omitempty"`
		Sorting        string        `json:"sorting,omitempty"`
		Count          int64         `json:"count,omitempty"`
		StartTimestamp int64         `json:"start_timestamp,omitempty"`
		EndTimestamp   int64         `json:"end_timestamp,omitempty"`
		IncludeOld     bool          `json:"include_old,omitempty"`
	}{
		Currency:       ccy,
		Kind:           kind,
		Count:          count,
		StartTimestamp: startTime.UnixMilli(),
		EndTimestamp:   endTime.UnixMilli(),
		IncludeOld:     includeOld,
		Sorting:        sorting,
	}
	var resp *PublicTradesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getLastTradesByCurrencyAndTime, input, &resp, false)
}

// WSRetrieveLastTradesByInstrument retrieves last trades for requested instrument requested through the websocket connection.
func (e *Exchange) WSRetrieveLastTradesByInstrument(ctx context.Context, instrument, startSeq, endSeq, sorting string, count int64, includeOld bool) (*PublicTradesData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	input := &struct {
		Instrument    string `json:"instrument_name,omitempty"`
		StartSequence string `json:"start_seq,omitempty"`
		EndSequence   string `json:"end_seq,omitempty"`
		Sorting       string `json:"sorting,omitempty"`
		Count         int64  `json:"count,omitempty"`
		IncludeOld    bool   `json:"include_old,omitempty"`
	}{
		Instrument:    instrument,
		StartSequence: startSeq,
		EndSequence:   endSeq,
		Sorting:       sorting,
		Count:         count,
		IncludeOld:    includeOld,
	}
	var resp *PublicTradesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getLastTradesByInstrument, input, &resp, false)
}

// WSRetrieveLastTradesByInstrumentAndTime retrieves last trades for requested instrument requested and time intervals through the websocket connection.
func (e *Exchange) WSRetrieveLastTradesByInstrumentAndTime(ctx context.Context, instrument, sorting string, count int64, includeOld bool, startTime, endTime time.Time) (*PublicTradesData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	if err := common.StartEndTimeCheck(startTime, endTime); err != nil {
		return nil, err
	}
	input := &struct {
		Instrument     string `json:"instrument_name,omitempty"`
		StartTimestamp int64  `json:"start_timestamp,omitempty"`
		EndTimestamp   int64  `json:"end_timestamp,omitempty"`
		Sorting        string `json:"sorting,omitempty"`
		Count          int64  `json:"count,omitempty"`
		IncludeOld     bool   `json:"include_old,omitempty"`
	}{
		Instrument: instrument,
		Sorting:    sorting,
		Count:      count,
		IncludeOld: includeOld,
	}
	input.StartTimestamp = startTime.UnixMilli()
	input.EndTimestamp = endTime.UnixMilli()
	var resp *PublicTradesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getLastTradesByInstrumentAndTime, input, &resp, false)
}

// WSRetrieveMarkPriceHistory retrieves data for mark price history through the websocket connection.
func (e *Exchange) WSRetrieveMarkPriceHistory(ctx context.Context, instrument string, startTime, endTime time.Time) ([]MarkPriceHistory, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	if err := common.StartEndTimeCheck(startTime, endTime); err != nil {
		return nil, err
	}
	input := &struct {
		Instrument     string `json:"instrument_name,omitempty"`
		StartTimestamp int64  `json:"start_timestamp,omitempty"`
		EndTimestamp   int64  `json:"end_timestamp,omitempty"`
	}{
		Instrument:     instrument,
		StartTimestamp: startTime.UnixMilli(),
		EndTimestamp:   endTime.UnixMilli(),
	}
	var resp []MarkPriceHistory
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getMarkPriceHistory, input, &resp, false)
}

// WSRetrieveOrderbookData retrieves data orderbook of requested instrument through the web-socket connection.
func (e *Exchange) WSRetrieveOrderbookData(ctx context.Context, instrument string, depth int64) (*Orderbook, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	input := &struct {
		Instrument string `json:"instrument_name"`
		Depth      int64  `json:"depth,omitempty"`
	}{
		Instrument: instrument,
		Depth:      depth,
	}
	var resp *Orderbook
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOrderbook, input, &resp, false)
}

// WSRetrieveOrderbookByInstrumentID retrieves orderbook by instrument ID through websocket connection.
func (e *Exchange) WSRetrieveOrderbookByInstrumentID(ctx context.Context, instrumentID int64, depth float64) (*Orderbook, error) {
	if instrumentID == 0 {
		return nil, errInvalidInstrumentID
	}
	input := &struct {
		InstrumentID int64   `json:"instrument_id"`
		Depth        float64 `json:"depth,omitempty"`
	}{
		InstrumentID: instrumentID,
		Depth:        depth,
	}
	var resp *Orderbook
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOrderbookByInstrumentID, input, &resp, false)
}

// WsRetrieveSupportedIndexNames retrieves the identifiers of all supported Price Indexes
// 'type' represents Type of a cryptocurrency price index. possible 'all', 'spot', 'derivative'
func (e *Exchange) WsRetrieveSupportedIndexNames(ctx context.Context, priceIndexType string) ([]string, error) {
	input := &struct {
		PriceIndexType string `json:"type,omitempty"`
	}{
		PriceIndexType: priceIndexType,
	}
	var resp []string
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, "public/get_supported_index_names", input, &resp, false)
}

// WSRetrieveTradeVolumes retrieves trade volumes' data of all instruments through the websocket connection.
func (e *Exchange) WSRetrieveTradeVolumes(ctx context.Context, extended bool) ([]TradeVolumesData, error) {
	input := &struct {
		Extended bool `json:"extended,omitempty"`
	}{
		Extended: extended,
	}
	var resp []TradeVolumesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getTradeVolumes, input, &resp, false)
}

// WSRetrievesTradingViewChartData retrieves volatility index data for the requested instrument through the websocket connection.
func (e *Exchange) WSRetrievesTradingViewChartData(ctx context.Context, instrument, resolution string, startTime, endTime time.Time) (*TVChartData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	if err := common.StartEndTimeCheck(startTime, endTime); err != nil {
		return nil, err
	}
	if resolution == "" {
		return nil, errors.New("unsupported resolution, resolution can not be empty")
	}
	input := &struct {
		Instrument     string `json:"instrument_name,omitempty"`
		StartTimestamp int64  `json:"start_timestamp,omitempty"`
		EndTimestamp   int64  `json:"end_timestamp,omitempty"`
		Resolution     string `json:"resolution,omitempty"`
	}{
		Instrument:     instrument,
		Resolution:     resolution,
		StartTimestamp: startTime.UnixMilli(),
		EndTimestamp:   endTime.UnixMilli(),
	}
	var resp *TVChartData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getTradingViewChartData, input, &resp, false)
}

// WSRetrieveVolatilityIndexData retrieves volatility index data for the requested currency through the websocket connection.
func (e *Exchange) WSRetrieveVolatilityIndexData(ctx context.Context, ccy currency.Code, resolution string, startTime, endTime time.Time) ([]VolatilityIndexData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if resolution == "" {
		return nil, errResolutionNotSet
	}
	err := common.StartEndTimeCheck(startTime, endTime)
	if err != nil {
		return nil, err
	}
	input := &struct {
		Currency       string `json:"currency,omitempty"`
		StartTimestamp int64  `json:"start_timestamp,omitempty"`
		EndTimestamp   int64  `json:"end_timestamp,omitempty"`
		Resolution     string `json:"resolution,omitempty"`
	}{
		Currency:       ccy.String(),
		Resolution:     resolution,
		StartTimestamp: startTime.UnixMilli(),
		EndTimestamp:   endTime.UnixMilli(),
	}
	var resp VolatilityIndexRawData
	err = e.SendWSRequest(ctx, nonMatchingEPL, getVolatilityIndex, input, &resp, false)
	if err != nil {
		return nil, err
	}
	response := make([]VolatilityIndexData, len(resp.Data))
	for x := range resp.Data {
		response[x] = VolatilityIndexData{
			TimestampMS: time.UnixMilli(int64(resp.Data[x][0])),
			Open:        resp.Data[x][1],
			High:        resp.Data[x][2],
			Low:         resp.Data[x][3],
			Close:       resp.Data[x][4],
		}
	}
	return response, nil
}

// WSRetrievePublicTicker retrieves public ticker data of the instrument requested through the websocket connection.
func (e *Exchange) WSRetrievePublicTicker(ctx context.Context, instrument string) (*TickerData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	input := &struct {
		Instrument string `json:"instrument_name,omitempty"`
	}{
		Instrument: instrument,
	}
	var resp *TickerData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getTicker, input, &resp, false)
}

// WSRetrieveAccountSummary retrieves account summary data for the requested instrument through the websocket connection.
func (e *Exchange) WSRetrieveAccountSummary(ctx context.Context, ccy currency.Code, extended bool) (*AccountSummaryData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		Extended bool          `json:"extended"`
	}{
		Currency: ccy,
		Extended: extended,
	}
	var resp *AccountSummaryData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getAccountSummary, input, &resp, true)
}

// WSCancelWithdrawal cancels withdrawal request for a given currency by its id through the websocket connection.
func (e *Exchange) WSCancelWithdrawal(ctx context.Context, ccy currency.Code, id int64) (*CancelWithdrawalData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if id <= 0 {
		return nil, fmt.Errorf("%w, withdrawal id has to be positive integer", errInvalidID)
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		ID       int64         `json:"id"`
	}{
		Currency: ccy,
		ID:       id,
	}
	var resp *CancelWithdrawalData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, cancelWithdrawal, input, &resp, true)
}

// WSCancelTransferByID cancels transfer by ID through the websocket connection.
func (e *Exchange) WSCancelTransferByID(ctx context.Context, ccy currency.Code, tfa string, id int64) (*AccountSummaryData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if id <= 0 {
		return nil, fmt.Errorf("%w, transfer id has to be positive integer", errInvalidID)
	}
	input := &struct {
		Currency                    string `json:"currency"`
		TwoFactorAuthenticationCode string `json:"tfa,omitempty"`
		ID                          int64  `json:"id"`
	}{
		Currency:                    ccy.String(),
		ID:                          id,
		TwoFactorAuthenticationCode: tfa,
	}
	var resp *AccountSummaryData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, cancelTransferByID, input, &resp, true)
}

// WSCreateDepositAddress creates a deposit address for the currency requested through the websocket connection.
func (e *Exchange) WSCreateDepositAddress(ctx context.Context, ccy currency.Code) (*DepositAddressData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
	}{
		Currency: ccy,
	}
	var resp *DepositAddressData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, createDepositAddress, input, &resp, true)
}

// WSRetrieveDeposits retrieves the deposits of a given currency through the websocket connection.
func (e *Exchange) WSRetrieveDeposits(ctx context.Context, ccy currency.Code, count, offset int64) (*DepositsData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		Count    int64         `json:"count,omitempty"`
		Offset   int64         `json:"offset,omitempty"`
	}{
		Currency: ccy,
		Count:    count,
		Offset:   offset,
	}
	var resp *DepositsData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getDeposits, input, &resp, true)
}

// WSRetrieveTransfers retrieves data for the requested currency through the websocket connection.
func (e *Exchange) WSRetrieveTransfers(ctx context.Context, ccy currency.Code, count, offset int64) (*TransfersData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency string `json:"currency,omitempty"`
		Count    int64  `json:"count,omitempty"`
		Offset   int64  `json:"offset,omitempty"`
	}{
		Currency: ccy.String(),
		Count:    count,
		Offset:   offset,
	}
	var resp *TransfersData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getTransfers, input, &resp, true)
}

// WSRetrieveCurrentDepositAddress retrieves the current deposit address for the requested currency through the websocket connection.
func (e *Exchange) WSRetrieveCurrentDepositAddress(ctx context.Context, ccy currency.Code) (*DepositAddressData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
	}{
		Currency: ccy,
	}
	var resp *DepositAddressData
	err := e.SendWSRequest(ctx, nonMatchingEPL, getCurrentDepositAddress, input, &resp, true)
	if err != nil {
		return nil, err
	} else if resp == nil {
		return nil, common.ErrNoResponse
	}
	return resp, nil
}

// WSRetrieveWithdrawals retrieves withdrawals data for a requested currency through the websocket connection.
func (e *Exchange) WSRetrieveWithdrawals(ctx context.Context, ccy currency.Code, count, offset int64) (*WithdrawalsData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		Count    int64         `json:"count,omitempty"`
		Offset   int64         `json:"offset,omitempty"`
	}{
		Currency: ccy,
		Count:    count,
		Offset:   offset,
	}
	var resp *WithdrawalsData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getWithdrawals, input, &resp, true)
}

// WsSubmitTransferBetweenSubAccounts transfer funds between two (sub)accounts.
func (e *Exchange) WsSubmitTransferBetweenSubAccounts(ctx context.Context, ccy currency.Code, amount float64, destinationID int64, source string) (*TransferData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if amount <= 0 {
		return nil, fmt.Errorf("%w, amount : %f", errInvalidAmount, amount)
	}
	if destinationID <= 0 {
		return nil, errInvalidDestinationID
	}
	input := &struct {
		Currency    string  `json:"currency"`
		Amount      float64 `json:"amount"`
		Destination int64   `json:"destination"`
		Source      string  `json:"source,omitempty"`
	}{
		Currency:    ccy.String(),
		Amount:      amount,
		Destination: destinationID,
		Source:      source,
	}
	var resp *TransferData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, submitTransferBetweenSubAccounts, input, &resp, true)
}

// WSSubmitTransferToSubAccount submits a request to transfer a currency to a subaccount
func (e *Exchange) WSSubmitTransferToSubAccount(ctx context.Context, ccy currency.Code, amount float64, destinationID int64) (*TransferData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if amount <= 0 {
		return nil, errInvalidAmount
	}
	if destinationID <= 0 {
		return nil, errInvalidDestinationID
	}
	input := &struct {
		Currency    string  `json:"currency"`
		Destination int64   `json:"destination"`
		Amount      float64 `json:"amount"`
	}{
		Currency:    ccy.String(),
		Destination: destinationID,
		Amount:      amount,
	}
	var resp *TransferData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, submitTransferToSubaccount, input, &resp, true)
}

// WSSubmitTransferToUser submits a request to transfer a currency to another user through the websocket connection.
func (e *Exchange) WSSubmitTransferToUser(ctx context.Context, ccy currency.Code, tfa, destinationAddress string, amount float64) (*TransferData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if amount <= 0 {
		return nil, errInvalidAmount
	}
	if destinationAddress == "" {
		return nil, errInvalidCryptoAddress
	}
	input := &struct {
		Currency                    string  `json:"currency"`
		TwoFactorAuthenticationCode string  `json:"tfa,omitempty"`
		DestinationID               string  `json:"destination"`
		Amount                      float64 `json:"amount"`
	}{
		Currency:                    ccy.String(),
		TwoFactorAuthenticationCode: tfa,
		DestinationID:               destinationAddress,
		Amount:                      amount,
	}
	var resp *TransferData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, submitTransferToUser, input, &resp, true)
}

// ----------------------------------------------------------------------------

// WSSubmitWithdraw submits a withdrawal request to the exchange for the requested currency through the websocket connection.
func (e *Exchange) WSSubmitWithdraw(ctx context.Context, ccy currency.Code, address, priority string, amount float64) (*WithdrawData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if amount <= 0 {
		return nil, errInvalidAmount
	}
	if address == "" {
		return nil, errInvalidCryptoAddress
	}
	input := &struct {
		Currency string  `json:"currency"`
		Address  string  `json:"address"`
		Priority string  `json:"priority,omitempty"`
		Amount   float64 `json:"amount"`
	}{
		Currency: ccy.String(),
		Address:  address,
		Priority: priority,
		Amount:   amount,
	}
	var resp *WithdrawData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, submitWithdraw, input, &resp, true)
}

// WSRetrieveAnnouncements retrieves announcements through the websocket connection. Default "start_timestamp" parameter value is current timestamp, "count" parameter value must be between 1 and 50, default is 5.
func (e *Exchange) WSRetrieveAnnouncements(ctx context.Context, startTime time.Time, count int64) ([]Announcement, error) {
	input := &struct {
		StartTime int64 `json:"start_time,omitempty"`
		Count     int64 `json:"count,omitempty"`
	}{}
	if !startTime.IsZero() {
		input.StartTime = startTime.UnixMilli()
	}
	if count > 0 {
		input.Count = count
	}
	var resp []Announcement
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getAnnouncements, input, &resp, false)
}

// WSChangeAPIKeyName changes the name of the api key requested through the websocket connection.
func (e *Exchange) WSChangeAPIKeyName(ctx context.Context, id int64, name string) (*APIKeyData, error) {
	if id <= 0 {
		return nil, fmt.Errorf("%w, invalid api key id", errInvalidID)
	}
	if !alphaNumericRegExp.MatchString(name) {
		return nil, errUnacceptableAPIKey
	}
	input := &struct {
		ID   int64  `json:"id"`
		Name string `json:"name"`
	}{
		ID:   id,
		Name: name,
	}
	var resp *APIKeyData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, changeAPIKeyName, input, &resp, true)
}

// WsChangeMarginModel change margin model
// Margin model: 'cross_pm', 'cross_sm', 'segregated_pm', 'segregated_sm'
// 'dry_run': If true request returns the result without switching the margining model. Default: false
func (e *Exchange) WsChangeMarginModel(ctx context.Context, userID int64, marginModel string, dryRun bool) ([]TogglePortfolioMarginResponse, error) {
	if marginModel == "" {
		return nil, errInvalidMarginModel
	}
	input := &struct {
		MarginModel string `json:"margin_model"`
		UserID      int64  `json:"user_id"`
		DryRun      bool   `json:"dry_run,omitempty"`
	}{
		MarginModel: marginModel,
		UserID:      userID,
		DryRun:      dryRun,
	}
	var resp []TogglePortfolioMarginResponse
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, changeMarginModel, input, &resp, true)
}

// WSChangeScopeInAPIKey changes the name of the requested subaccount id through the websocket connection.
func (e *Exchange) WSChangeScopeInAPIKey(ctx context.Context, id int64, maxScope string) (*APIKeyData, error) {
	if id <= 0 {
		return nil, fmt.Errorf("%w, invalid api key id", errInvalidID)
	}
	input := &struct {
		ID       int64  `json:"id"`
		MaxScope string `json:"max_scope"`
	}{
		ID:       id,
		MaxScope: maxScope,
	}
	var resp *APIKeyData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, changeScopeInAPIKey, input, &resp, true)
}

// WSChangeSubAccountName retrieves changes the name of the requested subaccount id through the websocket connection.
func (e *Exchange) WSChangeSubAccountName(ctx context.Context, sid int64, name string) error {
	if sid <= 0 {
		return fmt.Errorf("%w, invalid subaccount user id", errInvalidID)
	}
	if name == "" {
		return errInvalidUsername
	}
	input := &struct {
		SID  int64  `json:"sid"`
		Name string `json:"name"`
	}{
		SID:  sid,
		Name: name,
	}
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, changeSubAccountName, input, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return errSubAccountNameChangeFailed
	}
	return nil
}

// WSCreateAPIKey creates an api key based on the provided settings through the websocket connection.
func (e *Exchange) WSCreateAPIKey(ctx context.Context, maxScope, name string, defaultKey bool) (*APIKeyData, error) {
	input := &struct {
		MaxScope string `json:"max_scope"`
		Name     string `json:"name,omitempty"`
		Default  bool   `json:"default"`
	}{
		MaxScope: maxScope,
		Name:     name,
		Default:  defaultKey,
	}

	var resp *APIKeyData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, createAPIKey, input, &resp, true)
}

// WSCreateSubAccount creates a new subaccount through the websocket connection.
func (e *Exchange) WSCreateSubAccount(ctx context.Context) (*SubAccountData, error) {
	var resp *SubAccountData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, createSubAccount, nil, &resp, true)
}

// WSDisableAPIKey disables the api key linked to the provided id through the websocket connection.
func (e *Exchange) WSDisableAPIKey(ctx context.Context, id int64) (*APIKeyData, error) {
	if id <= 0 {
		return nil, fmt.Errorf("%w, invalid api key id", errInvalidID)
	}
	input := &struct {
		ID int64 `json:"id"`
	}{
		ID: id,
	}
	var resp *APIKeyData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, disableAPIKey, input, &resp, true)
}

// WsEditAPIKey edits existing API key. At least one parameter is required.
// Describes maximal access for tokens generated with given key, possible values:
// trade:[read, read_write, none],
// wallet:[read, read_write, none],
// account:[read, read_write, none],
// block_trade:[read, read_write, none].
func (e *Exchange) WsEditAPIKey(ctx context.Context, id int64, maxScope, name string, enabled bool, enabledFeatures, ipWhitelist []string) (*APIKeyData, error) {
	if id == 0 {
		return nil, errInvalidAPIKeyID
	}
	if maxScope == "" {
		return nil, errMaxScopeIsRequired
	}
	input := &struct {
		ID              int64    `json:"id"`
		MaxScope        string   `json:"max_scope"`
		Name            string   `json:"name,omitempty"`
		Enabled         bool     `json:"enabled,omitempty"`
		EnabledFeatures []string `json:"enabled_features,omitempty"`
		IPWhitelist     []string `json:"ip_whitelist,omitempty"`
	}{
		ID:              id,
		MaxScope:        maxScope,
		Name:            name,
		Enabled:         enabled,
		EnabledFeatures: enabledFeatures,
		IPWhitelist:     ipWhitelist,
	}
	var resp *APIKeyData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, editAPIKey, input, &resp, true)
}

// WSEnableAffiliateProgram enables the affiliate program through the websocket connection.
func (e *Exchange) WSEnableAffiliateProgram(ctx context.Context) error {
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, enableAffiliateProgram, nil, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return errors.New("could not enable affiliate program")
	}
	return nil
}

// WSEnableAPIKey enables the api key linked to the provided id through the websocket connection.
func (e *Exchange) WSEnableAPIKey(ctx context.Context, id int64) (*APIKeyData, error) {
	if id <= 0 {
		return nil, fmt.Errorf("%w, invalid api key id", errInvalidID)
	}
	var resp *APIKeyData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, enableAPIKey, map[string]int64{"id": id}, &resp, true)
}

// WSRetrieveAccessLog lists access logs for the user through the websocket connection.
func (e *Exchange) WSRetrieveAccessLog(ctx context.Context, offset, count int64) (*AccessLog, error) {
	input := &struct {
		Offset int64 `json:"offset,omitempty"`
		Count  int64 `json:"count,omitempty"`
	}{
		Offset: offset,
		Count:  count,
	}
	var resp *AccessLog
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getAccessLog, input, &resp, true)
}

// WSRetrieveAffiliateProgramInfo retrieves the affiliate program info through the websocket connection.
func (e *Exchange) WSRetrieveAffiliateProgramInfo(ctx context.Context) (*AffiliateProgramInfo, error) {
	var resp *AffiliateProgramInfo
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getAffiliateProgramInfo, nil, &resp, true)
}

// WSRetrieveEmailLanguage retrieves the current language set for the email through the websocket connection.
func (e *Exchange) WSRetrieveEmailLanguage(ctx context.Context) (string, error) {
	var resp string
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getEmailLanguage, nil, &resp, true)
}

// WSRetrieveNewAnnouncements retrieves new announcements through the websocket connection.
func (e *Exchange) WSRetrieveNewAnnouncements(ctx context.Context) ([]Announcement, error) {
	var resp []Announcement
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getNewAnnouncements, nil, &resp, true)
}

// WSRetrievePosition retrieves the data of all positions in the requested instrument name through the websocket connection.
func (e *Exchange) WSRetrievePosition(ctx context.Context, instrument string) (*PositionData, error) {
	if instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	var resp *PositionData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getPosition, map[string]string{"instrument_name": instrument}, &resp, true)
}

// WSRetrieveSubAccounts retrieves all subaccounts' data through the websocket connection.
func (e *Exchange) WSRetrieveSubAccounts(ctx context.Context, withPortfolio bool) ([]SubAccountData, error) {
	var resp []SubAccountData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getSubAccounts, map[string]bool{"with_portfolio": withPortfolio}, &resp, true)
}

// WSRetrieveSubAccountDetails retrieves sub-account detail information through the websocket connection.
func (e *Exchange) WSRetrieveSubAccountDetails(ctx context.Context, ccy currency.Code, withOpenOrders bool) ([]SubAccountDetail, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency       currency.Code `json:"currency"`
		WithOpenOrders bool          `json:"with_open_orders,omitempty"`
	}{
		Currency:       ccy,
		WithOpenOrders: withOpenOrders,
	}
	var resp []SubAccountDetail
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getSubAccountDetails, input, &resp, true)
}

// WSRetrievePositions retrieves positions data of the user account through the websocket connection.
func (e *Exchange) WSRetrievePositions(ctx context.Context, ccy currency.Code, kind string) ([]PositionData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		Kind     string        `json:"kind,omitempty"`
	}{
		Currency: ccy,
		Kind:     kind,
	}
	var resp []PositionData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getPositions, input, &resp, true)
}

// WSRetrieveTransactionLog retrieves transaction logs data through the websocket connection.
func (e *Exchange) WSRetrieveTransactionLog(ctx context.Context, ccy currency.Code, query string, startTime, endTime time.Time, count, continuation int64) (*TransactionsData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if err := common.StartEndTimeCheck(startTime, endTime); err != nil {
		return nil, err
	}
	input := &struct {
		Currency       currency.Code `json:"currency"`
		Query          string        `json:"query,omitempty"`
		StartTimestamp int64         `json:"start_timestamp,omitempty"`
		EndTimestamp   int64         `json:"end_timestamp,omitempty"`
		Count          int64         `json:"count,omitempty"`
		Continuation   int64         `json:"continuation,omitempty"`
	}{
		Currency:       ccy,
		Query:          query,
		StartTimestamp: startTime.UnixMilli(),
		EndTimestamp:   endTime.UnixMilli(),
		Count:          count,
		Continuation:   continuation,
	}
	var resp *TransactionsData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getTransactionLog, input, &resp, true)
}

// WSRetrieveUserLocks retrieves information about locks on user account through the websocket connection.
func (e *Exchange) WSRetrieveUserLocks(ctx context.Context) ([]UserLock, error) {
	var resp []UserLock
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getUserLocks, nil, &resp, true)
}

// WSListAPIKeys retrieves all the api keys associated with a user account through the websocket connection.
func (e *Exchange) WSListAPIKeys(ctx context.Context, tfa string) ([]APIKeyData, error) {
	var resp []APIKeyData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, listAPIKeys, map[string]string{"tfa": tfa}, &resp, true)
}

// WsRetrieveCustodyAccounts retrieves user custody accounts
func (e *Exchange) WsRetrieveCustodyAccounts(ctx context.Context, ccy currency.Code) ([]CustodyAccount, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	var resp []CustodyAccount
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, listCustodyAccounts, &map[string]string{"currency": ccy.String()}, &resp, true)
}

// WSRemoveAPIKey removes api key vid ID through the websocket connection.
func (e *Exchange) WSRemoveAPIKey(ctx context.Context, id int64) error {
	if id <= 0 {
		return fmt.Errorf("%w, invalid api key id", errInvalidID)
	}
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, removeAPIKey, map[string]int64{"id": id}, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return errors.New("removal of the api key requested failed")
	}
	return nil
}

// WSRemoveSubAccount removes a subaccount given its id through the websocket connection.
func (e *Exchange) WSRemoveSubAccount(ctx context.Context, subAccountID int64) error {
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, removeSubAccount, map[string]int64{"subaccount_id": subAccountID}, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return fmt.Errorf("removal of sub account %v failed", subAccountID)
	}
	return nil
}

// WSResetAPIKey sets an announcement as read through the websocket connection.
func (e *Exchange) WSResetAPIKey(ctx context.Context, id int64) error {
	if id <= 0 {
		return fmt.Errorf("%w, invalid announcement id", errInvalidID)
	}
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, resetAPIKey, map[string]int64{"announcement_id": id}, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return fmt.Errorf("setting announcement %v as read failed", id)
	}
	return nil
}

// WSSetEmailForSubAccount links an email given to the designated subaccount through the websocket connection.
func (e *Exchange) WSSetEmailForSubAccount(ctx context.Context, sid int64, email string) error {
	if sid <= 0 {
		return fmt.Errorf("%w, invalid subaccount user id", errInvalidID)
	}
	if !common.MatchesEmailPattern(email) {
		return errInvalidEmailAddress
	}
	input := &struct {
		SID   int64  `json:"sid"`
		Email string `json:"email"`
	}{
		Email: email,
		SID:   sid,
	}
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, setEmailForSubAccount, input, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return fmt.Errorf("could not link email (%v) to subaccount %v", email, sid)
	}
	return nil
}

// WSSetEmailLanguage sets a requested language for an email through the websocket connection.
func (e *Exchange) WSSetEmailLanguage(ctx context.Context, language string) error {
	if language == "" {
		return errLanguageIsRequired
	}
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, setEmailLanguage, map[string]string{"language": language}, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return fmt.Errorf("could not set the email language to %v", language)
	}
	return nil
}

// WsSetSelfTradingConfig configure self trading behavior through the websocket connection.
// mode: Self trading prevention behavior. Possible values: 'reject_taker', 'cancel_maker'
// extended_to_subaccounts: If value is true trading is prevented between subaccounts of given account
func (e *Exchange) WsSetSelfTradingConfig(ctx context.Context, mode string, extendedToSubaccounts bool) (string, error) {
	if mode == "" {
		return "", errTradeModeIsRequired
	}
	input := &struct {
		Mode                  string `json:"mode"`
		ExtendedToSubAccounts bool   `json:"extended_to_subaccounts"`
	}{
		Mode:                  mode,
		ExtendedToSubAccounts: extendedToSubaccounts,
	}
	var resp string
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, setSelfTradingConfig, input, &resp, true)
}

// WSToggleNotificationsFromSubAccount toggles the notifications from a subaccount specified through the websocket connection.
func (e *Exchange) WSToggleNotificationsFromSubAccount(ctx context.Context, sid int64, state bool) error {
	if sid <= 0 {
		return fmt.Errorf("%w, invalid subaccount user id", errInvalidID)
	}
	input := &struct {
		SID   int64 `json:"sid"`
		State bool  `json:"state"`
	}{
		SID:   sid,
		State: state,
	}
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, toggleNotificationsFromSubAccount, input, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return fmt.Errorf("toggling notifications for subaccount %v to %v failed", sid, state)
	}
	return nil
}

// WSTogglePortfolioMargining toggle between SM and PM models through the websocket connection.
func (e *Exchange) WSTogglePortfolioMargining(ctx context.Context, userID int64, enabled, dryRun bool) ([]TogglePortfolioMarginResponse, error) {
	if userID == 0 {
		return nil, errUserIDRequired
	}
	input := &struct {
		UserID  int64 `json:"user_id"`
		Enabled bool  `json:"enabled"`
		DryRun  bool  `json:"dry_run"`
	}{
		UserID:  userID,
		Enabled: enabled,
		DryRun:  dryRun,
	}
	var resp []TogglePortfolioMarginResponse
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, togglePortfolioMargining, input, &resp, true)
}

// WSToggleSubAccountLogin toggles access for subaccount login through the websocket connection.
func (e *Exchange) WSToggleSubAccountLogin(ctx context.Context, sid int64, state bool) error {
	if sid <= 0 {
		return fmt.Errorf("%w, invalid subaccount user id", errInvalidID)
	}
	input := &struct {
		SID   int64 `json:"sid"`
		State bool  `json:"state"`
	}{
		SID:   sid,
		State: state,
	}
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, toggleSubAccountLogin, input, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return fmt.Errorf("toggling login access for subaccount %v to %v failed", sid, state)
	}
	return nil
}

// WSSubmitBuy submits a private buy request through the websocket connection.
func (e *Exchange) WSSubmitBuy(ctx context.Context, arg *OrderBuyAndSellParams) (*PrivateTradeData, error) {
	if arg == nil || *arg == (OrderBuyAndSellParams{}) {
		return nil, fmt.Errorf("%w parameter is required", common.ErrNilPointer)
	}
	if arg.Instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	var resp *PrivateTradeData
	return resp, e.SendWSRequest(ctx, matchingEPL, submitBuy, &arg, &resp, true)
}

// WSSubmitSell submits a sell request with the parameters provided through the websocket connection.
func (e *Exchange) WSSubmitSell(ctx context.Context, arg *OrderBuyAndSellParams) (*PrivateTradeData, error) {
	if arg == nil || *arg == (OrderBuyAndSellParams{}) {
		return nil, fmt.Errorf("%w parameter is required", common.ErrNilPointer)
	}
	if arg.Instrument == "" {
		return nil, fmt.Errorf("%w, instrument_name is missing", errInvalidInstrumentName)
	}
	var resp *PrivateTradeData
	return resp, e.SendWSRequest(ctx, matchingEPL, submitSell, &arg, &resp, true)
}

// WSSubmitEdit submits an edit order request through the websocket connection.
func (e *Exchange) WSSubmitEdit(ctx context.Context, arg *OrderBuyAndSellParams) (*PrivateTradeData, error) {
	if arg == nil || *arg == (OrderBuyAndSellParams{}) {
		return nil, common.ErrNilPointer
	}
	if arg.OrderID == "" {
		return nil, fmt.Errorf("%w, order id is required", errInvalidID)
	}
	if arg.Amount <= 0 {
		return nil, errInvalidAmount
	}
	var resp *PrivateTradeData
	return resp, e.SendWSRequest(ctx, matchingEPL, submitEdit, &arg, &resp, true)
}

// WSEditOrderByLabel submits an edit order request sorted via label through the websocket connection.
func (e *Exchange) WSEditOrderByLabel(ctx context.Context, arg *OrderBuyAndSellParams) (*PrivateTradeData, error) {
	if arg == nil || *arg == (OrderBuyAndSellParams{}) {
		return nil, fmt.Errorf("%w argument cannot be null", common.ErrNilPointer)
	}
	if arg.Instrument == "" {
		return nil, errInvalidInstrumentName
	}
	if arg.Amount <= 0 {
		return nil, errInvalidAmount
	}
	var resp *PrivateTradeData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, editByLabel, &arg, &resp, true)
}

// WSSubmitCancel sends a request to cancel the order via its orderID through the websocket connection.
func (e *Exchange) WSSubmitCancel(ctx context.Context, orderID string) (*PrivateCancelData, error) {
	if orderID == "" {
		return nil, fmt.Errorf("%w, no order ID specified", errInvalidID)
	}
	var resp *PrivateCancelData
	return resp, e.SendWSRequest(ctx, matchingEPL, submitCancel, map[string]string{"order_id": orderID}, &resp, true)
}

// WSSubmitCancelAll sends a request to cancel all user orders in all currencies and instruments
func (e *Exchange) WSSubmitCancelAll(ctx context.Context, detailed bool) (*MultipleCancelResponse, error) {
	var resp *MultipleCancelResponse
	return resp, e.SendWSRequest(ctx, matchingEPL, submitCancelAll, map[string]bool{"detailed": detailed}, &resp, true)
}

// WSSubmitCancelAllByCurrency sends a request to cancel all user orders for the specified currency through the websocket connection.
func (e *Exchange) WSSubmitCancelAllByCurrency(ctx context.Context, ccy currency.Code, kind, orderType string, detailed bool) (*MultipleCancelResponse, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency  currency.Code `json:"currency"`
		Kind      string        `json:"kind,omitempty"`
		OrderType string        `json:"order_type,omitempty"`
		Detailed  bool          `json:"detailed"`
	}{
		Currency:  ccy,
		Kind:      kind,
		OrderType: orderType,
		Detailed:  detailed,
	}
	var resp *MultipleCancelResponse
	return resp, e.SendWSRequest(ctx, matchingEPL, submitCancelAllByCurrency, input, &resp, true)
}

// WSSubmitCancelAllByInstrument sends a request to cancel all user orders for the specified instrument through the websocket connection.
func (e *Exchange) WSSubmitCancelAllByInstrument(ctx context.Context, instrument, orderType string, detailed, includeCombos bool) (*MultipleCancelResponse, error) {
	if instrument == "" {
		return nil, errInvalidInstrumentName
	}
	input := &struct {
		Instrument    string `json:"instrument_name"`
		OrderType     string `json:"type,omitempty"`
		Detailed      bool   `json:"detailed,omitempty"`
		IncludeCombos bool   `json:"include_combos,omitempty"`
	}{
		Instrument:    instrument,
		OrderType:     orderType,
		Detailed:      detailed,
		IncludeCombos: includeCombos,
	}
	var resp *MultipleCancelResponse
	return resp, e.SendWSRequest(ctx, matchingEPL, submitCancelAllByInstrument, input, &resp, true)
}

// WsSubmitCancelAllByKind cancels all orders in currency(currencies), optionally filtered by instrument kind and/or order type.
// 'kind' Instrument kind. Possible values: 'future', 'option', 'spot', 'future_combo', 'option_combo', 'combo', 'any'
func (e *Exchange) WsSubmitCancelAllByKind(ctx context.Context, ccy currency.Code, kind, orderType string, detailed bool) (*MultipleCancelResponse, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency  string `json:"currency"`
		Kind      string `json:"kind,omitempty"`
		OrderType string `json:"type,omitempty"`
		Detailed  bool   `json:"detailed,omitempty"`
	}{
		Currency:  ccy.String(),
		Kind:      kind,
		OrderType: orderType,
		Detailed:  detailed,
	}
	var resp *MultipleCancelResponse
	return resp, e.SendWSRequest(ctx, matchingEPL, submitCancelAllByKind, input, &resp, true)
}

// WSSubmitCancelByLabel sends a request to cancel all user orders for the specified label through the websocket connection.
func (e *Exchange) WSSubmitCancelByLabel(ctx context.Context, label string, ccy currency.Code, detailed bool) (*MultipleCancelResponse, error) {
	input := &struct {
		Label    string `json:"label"`
		Currency string `json:"currency,omitempty"`
		Detailed bool   `json:"detailed,omitempty"`
	}{
		Label:    label,
		Currency: ccy.String(),
		Detailed: detailed,
	}
	var resp *MultipleCancelResponse
	return resp, e.SendWSRequest(ctx, matchingEPL, submitCancelByLabel, input, &resp, true)
}

// WSSubmitCancelQuotes cancels quotes based on the provided type.
//
// possible cancel_type values are delta, 'quote_set_id', 'instrument', 'instrument_kind', 'currency', and 'all'
// possible kind values are future 'option', 'spot', 'future_combo', 'option_combo', 'combo', and 'any'
func (e *Exchange) WSSubmitCancelQuotes(ctx context.Context, ccy currency.Code, minDelta, maxDelta float64, cancelType, quoteSetID, instrumentName, kind string, detailed bool) (*MultipleCancelResponse, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if cancelType == "" {
		return nil, errors.New("cancel type is required")
	}
	input := &struct {
		CancelType     string  `json:"cancel_type"`
		Currency       string  `json:"currency"`
		Detailed       bool    `json:"detailed,omitempty"`
		MinDelta       float64 `json:"min_delta,omitempty"`
		MaxDelta       float64 `json:"max_delta,omitempty"`
		InstrumentName string  `json:"instrument_name,omitempty"`
		QuoteSetID     string  `json:"quote_set_id,omitempty"`
		Kind           string  `json:"kind,omitempty"`
	}{
		CancelType:     cancelType,
		Currency:       ccy.String(),
		Detailed:       detailed,
		MinDelta:       minDelta,
		MaxDelta:       maxDelta,
		InstrumentName: instrumentName,
		Kind:           kind,
		QuoteSetID:     quoteSetID,
	}
	var resp *MultipleCancelResponse
	return resp, e.SendWSRequest(ctx, matchingEPL, submitCancelQuotes, input, &resp, true)
}

// WSSubmitClosePosition sends a request to cancel all user orders for the specified label through the websocket connection.
func (e *Exchange) WSSubmitClosePosition(ctx context.Context, instrument, orderType string, price float64) (*PrivateTradeData, error) {
	if instrument == "" {
		return nil, errInvalidInstrumentName
	}
	input := &struct {
		Instrument string  `json:"instrument_name"`
		Type       string  `json:"type,omitempty"`
		Price      float64 `json:"price"`
	}{
		Instrument: instrument,
		Type:       orderType,
		Price:      price,
	}
	var resp *PrivateTradeData
	return resp, e.SendWSRequest(ctx, matchingEPL, submitClosePosition, input, &resp, true)
}

// WSRetrieveMargins sends a request to fetch account margins data through the websocket connection.
func (e *Exchange) WSRetrieveMargins(ctx context.Context, instrument string, amount, price float64) (*MarginsData, error) {
	if instrument == "" {
		return nil, errInvalidInstrumentName
	}
	if amount <= 0 {
		return nil, errInvalidAmount
	}
	if price <= 0 {
		return nil, errInvalidPrice
	}
	input := &struct {
		Instrument string  `json:"instrument_name"`
		Amount     float64 `json:"amount"`
		Price      float64 `json:"price"`
	}{
		Instrument: instrument,
		Amount:     amount,
		Price:      price,
	}
	var resp *MarginsData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getMargins, input, &resp, true)
}

// WSRetrieveMMPConfig sends a request to fetch the config for MMP of the requested currency through the websocket connection.
func (e *Exchange) WSRetrieveMMPConfig(ctx context.Context, ccy currency.Code) (*MMPConfigData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	var resp *MMPConfigData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getMMPConfig, map[string]currency.Code{"currency": ccy}, &resp, true)
}

// WSRetrieveOpenOrdersByCurrency retrieves open order by symbol and kind
func (e *Exchange) WSRetrieveOpenOrdersByCurrency(ctx context.Context, ccy currency.Code, kind, orderType string) ([]OrderData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency  currency.Code `json:"currency"`
		Kind      string        `json:"kind,omitempty"`
		OrderType string        `json:"type,omitempty"`
	}{
		Currency:  ccy,
		Kind:      kind,
		OrderType: orderType,
	}
	var resp []OrderData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOpenOrdersByCurrency, input, &resp, true)
}

// WSRetrieveOpenOrdersByLabel retrieves open order by label and currency
func (e *Exchange) WSRetrieveOpenOrdersByLabel(ctx context.Context, ccy currency.Code, label string) ([]OrderData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		Label    string        `json:"label"`
	}{
		Currency: ccy,
		Label:    label,
	}
	var resp []OrderData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOpenOrdersByLabel, input, &resp, true)
}

// WSRetrieveOpenOrdersByInstrument sends a request to fetch open orders data sorted by requested params through the websocket connection.
func (e *Exchange) WSRetrieveOpenOrdersByInstrument(ctx context.Context, instrument, orderType string) ([]OrderData, error) {
	if instrument == "" {
		return nil, errInvalidInstrumentName
	}
	input := &struct {
		Instrument string `json:"instrument_name"`
		Type       string `json:"type,omitempty"`
	}{
		Instrument: instrument,
		Type:       orderType,
	}
	var resp []OrderData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOpenOrdersByInstrument, input, &resp, true)
}

// WSRetrieveOrderHistoryByCurrency sends a request to fetch order history according to given params and currency through the websocket connection.
func (e *Exchange) WSRetrieveOrderHistoryByCurrency(ctx context.Context, ccy currency.Code, kind string, count, offset int64, includeOld, includeUnfilled bool) ([]OrderData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency        currency.Code `json:"currency"`
		Kind            string        `json:"kind,omitempty"`
		Count           int64         `json:"count,omitempty"`
		Offset          int64         `json:"offset,omitempty"`
		IncludeOld      bool          `json:"include_old,omitempty"`
		IncludeUnfilled bool          `json:"include_unfilled,omitempty"`
	}{
		Currency:        ccy,
		Kind:            kind,
		Count:           count,
		Offset:          offset,
		IncludeOld:      includeOld,
		IncludeUnfilled: includeUnfilled,
	}
	var resp []OrderData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOrderHistoryByCurrency, input, &resp, true)
}

// WSRetrieveOrderHistoryByInstrument sends a request to fetch order history according to given params and instrument through the websocket connection.
func (e *Exchange) WSRetrieveOrderHistoryByInstrument(ctx context.Context, instrument string, count, offset int64, includeOld, includeUnfilled bool) ([]OrderData, error) {
	if instrument == "" {
		return nil, errInvalidInstrumentName
	}
	input := &struct {
		Instrument      string `json:"instrument_name"`
		Count           int64  `json:"count,omitempty"`
		Offset          int64  `json:"offset,omitempty"`
		IncludeOld      bool   `json:"include_old"`
		IncludeUnfilled bool   `json:"include_unfilled"`
	}{
		Instrument:      instrument,
		Count:           count,
		Offset:          offset,
		IncludeOld:      includeOld,
		IncludeUnfilled: includeUnfilled,
	}
	var resp []OrderData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOrderHistoryByInstrument, input, &resp, true)
}

// WSRetrieveOrderMarginsByID sends a request to fetch order margins data according to their ids through the websocket connection.
func (e *Exchange) WSRetrieveOrderMarginsByID(ctx context.Context, ids []string) ([]OrderData, error) {
	if len(ids) == 0 {
		return nil, fmt.Errorf("%w, order ids cannot be empty", errInvalidID)
	}
	var resp []OrderData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOrderMarginByIDs, map[string][]string{"ids": ids}, &resp, true)
}

// WSRetrievesOrderState sends a request to fetch order state of the order id provided
func (e *Exchange) WSRetrievesOrderState(ctx context.Context, orderID string) (*OrderData, error) {
	if orderID == "" {
		return nil, fmt.Errorf("%w, no order ID specified", errInvalidID)
	}
	var resp *OrderData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOrderState, map[string]string{"order_id": orderID}, &resp, true)
}

// WsRetrieveOrderStateByLabel retrieves an order state by label and currency
func (e *Exchange) WsRetrieveOrderStateByLabel(ctx context.Context, ccy currency.Code, label string) ([]OrderData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		Label    string        `json:"label"`
	}{
		Currency: ccy,
		Label:    label,
	}
	var resp []OrderData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getOrderStateByLabel, input, &resp, true)
}

// WSRetrieveTriggerOrderHistory sends a request to fetch order state of the order id provided through the websocket connection.
func (e *Exchange) WSRetrieveTriggerOrderHistory(ctx context.Context, ccy currency.Code, instrumentName, continuation string, count int64) (*OrderData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency     string `json:"currency,omitempty"`
		Instrument   string `json:"instrument,omitempty"`
		Continuation string `json:"continuation,omitempty"`
		Count        int64  `json:"count,omitempty"`
	}{
		Currency:     ccy.String(),
		Instrument:   instrumentName,
		Continuation: continuation,
		Count:        count,
	}
	var resp *OrderData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getTriggerOrderHistory, input, &resp, true)
}

// WSRetrieveUserTradesByCurrency sends a request to fetch user trades sorted by currency through the websocket connection.
func (e *Exchange) WSRetrieveUserTradesByCurrency(ctx context.Context, ccy currency.Code, kind, startID, endID, sorting string, count int64, includeOld bool) (*UserTradesData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency   currency.Code `json:"currency"`
		Kind       string        `json:"kind"`
		StartID    string        `json:"start_id,omitempty"`
		EndID      string        `json:"end_id,omitempty"`
		Sorting    string        `json:"sorting,omitempty"`
		Count      int64         `json:"count,omitempty"`
		IncludeOld bool          `json:"include_old,omitempty"`
	}{
		Currency:   ccy,
		Kind:       kind,
		StartID:    startID,
		EndID:      endID,
		Sorting:    sorting,
		Count:      count,
		IncludeOld: includeOld,
	}
	var resp *UserTradesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getUserTradesByCurrency, input, &resp, true)
}

// WSRetrieveUserTradesByCurrencyAndTime retrieves user trades sorted by currency and time through the websocket connection.
func (e *Exchange) WSRetrieveUserTradesByCurrencyAndTime(ctx context.Context, ccy currency.Code, kind, sorting string, count int64, startTime, endTime time.Time) (*UserTradesData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency  currency.Code `json:"currency"`
		Kind      string        `json:"kind,omitempty"`
		StartTime int64         `json:"start_timestamp,omitempty"`
		EndTime   int64         `json:"end_timestamp,omitempty"`
		Sorting   string        `json:"sorting,omitempty"`
		Count     int64         `json:"count,omitempty"`
	}{
		Currency: ccy,
		Kind:     kind,
		Sorting:  sorting,
		Count:    count,
	}
	if !startTime.IsZero() {
		input.StartTime = startTime.UnixMilli()
	}
	if !endTime.IsZero() {
		input.EndTime = endTime.UnixMilli()
	}
	var resp *UserTradesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getUserTradesByCurrencyAndTime, input, &resp, true)
}

// WsRetrieveUserTradesByInstrument retrieves user trades sorted by instrument through the websocket connection.
func (e *Exchange) WsRetrieveUserTradesByInstrument(ctx context.Context, instrument, sorting string, startSeq, endSeq, count int64, includeOld bool) (*UserTradesData, error) {
	if instrument == "" {
		return nil, errInvalidInstrumentName
	}
	input := &struct {
		Instrument string `json:"instrument_name"`
		StartSeq   int64  `json:"start_seq,omitempty"`
		EndSeq     int64  `json:"end_seq,omitempty"`
		Sorting    string `json:"sorting,omitempty"`
		Count      int64  `json:"count,omitempty"`
		IncludeOld bool   `json:"include_old,omitempty"`
	}{
		Instrument: instrument,
		StartSeq:   startSeq,
		EndSeq:     endSeq,
		Sorting:    sorting,
		Count:      count,
		IncludeOld: includeOld,
	}
	var resp *UserTradesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getUserTradesByInstrument, input, &resp, true)
}

// WSRetrieveUserTradesByInstrumentAndTime retrieves user trades sorted by instrument and time through the websocket connection.
func (e *Exchange) WSRetrieveUserTradesByInstrumentAndTime(ctx context.Context, instrument, sorting string, count int64, includeOld bool, startTime, endTime time.Time) (*UserTradesData, error) {
	if instrument == "" {
		return nil, errInvalidInstrumentName
	}
	if err := common.StartEndTimeCheck(startTime, endTime); err != nil {
		return nil, err
	}
	input := &struct {
		Instrument string `json:"instrument_name"`
		StartTime  int64  `json:"start_timestamp,omitempty"`
		EndTime    int64  `json:"end_timestamp,omitempty"`
		Sorting    string `json:"sorting,omitempty"`
		Count      int64  `json:"count,omitempty"`
		IncludeOld bool   `json:"include_old,omitempty"`
	}{
		Instrument: instrument,
		StartTime:  startTime.UnixMilli(),
		EndTime:    endTime.UnixMilli(),
		Sorting:    sorting,
		Count:      count,
		IncludeOld: includeOld,
	}
	var resp *UserTradesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getUserTradesByInstrumentAndTime, input, &resp, true)
}

// WSRetrieveUserTradesByOrder retrieves user trades fetched by orderID through the web socket connection.
func (e *Exchange) WSRetrieveUserTradesByOrder(ctx context.Context, orderID, sorting string) (*UserTradesData, error) {
	if orderID == "" {
		return nil, fmt.Errorf("%w, no order ID specified", errInvalidID)
	}
	input := &struct {
		OrderID string `json:"order_id"`
		Sorting string `json:"sorting,omitempty"`
	}{
		OrderID: orderID,
		Sorting: sorting,
	}
	var resp *UserTradesData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getUserTradesByOrder, input, &resp, true)
}

// WSResetMMP sends a request to reset MMP for a currency provided through the websocket connection.
func (e *Exchange) WSResetMMP(ctx context.Context, ccy currency.Code) error {
	if ccy.IsEmpty() {
		return currency.ErrCurrencyCodeEmpty
	}
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, resetMMP, map[string]currency.Code{"currency": ccy}, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return fmt.Errorf("mmp could not be reset for %v", ccy.String())
	}
	return nil
}

// WSSetMMPConfig sends a request to set the given parameter values to the mmp config for the provided currency through the websocket connection.
func (e *Exchange) WSSetMMPConfig(ctx context.Context, ccy currency.Code, interval kline.Interval, frozenTime int64, quantityLimit, deltaLimit float64) error {
	if ccy.IsEmpty() {
		return currency.ErrCurrencyCodeEmpty
	}
	params := make(map[string]any)
	params["currency"] = ccy
	intervalString, err := e.GetResolutionFromInterval(interval)
	if err != nil {
		return err
	}
	params["interval"] = intervalString
	params["frozen_time"] = frozenTime
	if quantityLimit != 0 {
		params["quantity_time"] = quantityLimit
	}
	if deltaLimit != 0 {
		params["delta_limit"] = deltaLimit
	}
	var resp string
	err = e.SendWSRequest(ctx, nonMatchingEPL, setMMPConfig, params, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return fmt.Errorf("mmp data could not be set for %v", ccy.String())
	}
	return nil
}

// WSRetrieveSettlementHistoryByInstrument sends a request to fetch settlement history data sorted by instrument through the websocket connection.
func (e *Exchange) WSRetrieveSettlementHistoryByInstrument(ctx context.Context, instrument, settlementType, continuation string, count int64, searchStartTimeStamp time.Time) (*PrivateSettlementsHistoryData, error) {
	if instrument == "" {
		return nil, errInvalidInstrumentName
	}
	input := &struct {
		Instrument           string `json:"instrument_name"`
		Continuation         string `json:"continuation,omitempty"`
		Count                int64  `json:"count,omitempty"`
		SearchStartTimestamp int64  `json:"search_start_timestamp,omitempty"`
		Type                 string `json:"type,omitempty"`
	}{
		Instrument:   instrument,
		Continuation: continuation,
		Count:        count,
		Type:         settlementType,
	}
	if !searchStartTimeStamp.IsZero() {
		input.SearchStartTimestamp = searchStartTimeStamp.UnixMilli()
	}
	var resp *PrivateSettlementsHistoryData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getSettlementHistoryByInstrument, input, &resp, true)
}

// WSRetrieveSettlementHistoryByCurency sends a request to fetch settlement history data sorted by currency through the websocket connection.
func (e *Exchange) WSRetrieveSettlementHistoryByCurency(ctx context.Context, ccy currency.Code, settlementType, continuation string, count int64, searchStartTimeStamp time.Time) (*PrivateSettlementsHistoryData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency             currency.Code `json:"currency"`
		SettlementType       string        `json:"settlement_type,omitempty"`
		Continuation         string        `json:"continuation,omitempty"`
		Count                int64         `json:"count,omitempty"`
		SearchStartTimestamp int64         `json:"search_start_timestamp,omitempty"`
	}{
		Currency:       ccy,
		SettlementType: settlementType,
		Continuation:   continuation,
		Count:          count,
	}
	if !searchStartTimeStamp.IsZero() {
		input.SearchStartTimestamp = searchStartTimeStamp.UnixMilli()
	}
	var resp *PrivateSettlementsHistoryData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getSettlementHistoryByCurrency, input, &resp, true)
}

// WSRetrieveComboIDs Retrieves available combos.
// This method can be used to get the list of all combos, or only the list of combos in the given state.
func (e *Exchange) WSRetrieveComboIDs(ctx context.Context, ccy currency.Code, state string) ([]string, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		State    string        `json:"state,omitempty"`
	}{
		Currency: ccy,
		State:    state,
	}
	var resp []string
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getComboIDs, input, &resp, false)
}

// WSRetrieveComboDetails retrieves information about a combo through the websocket connection.
func (e *Exchange) WSRetrieveComboDetails(ctx context.Context, comboID string) (*ComboDetail, error) {
	if comboID == "" {
		return nil, errInvalidComboID
	}
	var resp *ComboDetail
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getComboDetails, map[string]string{"combo_id": comboID}, &resp, false)
}

// WSRetrieveCombos retrieves information about active combos through the websocket connection.
func (e *Exchange) WSRetrieveCombos(ctx context.Context, ccy currency.Code) ([]ComboDetail, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	var resp []ComboDetail
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getCombos, map[string]currency.Code{"currency": ccy}, &resp, false)
}

// WSCreateCombo verifies and creates a combo book or returns an existing combo matching given trades through the websocket connection.
func (e *Exchange) WSCreateCombo(ctx context.Context, args []ComboParam) (*ComboDetail, error) {
	if len(args) == 0 {
		return nil, errNoArgumentPassed
	}
	for x := range args {
		if args[x].InstrumentName == "" {
			return nil, fmt.Errorf("%w, empty string", errInvalidInstrumentName)
		}
		args[x].Direction = strings.ToLower(args[x].Direction)
		if args[x].Direction != sideBUY && args[x].Direction != sideSELL {
			return nil, errInvalidOrderSideOrDirection
		}
		if args[x].Amount <= 0 {
			return nil, errInvalidAmount
		}
	}
	var resp *ComboDetail
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, createCombos, map[string]any{"trades": args}, &resp, true)
}

// WsLogout gracefully close websocket connection, when COD (Cancel On Disconnect) is enabled orders are not cancelled
func (e *Exchange) WsLogout(ctx context.Context, invalidateToken bool) error {
	input := struct {
		InvalidateToken bool `json:"invalidate_token,omitempty"`
	}{
		InvalidateToken: invalidateToken,
	}
	return e.SendWSRequest(ctx, nonMatchingEPL, "private/logout", input, &struct{}{}, true)
}

// WsEnableCancelOnDisconnect enable Cancel On Disconnect for the connection.
// After enabling Cancel On Disconnect all orders created by the connection will be removed when the connection is closed.
func (e *Exchange) WsEnableCancelOnDisconnect(ctx context.Context, scope string) (string, error) {
	input := &struct {
		Scope string `json:"scope,omitempty"`
	}{
		Scope: scope,
	}
	var resp string
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, "private/enable_cancel_on_disconnect", input, &resp, true)
}

// WsDisableCancelOnDisconnect isable Cancel On Disconnect for the connection.
// When change is applied for the account, then every newly opened connection will start with inactive Cancel on Disconnect.
// scope: possible values are 'connection', 'account'
func (e *Exchange) WsDisableCancelOnDisconnect(ctx context.Context, scope string) (string, error) {
	input := &struct {
		Scope string `json:"scope,omitempty"`
	}{
		Scope: scope,
	}
	var resp string
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, "private/disable_cancel_on_disconnect", input, &resp, true)
}

// SayHello method used to introduce the client software connected to Deribit platform over websocket.
// It returns version information
func (e *Exchange) SayHello(ctx context.Context, clientName, clientVersion string) (*Info, error) {
	if clientName == "" {
		return nil, errors.New("client name is required")
	}
	input := &struct {
		ClientName    string `json:"client_name"`
		ClientVersion string `json:"client_version"`
	}{
		ClientName:    clientName,
		ClientVersion: clientVersion,
	}
	var resp *Info
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, "public/hello", input, &resp, false)
}

// WsRetrieveCancelOnDisconnect read current Cancel On Disconnect configuration for the account.
// 'scope': Specifies if Cancel On Disconnect change should be applied/checked for the current connection or the account (default - connection)
// Scope connection can be used only when working via Websocket.
func (e *Exchange) WsRetrieveCancelOnDisconnect(ctx context.Context, scope string) (*CancelOnDisconnect, error) {
	input := &struct {
		Scope string `json:"scope,omitempty"`
	}{
		Scope: scope,
	}
	var resp *CancelOnDisconnect
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, "private/get_cancel_on_disconnect", input, &resp, true)
}

// WsExchangeToken generates a token for a new subject id. This method can be used to switch between subaccounts.
func (e *Exchange) WsExchangeToken(ctx context.Context, refreshToken string, subjectID int64) (*RefreshTokenInfo, error) {
	if refreshToken == "" {
		return nil, errRefreshTokenRequired
	}
	if subjectID == 0 {
		return nil, errors.New("subject id is required")
	}
	input := &struct {
		RefreshToken string `json:"retresh_token"`
		SubjectID    int64  `json:"subject_id"`
	}{
		RefreshToken: refreshToken,
		SubjectID:    subjectID,
	}
	var resp *RefreshTokenInfo
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, "public/exchange_token", input, &resp, true)
}

// WsForkToken generates a token for a new named session. This method can be used only with session scoped tokens.
func (e *Exchange) WsForkToken(ctx context.Context, refreshToken, sessionName string) (*RefreshTokenInfo, error) {
	if refreshToken == "" {
		return nil, errRefreshTokenRequired
	}
	if sessionName == "" {
		return nil, errSessionNameRequired
	}
	input := &struct {
		RefreshToken string `json:"refresh_token"`
		SessionName  string `json:"session_name"`
	}{
		RefreshToken: refreshToken,
		SessionName:  sessionName,
	}
	var resp *RefreshTokenInfo
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, "public/fork_token", input, &resp, true)
}

// UnsubscribeAll unsubscribe from all the public channels subscribed so far.
func (e *Exchange) UnsubscribeAll(ctx context.Context) (string, error) {
	var resp string
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, "public/unsubscribe_all", nil, &resp, false)
}

// UnsubscribeAllPrivateChannels sends an unsubscribe request to cancel all private channels subscriptions
func (e *Exchange) UnsubscribeAllPrivateChannels(ctx context.Context) (string, error) {
	var resp string
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, "private/unsubscribe_all", nil, &resp, false)
}

// ------------------------------------------------------------------------------------------------

// WSExecuteBlockTrade executes a block trade request
// The whole request have to be exact the same as in private/verify_block_trade, only role field should be set appropriately - it basically means that both sides have to agree on the same timestamp, nonce, trades fields and server will assure that role field is different between sides (each party accepted own role).
// Using the same timestamp and nonce by both sides in private/verify_block_trade assures that even if unintentionally both sides execute given block trade with valid counterparty_signature, the given block trade will be executed only once
func (e *Exchange) WSExecuteBlockTrade(ctx context.Context, timestampMS time.Time, nonce, role string, ccy currency.Code, trades []BlockTradeParam) ([]BlockTradeResponse, error) {
	if nonce == "" {
		return nil, errMissingNonce
	}
	if role != roleMaker && role != roleTaker {
		return nil, errInvalidTradeRole
	}
	if len(trades) == 0 {
		return nil, errNoArgumentPassed
	}
	for x := range trades {
		if trades[x].InstrumentName == "" {
			return nil, fmt.Errorf("%w, empty string", errInvalidInstrumentName)
		}
		trades[x].Direction = strings.ToLower(trades[x].Direction)
		if trades[x].Direction != sideBUY && trades[x].Direction != sideSELL {
			return nil, errInvalidOrderSideOrDirection
		}
		if trades[x].Amount <= 0 {
			return nil, errInvalidAmount
		}
		if trades[x].Price < 0 {
			return nil, fmt.Errorf("%w, trade price can't be negative", errInvalidPrice)
		}
	}
	signature, err := e.WSVerifyBlockTrade(ctx, timestampMS, nonce, role, ccy, trades)
	if err != nil {
		return nil, err
	}
	input := &struct {
		Nonce                 string            `json:"nonce"`
		Role                  string            `json:"role,omitempty"`
		CounterpartySignature string            `json:"counterparty_signature"`
		Trades                []BlockTradeParam `json:"trades"`
		Timestamp             int64             `json:"timestamp"`
		Currency              string            `json:"currency,omitempty"`
	}{
		Nonce:                 nonce,
		Role:                  role,
		CounterpartySignature: signature,
		Trades:                trades,
		Timestamp:             timestampMS.UnixMilli(),
		Currency:              ccy.String(),
	}
	var resp []BlockTradeResponse
	return resp, e.SendWSRequest(ctx, matchingEPL, executeBlockTrades, input, &resp, true)
}

// WSVerifyBlockTrade verifies and creates block trade signature through the websocket connection.
func (e *Exchange) WSVerifyBlockTrade(ctx context.Context, timestampMS time.Time, nonce, role string, ccy currency.Code, trades []BlockTradeParam) (string, error) {
	if nonce == "" {
		return "", errMissingNonce
	}
	if role != roleMaker && role != roleTaker {
		return "", errInvalidTradeRole
	}
	if len(trades) == 0 {
		return "", errNoArgumentPassed
	}
	for x := range trades {
		if trades[x].InstrumentName == "" {
			return "", fmt.Errorf("%w, empty string", errInvalidInstrumentName)
		}
		trades[x].Direction = strings.ToLower(trades[x].Direction)
		if trades[x].Direction != sideBUY && trades[x].Direction != sideSELL {
			return "", errInvalidOrderSideOrDirection
		}
		if trades[x].Amount <= 0 {
			return "", errInvalidAmount
		}
		if trades[x].Price < 0 {
			return "", fmt.Errorf("%w, trade price can't be negative", errInvalidPrice)
		}
	}
	if timestampMS.IsZero() {
		return "", errZeroTimestamp
	}
	input := &struct {
		Nonce                 string            `json:"nonce"`
		Role                  string            `json:"role,omitempty"`
		CounterpartySignature string            `json:"counterparty_signature"`
		Trades                []BlockTradeParam `json:"trades"`
		Timestamp             int64             `json:"timestamp"`
		Currency              string            `json:"currency,omitempty"`
	}{
		Nonce:     nonce,
		Role:      role,
		Trades:    trades,
		Timestamp: timestampMS.UnixMilli(),
		Currency:  ccy.String(),
	}
	resp := &struct {
		Signature string `json:"signature"`
	}{}
	return resp.Signature, e.SendWSRequest(ctx, matchingEPL, verifyBlockTrades, input, &resp, true)
}

// WsInvalidateBlockTradeSignature user at any time (before the private/execute_block_trade is called) can invalidate its own signature effectively cancelling block trade through the websocket connection.
func (e *Exchange) WsInvalidateBlockTradeSignature(ctx context.Context, signature string) error {
	if signature == "" {
		return errMissingSignature
	}
	params := url.Values{}
	params.Set("signature", signature)
	var resp string
	err := e.SendWSRequest(ctx, nonMatchingEPL, invalidateBlockTradesSignature, params, &resp, true)
	if err != nil {
		return err
	}
	if resp != "ok" {
		return fmt.Errorf("server response: %s", resp)
	}
	return nil
}

// WSRetrieveUserBlockTrade returns information about users block trade through the websocket connection.
func (e *Exchange) WSRetrieveUserBlockTrade(ctx context.Context, id string) ([]BlockTradeData, error) {
	if id == "" {
		return nil, errMissingBlockTradeID
	}
	var resp []BlockTradeData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getBlockTrades, map[string]string{"id": id}, &resp, true)
}

// WSRetrieveLastBlockTradesByCurrency returns list of last users block trades through the websocket connection.
func (e *Exchange) WSRetrieveLastBlockTradesByCurrency(ctx context.Context, ccy currency.Code, startID, endID string, count int64) ([]BlockTradeData, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	input := &struct {
		Currency currency.Code `json:"currency"`
		StartID  string        `json:"start_id,omitempty"`
		EndID    string        `json:"end_id,omitempty"`
		Count    int64         `json:"count,omitempty"`
	}{
		Currency: ccy,
		StartID:  startID,
		EndID:    endID,
		Count:    count,
	}
	var resp []BlockTradeData
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, getLastBlockTradesByCurrency, input, &resp, true)
}

// WSMovePositions moves positions from source subaccount to target subaccount through the websocket connection.
func (e *Exchange) WSMovePositions(ctx context.Context, ccy currency.Code, sourceSubAccountUID, targetSubAccountUID int64, trades []BlockTradeParam) ([]BlockTradeMoveResponse, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if sourceSubAccountUID == 0 {
		return nil, fmt.Errorf("%w source sub-account id", errMissingSubAccountID)
	}
	if targetSubAccountUID == 0 {
		return nil, fmt.Errorf("%w target sub-account id", errMissingSubAccountID)
	}
	for x := range trades {
		if trades[x].InstrumentName == "" {
			return nil, fmt.Errorf("%w, empty string", errInvalidInstrumentName)
		}
		if trades[x].Amount <= 0 {
			return nil, errInvalidAmount
		}
		if trades[x].Price < 0 {
			return nil, fmt.Errorf("%w, trade price can't be negative", errInvalidPrice)
		}
	}
	input := &struct {
		Currency  currency.Code     `json:"currency"`
		Trades    []BlockTradeParam `json:"trades"`
		TargetUID int64             `json:"target_uid"`
		SourceUID int64             `json:"source_uid"`
	}{
		Currency:  ccy,
		Trades:    trades,
		TargetUID: targetSubAccountUID,
		SourceUID: sourceSubAccountUID,
	}
	var resp []BlockTradeMoveResponse
	return resp, e.SendWSRequest(ctx, nonMatchingEPL, movePositions, input, &resp, true)
}

// WsSimulateBlockTrade checks if a block trade can be executed through the websocket
func (e *Exchange) WsSimulateBlockTrade(ctx context.Context, role string, trades []BlockTradeParam) (bool, error) {
	if role != roleMaker && role != roleTaker {
		return false, errInvalidTradeRole
	}
	if len(trades) == 0 {
		return false, errNoArgumentPassed
	}
	for x := range trades {
		if trades[x].InstrumentName == "" {
			return false, fmt.Errorf("%w, empty string", errInvalidInstrumentName)
		}
		trades[x].Direction = strings.ToLower(trades[x].Direction)
		if trades[x].Direction != sideBUY && trades[x].Direction != sideSELL {
			return false, errInvalidOrderSideOrDirection
		}
		if trades[x].Amount <= 0 {
			return false, errInvalidAmount
		}
		if trades[x].Price < 0 {
			return false, fmt.Errorf("%w, trade price can't be negative", errInvalidPrice)
		}
	}
	input := &struct {
		Role   string            `json:"role"`
		Trades []BlockTradeParam `json:"trades"`
	}{
		Role:   role,
		Trades: trades,
	}
	var resp bool
	return resp, e.SendWSRequest(ctx, matchingEPL, simulateBlockPosition, input, resp, true)
}

// SendWSRequest sends a request through the websocket connection.
// both authenticated and public endpoints are allowed.
func (e *Exchange) SendWSRequest(ctx context.Context, epl request.EndpointLimit, method string, params, response any, authenticated bool) error {
	if authenticated && !e.Websocket.CanUseAuthenticatedEndpoints() {
		return errWebsocketConnectionNotAuthenticated
	}
	input := &WsRequest{
		JSONRPCVersion: rpcVersion,
		ID:             e.MessageID(),
		Method:         method,
		Params:         params,
	}
	resp := &wsResponse{Result: response}
	err := e.sendWsPayload(ctx, epl, input, resp)
	if err != nil {
		return err
	}
	if resp.Error.Code != 0 || resp.Error.Message != "" {
		var data string
		if resp.Error.Data != nil {
			value, err := json.Marshal(resp.Error.Data)
			if err == nil {
				data = string(value)
			}
		}
		return fmt.Errorf("code: %d message: %s %s", resp.Error.Code, resp.Error.Message, data)
	}
	return nil
}

// sendWsPayload handles sending Websocket requests
// TODO: Refactor to use rate limiting system
func (e *Exchange) sendWsPayload(ctx context.Context, ep request.EndpointLimit, input *WsRequest, response *wsResponse) error {
	if input == nil {
		return fmt.Errorf("%w, input can not be ", common.ErrNilPointer)
	}
	deadline := time.Now().Add(websocketRequestTimeout)
	ctx, cancelFunc := context.WithDeadline(ctx, deadline)
	defer func() {
		if time.Now().After(deadline) {
			cancelFunc()
		}
	}()
	for attempt := 1; ; attempt++ {
		// Initiate a rate limit reservation and sleep on requested endpoint
		err := e.Requester.InitiateRateLimit(ctx, ep)
		if err != nil {
			return fmt.Errorf("failed to rate limit Websocket request: %w", err)
		}

		if e.Verbose {
			log.Debugf(log.RequestSys, "%s attempt %d", e.Name, attempt)
		}
		var payload []byte
		payload, err = e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, input.ID, input)
		if err != nil {
			return err
		}
		err = json.Unmarshal(payload, response)
		if err != nil {
			return err
		}
		switch response.Error.Code {
		case 10040:
			after := 100 * time.Millisecond // because all the request rate will be reset after 1 sec interval
			backoff := request.DefaultBackoff()(attempt)
			delay := max(after, backoff)

			if dl, ok := ctx.Deadline(); ok && dl.Before(time.Now().Add(delay)) {
				return errors.New("deadline would be exceeded by retry")
			}

			if e.Verbose {
				log.Errorf(log.RequestSys,
					"%s request has failed. Retrying request in %s, attempt %d",
					e.Name,
					delay,
					attempt)
			}
			time.Sleep(delay)
			continue
		default:
			return nil
		}
	}
}
