diff --git a/bun.lock b/bun.lock
index 2c7db02..2edeecb 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,8 +5,9 @@
"name": "ritmex-bot",
"dependencies": {
"@grvt/client": "^1.6.4",
+ "@noble/ed25519": "^3.0.0",
"axios": "^1.12.2",
- "ccxt": "^4.5.5",
+ "ccxt": "^4.5.12",
"dotenv": "^17.2.2",
"ethereum-cryptography": "^2.1.3",
"ink": "^6.3.1",
@@ -83,6 +84,8 @@
"@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="],
+ "@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="],
+
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="],
@@ -185,7 +188,7 @@
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
- "ccxt": ["ccxt@4.5.5", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-AyhwTFLkx4sO985ImIOfumEBox7AHD/iqk5tPGICObUSZG6wTXg0aRzU8Hjz974aCMG4msFwLk3A/iXPKAU4wA=="],
+ "ccxt": ["ccxt@4.5.12", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-2lfL2TKjq4vBkQUQWJfDqFywhvYCZmk9r0SWC8GqA4AHZ6qozKVUJowxQTvdRsLX9jBwYSE0nc7JVurBrQ6SHg=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
diff --git a/docs/backpack/ccxt.md b/docs/backpack/ccxt.md
new file mode 100644
index 0000000..39424b5
--- /dev/null
+++ b/docs/backpack/ccxt.md
@@ -0,0 +1,1152 @@
+
+
+
+## backpack{docsify-ignore}
+**Kind**: global class
+**Extends**: Exchange
+
+* [fetchCurrencies](#fetchcurrencies)
+* [fetchMarkets](#fetchmarkets)
+* [fetchTickers](#fetchtickers)
+* [fetchTicker](#fetchticker)
+* [fetchOrderBook](#fetchorderbook)
+* [fetchOHLCV](#fetchohlcv)
+* [fetchFundingRate](#fetchfundingrate)
+* [fetchOpenInterest](#fetchopeninterest)
+* [fetchFundingRateHistory](#fetchfundingratehistory)
+* [fetchTrades](#fetchtrades)
+* [fetchMyTrades](#fetchmytrades)
+* [fetchStatus](#fetchstatus)
+* [fetchTime](#fetchtime)
+* [fetchBalance](#fetchbalance)
+* [fetchDeposits](#fetchdeposits)
+* [fetchWithdrawals](#fetchwithdrawals)
+* [withdraw](#withdraw)
+* [fetchDepositAddress](#fetchdepositaddress)
+* [createOrder](#createorder)
+* [createOrders](#createorders)
+* [fetchOpenOrders](#fetchopenorders)
+* [fetchOpenOrder](#fetchopenorder)
+* [cancelOrder](#cancelorder)
+* [cancelAllOrders](#cancelallorders)
+* [fetchOrders](#fetchorders)
+* [fetchPositions](#fetchpositions)
+* [fetchFundingHistory](#fetchfundinghistory)
+* [watchTicker](#watchticker)
+* [unWatchTicker](#unwatchticker)
+* [watchTickers](#watchtickers)
+* [unWatchTickers](#unwatchtickers)
+* [watchBidsAsks](#watchbidsasks)
+* [unWatchBidsAsks](#unwatchbidsasks)
+* [watchOHLCV](#watchohlcv)
+* [unWatchOHLCV](#unwatchohlcv)
+* [watchOHLCVForSymbols](#watchohlcvforsymbols)
+* [unWatchOHLCVForSymbols](#unwatchohlcvforsymbols)
+* [watchTrades](#watchtrades)
+* [unWatchTrades](#unwatchtrades)
+* [watchTradesForSymbols](#watchtradesforsymbols)
+* [unWatchTradesForSymbols](#unwatchtradesforsymbols)
+* [watchOrderBook](#watchorderbook)
+* [watchOrderBookForSymbols](#watchorderbookforsymbols)
+* [unWatchOrderBook](#unwatchorderbook)
+* [unWatchOrderBookForSymbols](#unwatchorderbookforsymbols)
+* [watchOrders](#watchorders)
+* [unWatchOrders](#unwatchorders)
+* [watchPositions](#watchpositions)
+* [unWatchPositions](#unwatchpositions)
+
+
+
+### fetchCurrencies{docsify-ignore}
+fetches all available currencies on an exchange
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - an associative dictionary of currencies
+
+**See**: https://docs.backpack.exchange/#tag/Assets
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchCurrencies ([params])
+```
+
+
+
+
+### fetchMarkets{docsify-ignore}
+retrieves data on all markets for bitbank
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - an array of objects representing market data
+
+**See**: https://docs.backpack.exchange/#tag/Markets/operation/get_markets
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchMarkets ([params])
+```
+
+
+
+
+### fetchTickers{docsify-ignore}
+fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a dictionary of [ticker structures](https://docs.ccxt.com/#/?id=ticker-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Markets/operation/get_tickers
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string>, undefined | Yes | unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchTickers (symbols[, params])
+```
+
+
+
+
+### fetchTicker{docsify-ignore}
+fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Markets/operation/get_ticker
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch the ticker for |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchTicker (symbol[, params])
+```
+
+
+
+
+### fetchOrderBook{docsify-ignore}
+fetches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - A dictionary of [order book structures](https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure) indexed by market symbols
+
+**See**: https://docs.backpack.exchange/#tag/Markets/operation/get_depth
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch the order book for |
+| limit | int | No | the maximum amount of order book entries to return (default 100, max 200) |
+| params | object | No | extra parameters specific to the bitteam api endpoint |
+
+
+```javascript
+backpack.fetchOrderBook (symbol[, limit, params])
+```
+
+
+
+
+### fetchOHLCV{docsify-ignore}
+fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Array<int>> - A list of candles ordered as timestamp, open, high, low, close, volume
+
+**See**: https://docs.backpack.exchange/#tag/Markets/operation/get_klines
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch OHLCV data for |
+| timeframe | string | Yes | the length of time each candle represents |
+| since | int | No | timestamp in seconds of the earliest candle to fetch |
+| limit | int | No | the maximum amount of candles to fetch (default 100) |
+| params | object | No | extra parameters specific to the bitteam api endpoint |
+
+
+```javascript
+backpack.fetchOHLCV (symbol, timeframe[, since, limit, params])
+```
+
+
+
+
+### fetchFundingRate{docsify-ignore}
+fetch the current funding rate
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [funding rate structure](https://docs.ccxt.com/#/?id=funding-rate-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Markets/operation/get_mark_prices
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified market symbol |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchFundingRate (symbol[, params])
+```
+
+
+
+
+### fetchOpenInterest{docsify-ignore}
+Retrieves the open interest of a derivative trading pair
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - an open interest structure[https://docs.ccxt.com/#/?id=interest-history-structure](https://docs.ccxt.com/#/?id=interest-history-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Markets/operation/get_open_interest
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | Unified CCXT market symbol |
+| params | object | No | exchange specific parameters |
+
+
+```javascript
+backpack.fetchOpenInterest (symbol[, params])
+```
+
+
+
+
+### fetchFundingRateHistory{docsify-ignore}
+fetches historical funding rate prices
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [funding rate structures](https://docs.ccxt.com/#/?id=funding-rate-history-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Markets/operation/get_funding_interval_rates
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch the funding rate history for |
+| since | int | No | timestamp in ms of the earliest funding rate to fetch |
+| limit | int | No | the maximum amount of funding rate structures |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchFundingRateHistory (symbol[, since, limit, params])
+```
+
+
+
+
+### fetchTrades{docsify-ignore}
+get the list of most recent trades for a particular symbol
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Trade> - a list of [trade structures](https://docs.ccxt.com/#/?id=public-trades)
+
+**See**
+
+- https://docs.backpack.exchange/#tag/Trades/operation/get_recent_trades
+- https://docs.backpack.exchange/#tag/Trades/operation/get_historical_trades
+
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch trades for |
+| since | int | No | timestamp in ms of the earliest trade to fetch |
+| limit | int | No | the maximum amount of trades to fetch |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.offset | int | No | the number of trades to skip, default is 0 |
+
+
+```javascript
+backpack.fetchTrades (symbol[, since, limit, params])
+```
+
+
+
+
+### fetchMyTrades{docsify-ignore}
+fetch all trades made by the user
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Trade> - a list of [trade structures](https://docs.ccxt.com/#/?id=trade-structure)
+
+**See**: https://docs.backpack.exchange/#tag/History/operation/get_fills
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified market symbol |
+| since | int | No | the earliest time in ms to fetch trades for |
+| limit | int | No | the maximum number of trades structures to retrieve (default 100, max 1000) |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.until | int | No | the latest time in ms to fetch trades for |
+| params.fillType | string | No | 'User' (default) 'BookLiquidation' or 'Adl' or 'Backstop' or 'Liquidation' or 'AllLiquidation' or 'CollateralConversion' or 'CollateralConversionAndSpotLiquidation' |
+
+
+```javascript
+backpack.fetchMyTrades (symbol[, since, limit, params])
+```
+
+
+
+
+### fetchStatus{docsify-ignore}
+the latest known information on the availability of the exchange API
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [status structure](https://docs.ccxt.com/#/?id=exchange-status-structure)
+
+**See**: https://docs.backpack.exchange/#tag/System/operation/get_status
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchStatus ([params])
+```
+
+
+
+
+### fetchTime{docsify-ignore}
+fetches the current integer timestamp in milliseconds from the exchange server
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: int - the current integer timestamp in milliseconds from the exchange server
+
+**See**: https://developer-pro.bitmart.com/en/spot/#get-system-time
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchTime ([params])
+```
+
+
+
+
+### fetchBalance{docsify-ignore}
+query for balance and get the amount of funds available for trading or funds locked in orders
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [balance structure](https://docs.ccxt.com/#/?id=balance-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Capital/operation/get_balances
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchBalance ([params])
+```
+
+
+
+
+### fetchDeposits{docsify-ignore}
+fetch all deposits made to an account
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [transaction structures](https://docs.ccxt.com/#/?id=transaction-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Capital/operation/get_deposits
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| code | string | Yes | unified currency code |
+| since | int | No | the earliest time in ms to fetch deposits for |
+| limit | int | No | the maximum number of deposits structures to retrieve |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.until | int | No | the latest time in ms to fetch entries for |
+
+
+```javascript
+backpack.fetchDeposits (code[, since, limit, params])
+```
+
+
+
+
+### fetchWithdrawals{docsify-ignore}
+fetch all withdrawals made from an account
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [transaction structures](https://docs.ccxt.com/#/?id=transaction-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Capital/operation/get_withdrawals
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| code | string | Yes | unified currency code of the currency transferred |
+| since | int | No | the earliest time in ms to fetch transfers for (default 24 hours ago) |
+| limit | int | No | the maximum number of transfer structures to retrieve (default 50, max 200) |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.until | int | No | the latest time in ms to fetch transfers for (default time now) |
+
+
+```javascript
+backpack.fetchWithdrawals (code[, since, limit, params])
+```
+
+
+
+
+### withdraw{docsify-ignore}
+make a withdrawal
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [transaction structure](https://docs.ccxt.com/#/?id=transaction-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Capital/operation/request_withdrawal
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| code | string | Yes | unified currency code |
+| amount | float | Yes | the amount to withdraw |
+| address | string | Yes | the address to withdraw to |
+| tag | string | Yes | |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.network | string | No | the network to withdraw on (mandatory) |
+
+
+```javascript
+backpack.withdraw (code, amount, address, tag[, params])
+```
+
+
+
+
+### fetchDepositAddress{docsify-ignore}
+fetch the deposit address for a currency associated with this account
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - an [address structure](https://docs.ccxt.com/#/?id=address-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Capital/operation/get_deposit_address
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| code | string | Yes | unified currency code |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.networkCode | string | No | the network to fetch the deposit address (mandatory) |
+
+
+```javascript
+backpack.fetchDepositAddress (code[, params])
+```
+
+
+
+
+### createOrder{docsify-ignore}
+create a trade order
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - an [order structure](https://docs.ccxt.com/#/?id=order-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Order/operation/execute_order
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to create an order in |
+| type | string | Yes | 'market' or 'limit' |
+| side | string | Yes | 'buy' or 'sell' |
+| amount | float | Yes | how much of currency you want to trade in units of base currency |
+| price | float | No | the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.cost | float | No | *market orders only* the cost of the order in units of the quote currency (could be used instead of amount) |
+| params.clientOrderId | int | No | a unique id for the order |
+| params.postOnly | boolean | No | true to place a post only order |
+| params.timeInForce | string | No | 'GTC', 'IOC', 'FOK' or 'PO' |
+| params.reduceOnly | bool | No | *contract only* Indicates if this order is to reduce the size of a position |
+| params.selfTradePrevention | string | No | one of EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH |
+| params.autoLend | bool | No | *spot margin only* if true then the order can lend |
+| params.autoLendRedeem | bool | No | *spot margin only* if true then the order can redeem a lend if required |
+| params.autoBorrow | bool | No | *spot margin only* if true then the order can borrow |
+| params.autoBorrowRepay | bool | No | *spot margin only* if true then the order can repay a borrow |
+| params.triggerPrice | float | No | the price that a trigger order is triggered at |
+| params.takeProfit | object | No | *swap markets only - takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered |
+| params.takeProfit.triggerPrice | float | No | take profit trigger price |
+| params.takeProfit.price | float | No | take profit order price (if not provided the order will be a market order) |
+| params.stopLoss | object | No | *swap markets only - stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered |
+| params.stopLoss.triggerPrice | float | No | stop loss trigger price |
+| params.stopLoss.price | float | No | stop loss order price (if not provided the order will be a market order) |
+
+
+```javascript
+backpack.createOrder (symbol, type, side, amount[, price, params])
+```
+
+
+
+
+### createOrders{docsify-ignore}
+create a list of trade orders
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - an [order structure](https://docs.ccxt.com/#/?id=order-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Order/operation/execute_order_batch
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| orders | Array | Yes | list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.createOrders (orders[, params])
+```
+
+
+
+
+### fetchOpenOrders{docsify-ignore}
+fetch all unfilled currently open orders
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Order> - a list of [order structures](https://docs.ccxt.com/#/?id=order-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Order/operation/get_open_orders
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified market symbol |
+| since | int | No | the earliest time in ms to fetch open orders for |
+| limit | int | No | the maximum number of open orders structures to retrieve |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchOpenOrders (symbol[, since, limit, params])
+```
+
+
+
+
+### fetchOpenOrder{docsify-ignore}
+fetch an open order by it's id
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - an [order structure](https://docs.ccxt.com/#/?id=order-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Order/operation/get_order
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| id | string | Yes | order id |
+| symbol | string | Yes | not used by hollaex fetchOpenOrder () |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchOpenOrder (id, symbol[, params])
+```
+
+
+
+
+### cancelOrder{docsify-ignore}
+cancels an open order
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - An [order structure](https://docs.ccxt.com/#/?id=order-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Order/operation/cancel_order
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| id | string | Yes | order id |
+| symbol | string | Yes | unified symbol of the market the order was made in |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.cancelOrder (id, symbol[, params])
+```
+
+
+
+
+### cancelAllOrders{docsify-ignore}
+cancel all open orders
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [order structures](https://docs.ccxt.com/#/?id=order-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Order/operation/cancel_open_orders
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified market symbol, only orders in the market of this symbol are cancelled when symbol is not undefined |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.cancelAllOrders (symbol[, params])
+```
+
+
+
+
+### fetchOrders{docsify-ignore}
+fetches information on multiple orders made by the user
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Order> - a list of [order structures](https://github.com/ccxt/ccxt/wiki/Manual#order-structure)
+
+**See**: https://docs.backpack.exchange/#tag/History/operation/get_order_history
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified market symbol of the market orders were made in |
+| since | int | No | the earliest time in ms to fetch orders for |
+| limit | int | No | the maximum number of orde structures to retrieve (default 100, max 1000) |
+| params | object | No | extra parameters specific to the bitteam api endpoint |
+
+
+```javascript
+backpack.fetchOrders (symbol[, since, limit, params])
+```
+
+
+
+
+### fetchPositions{docsify-ignore}
+fetch all open positions
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [position structure](https://docs.ccxt.com/#/?id=position-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Futures/operation/get_positions
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string>, undefined | Yes | list of unified market symbols |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.fetchPositions (symbols[, params])
+```
+
+
+
+
+### fetchFundingHistory{docsify-ignore}
+fetches the history of funding payments
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Trade> - a list of [trade structures](https://docs.ccxt.com/#/?id=public-trades)
+
+**See**: https://docs.backpack.exchange/#tag/History/operation/get_funding_payments
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch trades for |
+| since | int | No | timestamp in ms of the earliest trade to fetch (default 24 hours ago) |
+| limit | int | No | the maximum amount of trades to fetch (default 200, max 500) |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.until | int | No | timestamp in ms of the latest trade to fetch (default now) |
+
+
+```javascript
+backpack.fetchFundingHistory (symbol[, since, limit, params])
+```
+
+
+
+
+### watchTicker{docsify-ignore}
+watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Ticker
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch the ticker for |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchTicker (symbol[, params])
+```
+
+
+
+
+### unWatchTicker{docsify-ignore}
+unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Ticker
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch the ticker for |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchTicker (symbol[, params])
+```
+
+
+
+
+### watchTickers{docsify-ignore}
+watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Ticker
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | Yes | unified symbol of the market to fetch the ticker for |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchTickers (symbols[, params])
+```
+
+
+
+
+### unWatchTickers{docsify-ignore}
+watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Ticker
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | Yes | unified symbol of the market to fetch the ticker for |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchTickers (symbols[, params])
+```
+
+
+
+
+### watchBidsAsks{docsify-ignore}
+watches best bid & ask for symbols
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Book-ticker
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | Yes | unified symbol of the market to fetch the ticker for |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchBidsAsks (symbols[, params])
+```
+
+
+
+
+### unWatchBidsAsks{docsify-ignore}
+unWatches best bid & ask for symbols
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
+
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | Yes | unified symbol of the market to fetch the ticker for |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchBidsAsks (symbols[, params])
+```
+
+
+
+
+### watchOHLCV{docsify-ignore}
+watches historical candlestick data containing the open, high, low, close price, and the volume of a market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Array<int>> - A list of candles ordered as timestamp, open, high, low, close, volume
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/K-Line
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch OHLCV data for |
+| timeframe | string | Yes | the length of time each candle represents |
+| since | int | No | timestamp in ms of the earliest candle to fetch |
+| limit | int | No | the maximum amount of candles to fetch |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchOHLCV (symbol, timeframe[, since, limit, params])
+```
+
+
+
+
+### unWatchOHLCV{docsify-ignore}
+watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Array<int>> - A list of candles ordered as timestamp, open, high, low, close, volume
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/K-Line
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch OHLCV data for |
+| timeframe | string | Yes | the length of time each candle represents |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchOHLCV (symbol, timeframe[, params])
+```
+
+
+
+
+### watchOHLCVForSymbols{docsify-ignore}
+watches historical candlestick data containing the open, high, low, close price, and the volume of a market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Array<int>> - A list of candles ordered as timestamp, open, high, low, close, volume
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/K-Line
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbolsAndTimeframes | Array<Array<string>> | Yes | array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] |
+| since | int | No | timestamp in ms of the earliest candle to fetch |
+| limit | int | No | the maximum amount of candles to fetch |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchOHLCVForSymbols (symbolsAndTimeframes[, since, limit, params])
+```
+
+
+
+
+### unWatchOHLCVForSymbols{docsify-ignore}
+unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<Array<int>> - A list of candles ordered as timestamp, open, high, low, close, volume
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/K-Line
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbolsAndTimeframes | Array<Array<string>> | Yes | array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchOHLCVForSymbols (symbolsAndTimeframes[, params])
+```
+
+
+
+
+### watchTrades{docsify-ignore}
+watches information on multiple trades made in a market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Trade
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch the ticker for |
+| since | int | No | the earliest time in ms to fetch trades for |
+| limit | int | No | the maximum number of trade structures to retrieve |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchTrades (symbol[, since, limit, params])
+```
+
+
+
+
+### unWatchTrades{docsify-ignore}
+unWatches from the stream channel
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [trade structures](https://docs.ccxt.com/#/?id=public-trades)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Trade
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch trades for |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchTrades (symbol[, params])
+```
+
+
+
+
+### watchTradesForSymbols{docsify-ignore}
+watches information on multiple trades made in a market
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - a [ticker structure](https://docs.ccxt.com/#/?id=ticker-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Trade
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | Yes | unified symbol of the market to fetch trades for |
+| since | int | No | the earliest time in ms to fetch trades for |
+| limit | int | No | the maximum number of trade structures to retrieve |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchTradesForSymbols (symbols[, since, limit, params])
+```
+
+
+
+
+### unWatchTradesForSymbols{docsify-ignore}
+unWatches from the stream channel
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [trade structures](https://docs.ccxt.com/#/?id=public-trades)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Trade
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | Yes | unified symbol of the market to fetch trades for |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchTradesForSymbols (symbols[, params])
+```
+
+
+
+
+### watchOrderBook{docsify-ignore}
+watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - A dictionary of [order book structures](https://docs.ccxt.com/#/?id=order-book-structure) indexed by market symbols
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Depth
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified symbol of the market to fetch the order book for |
+| limit | int | No | the maximum amount of order book entries to return |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchOrderBook (symbol[, limit, params])
+```
+
+
+
+
+### watchOrderBookForSymbols{docsify-ignore}
+watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - A dictionary of [order book structures](https://docs.ccxt.com/#/?id=order-book-structure) indexed by market symbols
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Public/Depth
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | Yes | unified array of symbols |
+| limit | int | No | the maximum amount of order book entries to return |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.method | string | No | either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2' |
+
+
+```javascript
+backpack.watchOrderBookForSymbols (symbols[, limit, params])
+```
+
+
+
+
+### unWatchOrderBook{docsify-ignore}
+unWatches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - A dictionary of [order book structures](https://docs.ccxt.com/#/?id=order-book-structure) indexed by market symbols
+
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | Yes | unified array of symbols |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchOrderBook (symbol[, params])
+```
+
+
+
+
+### unWatchOrderBookForSymbols{docsify-ignore}
+unWatches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: object - A dictionary of [order book structures](https://docs.ccxt.com/#/?id=order-book-structure) indexed by market symbols
+
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | Yes | unified array of symbols |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+| params.method | string | No | either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2' |
+
+
+```javascript
+backpack.unWatchOrderBookForSymbols (symbols[, params])
+```
+
+
+
+
+### watchOrders{docsify-ignore}
+watches information on multiple orders made by the user
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [order structures](https://docs.ccxt.com/#/?id=order-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Private/Order-update
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | No | unified market symbol of the market orders were made in |
+| since | int | No | the earliest time in ms to fetch orders for |
+| limit | int | No | the maximum number of order structures to retrieve |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchOrders ([symbol, since, limit, params])
+```
+
+
+
+
+### unWatchOrders{docsify-ignore}
+unWatches information on multiple orders made by the user
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [order structures](https://docs.ccxt.com/#/?id=order-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Private/Order-update
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbol | string | No | unified market symbol of the market orders were made in |
+| params | object | No | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchOrders ([symbol, params])
+```
+
+
+
+
+### watchPositions{docsify-ignore}
+watch all open positions
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [position structure](https://docs.ccxt.com/en/latest/manual.html#position-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Private/Position-update
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | No | list of unified market symbols to watch positions for |
+| since | int | No | the earliest time in ms to fetch positions for |
+| limit | int | No | the maximum number of positions to retrieve |
+| params | object | Yes | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.watchPositions ([symbols, since, limit, params])
+```
+
+
+
+
+### unWatchPositions{docsify-ignore}
+unWatches from the stream channel
+
+**Kind**: instance method of [backpack](#backpack)
+**Returns**: Array<object> - a list of [position structure](https://docs.ccxt.com/en/latest/manual.html#position-structure)
+
+**See**: https://docs.backpack.exchange/#tag/Streams/Private/Position-update
+
+| Param | Type | Required | Description |
+| --- | --- | --- | --- |
+| symbols | Array<string> | No | list of unified market symbols to watch positions for |
+| params | object | Yes | extra parameters specific to the exchange API endpoint |
+
+
+```javascript
+backpack.unWatchPositions ([symbols, params])
+```
diff --git a/docs/backpack/openapi.json b/docs/backpack/openapi.json
index d1b2d9e..f5e8356 100644
--- a/docs/backpack/openapi.json
+++ b/docs/backpack/openapi.json
@@ -68,7 +68,7 @@
},
{
"name": "Streams",
- "description": "# Usage\n\n## Subscribing\n\nTo use the websocket API, connect to\n`wss://ws.backpack.exchange`.\n\nTo subscribe to a stream with the name `stream` send a text frame\nover the websocket connection with the following JSON payload:\n\n```\n{\n \"method\": \"SUBSCRIBE\",\n \"params\": [\"stream\"]\n}\n```\n\nSimilarly, to unsubscribe from a stream with the name `stream`:\n\n```\n{\n \"method\": \"UNSUBSCRIBE\",\n \"params\": [\"stream\"]\n}\n```\n\nYou can subscribe or unsubscribe from multiple streams if you include\nmore than one in the params field.\n\nAll data from streams is wrapped in a JSON object of the following form:\n\n```\n{\n \"stream\": \"\",\n \"data\": \"\"\n}\n```\n\nThe following command can be used to test subscribing to a stream:\n```\n(sleep 1; \\\necho '{\"method\":\"SUBSCRIBE\",\"params\":[\"depth.SOL_USDC\"]}';\\\ncat) |\\\nwscat -c wss://ws.backpack.exchange\n```\nThe payloads for each stream time are outlined below.\n\n## Timing\n\nTimestamps are in microseconds (except for the K-line start and end\ntimes). The event timestamp is the time the event was emitted from\nthe websocket server, and the engine timestamp is the time the event\nwas generated by the matching engine.\n\nIf a message aggregates more than one event (for example, a depth\nmessage), the engine timestamp will be the timestamp of the last\nmatching engine event.\n\n## Keeping the connection alive\n\nTo keep the connection alive, a `Ping` frame will be sent from the\nserver every 60s, and a `Pong` is expected to be received from the\nclient. If a `Pong` is not received within 120s, a `Close` frame will be\nsent and the connection will be closed.\n\nIf the server is shutting down, a `Close` frame will be sent and then a\ngrace period of 30s will be given before the connection is closed. The\nclient should reconnect after receiving the `Close` frame. The client\nwill be reconnected to a server that is not shutting down.\n\n# Private\n\nSubscribing to a private stream requires a valid signature generated\nfrom an ED25519 keypair. For stream subscriptions, the signature\nshould be of the form:\n\n```text\ninstruction=subscribe×tamp=1614550000000&window=5000\n```\n\nWhere the timestamp and window are in milliseconds.\n\nPrivate streams are prefixed with `account.` and require signature data\nto be submitted in the subscribe parameters. The verifying key and\nsignature should be base64 encoded.\n\n```\n{\n \"method\": \"SUBSCRIBE\",\n \"params\": [\"stream\"],\n \"signature\": [\"\", \"\", \"\", \"\"]\n}\n````\n\n## Order update\n\nOn any mutation to an order the order will be pushed to the order update\nstream. The event type of the order update will be one of the\nfollowing:\n\n- `orderAccepted`\n- `orderCancelled`\n- `orderExpired`\n- `orderFill`\n- `orderModified`\n- `triggerPlaced`\n- `triggerFailed`\n\nAn `orderModified` update will be received when a resting reduce only\norder's quantity is decreased in order to prevent position side\nreversal.\n\n### Stream Name Format\n- For all markets: `account.orderUpdate`\n- For single market: `account.orderUpdate.`\n\n```\n{\n \"e\": \"orderAccepted\", // Event type\n \"E\": 1694687692980000, // Event time in microseconds\n \"s\": \"SOL_USD\", // Symbol\n \"c\": 123, // Client order ID\n \"S\": \"Bid\", // Side\n \"o\": \"LIMIT\", // Order type\n \"f\": \"GTC\", // Time in force\n \"q\": \"32123\", // Quantity\n \"Q\": \"32123\", // Quantity in quote\n \"p\": \"20\", // Price\n \"P\": \"21\", // Trigger price\n \"B\": \"LastPrice\", // Trigger by\n \"a\": \"30\", // Take profit trigger price\n \"b\": \"10\", // Stop loss trigger price\n \"d\": \"MarkPrice\", // Take profit trigger by\n \"g\": \"IndexPrice\", // Stop loss trigger by\n \"Y\": \"10\", // Trigger quantity\n \"X\": \"Filled\", // Order state\n \"R\": \"PRICE_BAND\", // Order expiry reason\n \"i\": \"1111343026172067\" // Order ID\n \"t\": 567, // Trade ID\n \"l\": \"1.23\", // Fill quantity\n \"z\": \"321\", // Executed quantity\n \"Z\": \"123\", // Executed quantity in quote\n \"L\": \"20\", // Fill price\n \"m\": true, // Whether the order was maker\n \"n\": \"23\", // Fee\n \"N\": \"USD\", // Fee symbol\n \"V\": \"RejectTaker\", // Self trade prevention\n \"T\": 1694687692989999, // Engine timestamp in microseconds\n \"O\": \"USER\" // Origin of the update\n \"I\": \"1111343026156135\" // Related order ID\n \"H\": 6023471188 // Strategy ID\n \"y\": true // Post only\n}\n```\n\nThere are several possible values for the `O` field (origin of the\nupdate):\n- `USER`: The origin of the update was due to order entry by the user.\n- `LIQUIDATION_AUTOCLOSE`: The origin of the update was due to a\nliquidation by the liquidation engine.\n- `ADL_AUTOCLOSE`: The origin of the update was due to an ADL\n(auto-deleveraging) event.\n- `COLLATERAL_CONVERSION`: The origin of the update was due to a\ncollateral conversion to settle debt on the account.\n- `SETTLEMENT_AUTOCLOSE`: The origin of the update was due to the\nsettlement of a position on a dated market.\n- `BACKSTOP_LIQUIDITY_PROVIDER`: The origin of the update was due to a\nbackstop liquidity provider facilitating a liquidation.\n\nSome fields are conditional on the order settings or event type:\n\n- `c` - Only present if the order has a client order ID.\n- `q` - Only present if the order has a quantity set.\n- `Q` - Only present if the order is reverse market order.\n- `p` - Only present if the order is a limit order.\n- `P` - Only present if the order is a trigger order.\n- `B` - Only present if the order is a trigger order.\n- `a` - Only present if the order has a take profit trigger price set.\n- `b` - Only present if the order has a stop loss trigger price set.\n- `d` - Only present if the order has a take profit trigger price set.\n- `g` - Only present if the order has a stop loss trigger price set.\n- `Y` - Only present if the order is a trigger order.\n- `R` - Only present if the event is a `orderExpired` event.\n- `t` - Only present if the event is a `orderFill` event.\n- `l` - Only present if the event is a `orderFill` event.\n- `L` - Only present if the event is a `orderFill` event.\n- `m` - Only present if the event is a `orderFill` event.\n- `n` - Only present if the event is a `orderFill` event.\n- `N` - Only present if the event is a `orderFill` event.\n\n## Position update\n\nOn any mutation to a position the position will be pushed to the\nposition update stream. The event type of the position update will\nbe one of the following:\n\n- `positionAdjusted`\n- `positionOpened`\n- `positionClosed`\n\nOn subscription, a message will be sent to the client with the current\nopen positions, if any. The `e` field will not be present in the\nmessage.\n\n### Stream Name Format\n- For all markets: `account.positionUpdate`\n- For single market: `account.positionUpdate.`\n\n```\n{\n \"e\": \"positionOpened\", // Event type\n \"E\": 1694687692980000, // Event time in microseconds\n \"s\": \"SOL_USDC_PERP\", // Symbol\n \"b\": 123, // Break event price\n \"B\": 122, // Entry price\n \"f\": 0.5, // Initial margin fraction\n \"M\": 122, // Mark price\n \"m\": 0.01, // Maintenance margin fraction\n \"q\": 5, // Net quantity\n \"Q\": 6, // Net exposure quantity\n \"n\": 732 , // Net exposure notional\n \"i\": \"1111343026172067\" // Position ID\n \"p\": \"-1\", // PnL realized\n \"P\": \"0\", // PnL unrealized\n \"T\": 1694687692989999 // Engine timestamp in microseconds\n}\n```\n\nThe net quantity field will be positive if the position is long and\nnegative if the position is short.\n\nThe net exposure quantity field includes exposure from the open\nposition, as well as any open orders.\n\n## RFQ Update\n\nThis WebSocket stream provides real-time updates on RFQs (Request for\nQuotes) that are relevant to makers. Events are pushed to this\nstream whenever there is a significant state change in an RFQ or its\nassociated quotes, allowing makers to monitor and respond to RFQs as\nthey progress through various states.\n\n### Event Types\n\nFor RFQs that submitted by other requesters.\n- `rfqActive`: Indicates that an RFQ is active and open for quotes.\n\nFor RFQs that submitted by your account.\n- `rfqAccepted`: Indicates that an RFQ has been accepted and is no\n- `rfqRefreshed`: Indicates that an RFQ has been refreshed, is active\nand open for quotes.\n- `rfqCancelled`: Indicates that an RFQ has been cancelled or expired.\n- `rfqCandidate`: RFQ has received a new best quote.\n- `rfqFilled`: Indicates that an RFQ has been fully filled with a quote.\n\nFor Quotes submitted by your account.\n- `quoteAccepted`: Indicates that a quote submitted by the maker has\nbeen accepted.\n- `quoteCancelled`: Indicates that a quote has been cancelled due to\nquote submission, RFQ being filled, refreshed, cancelled, or expired.\n\n### Quote Submission and RFQ Timing\n\nMakers should submit quotes before the **submission time** (`w` field)\nis reached, as indicated in each `rfqActive` event. An RFQ remains\nactive until the **expiration time** (`W` field). If no quote is\naccepted or the RFQ is not cancelled, makers may continue to submit\nquotes until expiration.\n\nRFQs can periodically request new quotes by issuing additional\n`rfqActive` events. Each new `rfqActive` event will have the same\nRFQ ID (`R` field) but updated values for **submission time** and\n**expiration time**, allowing makers to participate in extended or\nrenewed quoting periods for ongoing RFQs.\n\n### Stream Name Format\n- For all markets: `account.rfqUpdate`\n- For single market: `account.rfqUpdate.`\n\n### Example Messages\n\n**RFQ Accepted** (sent to requester)\n```\n{\n \"e\": \"rfqAccepted\", // Event type\n \"E\": 1730225420369829, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"q\": \"10\", // Quantity (if quantity in base asset)\n \"w\": 1730225480368, // Submission time in milliseconds\n \"W\": 1730225540368, // Expiry time in milliseconds\n \"X\": \"New\", // RFQ status\n \"T\": 1730225420368765 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Active** (broadcast to all rfq listeners)\n```\n{\n \"e\": \"rfqActive\", // Event type\n \"E\": 1730225420369829, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"q\": \"10\", // Quantity (optional) (if quantity in base asset)\n \"w\": 1730225480368, // Submission time in milliseconds\n \"W\": 1730225540368, // Expiry time in milliseconds\n \"X\": \"New\", // RFQ status\n \"T\": 1730225420368765 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Refreshed** (sent to requester)\n```\n{\n \"e\": \"rfqRefreshed\", // Event type\n \"E\": 1730225450369829, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"q\": \"10\", // Quantity (optional) (if quantity in base asset)\n \"w\": 1730225480368, // Submission time in milliseconds\n \"W\": 1730225540368, // Expiry time in milliseconds\n \"X\": \"New\", // RFQ status\n \"T\": 1730225450368765 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Cancelled** (sent to taker only)\n```\n{\n \"e\": \"rfqCancelled\", // Event type\n \"E\": 1730225460369829, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"Q\": \"150\", // Quote quantity (optional) (if quantity in quote asset)\n \"w\": 1730225480368, // Submission time in milliseconds\n \"W\": 1730225540368, // Expiry time in milliseconds\n \"X\": \"Cancelled\", // RFQ status\n \"T\": 1730225460368765 // Engine timestamp in microseconds\n}\n```\n\n**Quote Accepted** (sent to quoter)\n```\n{\n \"e\": \"quoteAccepted\", // Event type\n \"E\": 1730225434631394, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"u\": 113392054083780608, // Quote ID\n \"C\": \"123\", // Client Quote ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"X\": \"New\", // Quote status\n \"T\": 1730225434629778 // Engine timestamp in microseconds\n}\n```\n\n**Quote Cancelled** (sent to quoter)\n```\n{\n \"e\": \"quoteCancelled\", // Event type\n \"E\": 1730225583761963, // Event time in microseconds\n \"R\": 113392061354344448, // RFQ ID\n \"u\": 113392062870847488, // Quote ID\n \"C\": \"123\", // Client Quote ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"X\": \"Cancelled\", // Quote status\n \"T\": 1730225583753811 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Candidate** (sent to requester with quote details)\n```\n{\n \"e\": \"rfqCandidate\", // Event type\n \"E\": 1730225490648996, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"u\": 113392054083780608, // Quote ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"q\": \"10\", // RFQ quantity (in base asset)\n \"Q\": \"150\", // RFQ quote quantity (in quote asset)\n \"p\": \"15.50\", // Taker price (quote price + fee)\n \"X\": \"New\", // RFQ status\n \"T\": 1730225490647080 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Filled** (sent to both requester and quoter)\n```\n// To requester\n{\n \"e\": \"rfqFilled\", // Event type\n \"E\": 1730225497648996, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"u\": 113392054083780608, // Quote ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"Q\": \"150\", // RFQ quote quantity (optional) (if quantity in quote asset)\n \"p\": \"15.50\", // Taker price (quote price + fee)\n \"X\": \"Filled\", // RFQ status\n \"T\": 1730225497647080 // Engine timestamp in microseconds\n}\n\n// To quoter\n{\n \"e\": \"rfqFilled\", // Event type\n \"E\": 1730225497648996, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"u\": 113392054083780608, // Quote ID\n \"C\": \"123\", // Client Quote ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"p\": \"15.00\", // Price\n \"X\": \"Filled\", // Quote status\n \"T\": 1730225497647080 // Engine timestamp in microseconds\n}\n```\n\n### Field Descriptions\n\n- `e` - Event type (e.g., `rfqActive`, `rfqAccepted`, `rfqRefreshed`,\n`rfqCancelled`, `quoteAccepted`, `quoteCancelled`, `rfqCandidate`,\n`rfqFilled`).\n- `E` - Event time in microseconds.\n- `R` - RFQ ID, identifying the request for quote.\n- `u` - Quote ID, identifying the specific quote.\n- `C` - Client ID (either Client RFQ ID or Client Quote ID depending on\ncontext).\n- `s` - Symbol the RFQ is for.\n- `S` - Side of the RFQ, either \"Bid\" or \"Ask\".\n- `q` - Quantity for the RFQ (in base asset, if quantity in base asset).\n- `Q` - Quote quantity for the RFQ (in quote asset, if quantity in quote\nasset).\n- `p` - Price associated with the quote/fill event.\n- `w` - Submission time for the RFQ in milliseconds.\n- `W` - Expiry time for the RFQ in milliseconds.\n- `X` - Order status (e.g., `New`, `Cancelled`, `Filled`).\n- `T` - Engine timestamp in microseconds.\n\nSome fields are conditional and may be present only in specific events.\n\n# Public\n\n## Book ticker\n\nStream name format: `bookTicker.`\n```\n{\n \"e\": \"bookTicker\", // Event type\n \"E\": 1694687965941000, // Event time in microseconds\n \"s\": \"SOL_USDC\", // Symbol\n \"a\": \"18.70\", // Inside ask price\n \"A\": \"1.000\", // Inside ask quantity\n \"b\": \"18.67\", // Inside bid price\n \"B\": \"2.000\", // Inside bid quantity\n \"u\": \"111063070525358080\", // Update ID of event\n \"T\": 1694687965940999 // Engine timestamp in microseconds\n}\n```\n\n## Depth\n\nContains incremental depth updates. Each depth update has the absolute\nvalue of the depths at the given levels, and only changes when the\ndepth has changed.\n\nTo obtain an initial snapshot of the depth, the client should query the\n[REST API](https://docs.backpack.exchange/#tag/Markets/operation/get_depth).\n\nThe depth stream will push updates as quickly as possible, but under\nload it may aggregate more than one update into a single event. In\nthis case the `U` and `u` fields will not be the same. The `U` field\nis the first update ID in the event, and the `u` field is the final\nupdate ID in the event.\n\nThere are alternative depth streams that aggregates updates into a\nsingle message over a 200ms, 600ms or 1000ms period instead of pushing\nupdates in realtime. This is useful for reducing network traffic.\n\nUpdates are sequential, so `U` will always be `u + 1` from the previous\nmessage. If this is not the case, the client should assume that the\ndepth has been invalidated and requery the REST API.\n\nStream name format: `depth.` (realtime)\nStream name format: `depth.200ms.` (aggregated)\nStream name format: `depth.600ms.` (aggregated)\nStream name format: `depth.1000ms.` (aggregated)\n```\n{\n \"e\": \"depth\", // Event type\n \"E\": 1694687965941000, // Event time in microseconds\n \"s\": \"SOL_USDC\", // Symbol\n \"a\": [ // Asks\n [\n \"18.70\",\n \"0.000\"\n ]\n ],\n \"b\": [ // Bids\n [\n \"18.67\",\n \"0.832\"\n ],\n [\n \"18.68\",\n \"0.000\"\n ]\n ],\n \"U\": 94978271, // First update ID in event\n \"u\": 94978271, // Last update ID in event\n \"T\": 1694687965940999 // Engine timestamp in microseconds\n}\n```\n\n## K-Line\n\nStream name format: `kline..`\n\n```\n{\n \"e\": \"kline\", // Event type\n \"E\": 1694687692980000, // Event time in microseconds\n \"s\": \"SOL_USD\", // Symbol\n \"t\": 123400000, // K-Line start time in seconds\n \"T\": 123460000, // K-Line close time in seconds\n \"o\": \"18.75\", // Open price\n \"c\": \"19.25\", // Close price\n \"h\": \"19.80\", // High price\n \"l\": \"18.50\", // Low price\n \"v\": \"32123\", // Base asset volume\n \"n\": 93828, // Number of trades\n \"X\": false // Is this k-line closed?\n}\n```\n\n## Liquidation\n\nContains updates for liquidation events for all liquidation types.\n\nStream name format: `liquidation`\n\n```\n{\n \"e\": \"liquidation\", // Event type\n \"E\": 1694688638091000, // Event time in microseconds\n \"q\": \"10\", // Quantity\n \"p\": \"18.70\", // Price\n \"S\": \"Bid\", // Side\n \"s\": \"SOL_USDC\", // Symbol\n \"T\": 567, // Engine timestamp in microseconds\n}\n```\n\n## Mark price\n\nStream name format: `markPrice.`\n\n```\n{\n \"e\": \"markPrice\", // Event type\n \"E\": 1694687965941000, // Event time in microseconds\n \"s\": \"SOL_USDC\", // Symbol\n \"p\": \"18.70\", // Mark price\n \"f\": \"1.70\", // Estimated funding rate\n \"i\": \"19.70\", // Index price\n \"n\": 1694687965941000, // Next funding timestamp in microseconds\n}\n```\n\n## Ticker\n\nThe ticker stream pushes 24hr rolling statistics for a single symbol\nevery second.\n\nStream name format: `ticker.`\n\n```\n{\n \"e\": \"ticker\", // Event type\n \"E\": 1694687692980000, // Event time in microseconds\n \"s\": \"SOL_USD\", // Symbol\n \"o\": \"18.75\", // First price\n \"c\": \"19.24\", // Last price\n \"h\": \"19.80\", // High price\n \"l\": \"18.50\", // Low price\n \"v\": \"32123\", // Base asset volume\n \"V\": \"928190\", // Quote asset volume\n \"n\": 93828 // Number of trades\n}\n```\n## Open interest\n\nOpen interest updates are pushed to the openInterest stream every 60\nseconds.\n\nStream name format: `openInterest.`\n```\n{\n \"e\": \"openInterest\", // Event type\n \"E\": 1694687965941000, // Event time in microseconds\n \"s\": \"SOL_USDC_PERP\", // Symbol\n \"o\": \"100\", // Open interest in contracts\n}\n```\n\n## Trade\n\nContains public trade data for a single symbol. The trade ID is a\nsequential number specific to the symbol. This stream includes updates\nfor trades executed as a result of liquidations.\n\nStream name format: `trade.`\n```\n{\n \"e\": \"trade\", // Event type\n \"E\": 1694688638091000, // Event time in microseconds\n \"s\": \"SOL_USDC\", // Symbol\n \"p\": \"18.68\", // Price\n \"q\": \"0.122\", // Quantity\n \"b\": \"111063114377265150\", // Buyer order ID\n \"a\": \"111063114585735170\", // Seller order ID\n \"t\": 12345, // Trade ID\n \"T\": 1694688638089000, // Engine timestamp in microseconds\n \"m\": true // Is the buyer the maker?\n}\n```"
+ "description": "# Usage\n\n## Subscribing\n\nTo use the websocket API, connect to\n`wss://ws.backpack.exchange`.\n\nTo subscribe to a stream with the name `stream` send a text frame\nover the websocket connection with the following JSON payload:\n\n```\n{\n \"method\": \"SUBSCRIBE\",\n \"params\": [\"stream\"]\n}\n```\n\nSimilarly, to unsubscribe from a stream with the name `stream`:\n\n```\n{\n \"method\": \"UNSUBSCRIBE\",\n \"params\": [\"stream\"]\n}\n```\n\nYou can subscribe or unsubscribe from multiple streams if you include\nmore than one in the params field.\n\nAll data from streams is wrapped in a JSON object of the following form:\n\n```\n{\n \"stream\": \"\",\n \"data\": \"\"\n}\n```\n\nThe following command can be used to test subscribing to a stream:\n```\n(sleep 1; \\\necho '{\"method\":\"SUBSCRIBE\",\"params\":[\"depth.SOL_USDC\"]}';\\\ncat) |\\\nwscat -c wss://ws.backpack.exchange\n```\nThe payloads for each stream time are outlined below.\n\n## Timing\n\nTimestamps are in microseconds (except for the K-line start and end\ntimes). The event timestamp is the time the event was emitted from\nthe websocket server, and the engine timestamp is the time the event\nwas generated by the matching engine.\n\nIf a message aggregates more than one event (for example, a depth\nmessage), the engine timestamp will be the timestamp of the last\nmatching engine event.\n\n## Keeping the connection alive\n\nTo keep the connection alive, a `Ping` frame will be sent from the\nserver every 60s, and a `Pong` is expected to be received from the\nclient. If a `Pong` is not received within 120s, a `Close` frame will be\nsent and the connection will be closed.\n\nIf the server is shutting down, a `Close` frame will be sent and then a\ngrace period of 30s will be given before the connection is closed. The\nclient should reconnect after receiving the `Close` frame. The client\nwill be reconnected to a server that is not shutting down.\n\n# Private\n\nSubscribing to a private stream requires a valid signature generated\nfrom an ED25519 keypair. For stream subscriptions, the signature\nshould be of the form:\n\n```text\ninstruction=subscribe×tamp=1614550000000&window=5000\n```\n\nWhere the timestamp and window are in milliseconds.\n\nPrivate streams are prefixed with `account.` and require signature data\nto be submitted in the subscribe parameters. The verifying key and\nsignature should be base64 encoded.\n\n```\n{\n \"method\": \"SUBSCRIBE\",\n \"params\": [\"stream\"],\n \"signature\": [\"\", \"\", \"\", \"\"]\n}\n````\n\n## Order update\n\nOn any mutation to an order the order will be pushed to the order update\nstream. The event type of the order update will be one of the\nfollowing:\n\n- `orderAccepted`\n- `orderCancelled`\n- `orderExpired`\n- `orderFill`\n- `orderModified`\n- `triggerPlaced`\n- `triggerFailed`\n\nAn `orderModified` update will be received when a resting reduce only\norder's quantity is decreased in order to prevent position side\nreversal.\n\n### Stream Name Format\n- For all markets: `account.orderUpdate`\n- For single market: `account.orderUpdate.`\n\n```\n{\n \"e\": \"orderAccepted\", // Event type\n \"E\": 1694687692980000, // Event time in microseconds\n \"s\": \"SOL_USD\", // Symbol\n \"c\": 123, // Client order ID\n \"S\": \"Bid\", // Side\n \"o\": \"LIMIT\", // Order type\n \"f\": \"GTC\", // Time in force\n \"q\": \"32123\", // Quantity\n \"Q\": \"32123\", // Quantity in quote\n \"p\": \"20\", // Price\n \"P\": \"21\", // Trigger price\n \"B\": \"LastPrice\", // Trigger by\n \"a\": \"30\", // Take profit trigger price\n \"b\": \"10\", // Stop loss trigger price\n \"d\": \"MarkPrice\", // Take profit trigger by\n \"g\": \"IndexPrice\", // Stop loss trigger by\n \"Y\": \"10\", // Trigger quantity\n \"X\": \"Filled\", // Order state\n \"R\": \"PRICE_BAND\", // Order expiry reason\n \"i\": \"1111343026172067\" // Order ID\n \"t\": 567, // Trade ID\n \"l\": \"1.23\", // Fill quantity\n \"z\": \"321\", // Executed quantity\n \"Z\": \"123\", // Executed quantity in quote\n \"L\": \"20\", // Fill price\n \"m\": true, // Whether the order was maker\n \"n\": \"23\", // Fee\n \"N\": \"USD\", // Fee symbol\n \"V\": \"RejectTaker\", // Self trade prevention\n \"T\": 1694687692989999, // Engine timestamp in microseconds\n \"O\": \"USER\" // Origin of the update\n \"I\": \"1111343026156135\" // Related order ID\n \"H\": 6023471188 // Strategy ID\n \"y\": true // Post only\n}\n```\n\nThere are several possible values for the `O` field (origin of the\nupdate):\n- `USER`: The origin of the update was due to order entry by the user.\n- `LIQUIDATION_AUTOCLOSE`: The origin of the update was due to a\nliquidation by the liquidation engine.\n- `ADL_AUTOCLOSE`: The origin of the update was due to an ADL\n(auto-deleveraging) event.\n- `COLLATERAL_CONVERSION`: The origin of the update was due to a\ncollateral conversion to settle debt on the account.\n- `SETTLEMENT_AUTOCLOSE`: The origin of the update was due to the\nsettlement of a position on a dated market.\n- `BACKSTOP_LIQUIDITY_PROVIDER`: The origin of the update was due to a\nbackstop liquidity provider facilitating a liquidation.\n\nSome fields are conditional on the order settings or event type:\n\n- `c` - Only present if the order has a client order ID.\n- `q` - Only present if the order has a quantity set.\n- `Q` - Only present if the order is reverse market order.\n- `p` - Only present if the order is a limit order.\n- `P` - Only present if the order is a trigger order.\n- `B` - Only present if the order is a trigger order.\n- `a` - Only present if the order has a take profit trigger price set.\n- `b` - Only present if the order has a stop loss trigger price set.\n- `d` - Only present if the order has a take profit trigger price set.\n- `g` - Only present if the order has a stop loss trigger price set.\n- `Y` - Only present if the order is a trigger order.\n- `R` - Only present if the event is a `orderExpired` event.\n- `t` - Only present if the event is a `orderFill` event.\n- `l` - Only present if the event is a `orderFill` event.\n- `L` - Only present if the event is a `orderFill` event.\n- `m` - Only present if the event is a `orderFill` event.\n- `n` - Only present if the event is a `orderFill` event.\n- `N` - Only present if the event is a `orderFill` event.\n\n## Position update\n\nOn any mutation to a position the position will be pushed to the\nposition update stream. The event type of the position update will\nbe one of the following:\n\n- `positionAdjusted`\n- `positionOpened`\n- `positionClosed`\n\nOn subscription, a message will be sent to the client with the current\nopen positions, if any. The `e` field will not be present in the\nmessage.\n\n### Stream Name Format\n- For all markets: `account.positionUpdate`\n- For single market: `account.positionUpdate.`\n\n```\n{\n \"e\": \"positionOpened\", // Event type\n \"E\": 1694687692980000, // Event time in microseconds\n \"s\": \"SOL_USDC_PERP\", // Symbol\n \"b\": 123, // Break event price\n \"B\": 122, // Entry price\n \"f\": 0.5, // Initial margin fraction\n \"M\": 122, // Mark price\n \"m\": 0.01, // Maintenance margin fraction\n \"q\": 5, // Net quantity\n \"Q\": 6, // Net exposure quantity\n \"n\": 732 , // Net exposure notional\n \"i\": \"1111343026172067\" // Position ID\n \"p\": \"-1\", // PnL realized\n \"P\": \"0\", // PnL unrealized\n \"T\": 1694687692989999 // Engine timestamp in microseconds\n}\n```\n\nThe net quantity field will be positive if the position is long and\nnegative if the position is short.\n\nThe net exposure quantity field includes exposure from the open\nposition, as well as any open orders.\n\n## RFQ Update\n\nThis WebSocket stream provides real-time updates on RFQs (Request for\nQuotes) that are relevant to makers. Events are pushed to this\nstream whenever there is a significant state change in an RFQ or its\nassociated quotes, allowing makers to monitor and respond to RFQs as\nthey progress through various states.\n\n### Event Types\n\nFor RFQs that submitted by other requesters.\n- `rfqActive`: Indicates that an RFQ is active and open for quotes.\n\nFor RFQs that submitted by your account.\n- `rfqAccepted`: Indicates that an RFQ has been accepted and is no\n- `rfqRefreshed`: Indicates that an RFQ has been refreshed, is active\nand open for quotes.\n- `rfqCancelled`: Indicates that an RFQ has been cancelled or expired.\n- `rfqCandidate`: RFQ has received a new best quote.\n- `rfqFilled`: Indicates that an RFQ has been fully filled with a quote.\n\nFor Quotes submitted by your account.\n- `quoteAccepted`: Indicates that a quote submitted by the maker has\nbeen accepted.\n- `quoteCancelled`: Indicates that a quote has been cancelled due to\nquote submission, RFQ being filled, refreshed, cancelled, or expired.\n\n### Quote Submission and RFQ Timing\n\nMakers should submit quotes before the **submission time** (`w` field)\nis reached, as indicated in each `rfqActive` event. An RFQ remains\nactive until the **expiration time** (`W` field). If no quote is\naccepted or the RFQ is not cancelled, makers may continue to submit\nquotes until expiration.\n\nRFQs can periodically request new quotes by issuing additional\n`rfqActive` events. Each new `rfqActive` event will have the same\nRFQ ID (`R` field) but updated values for **submission time** and\n**expiration time**, allowing makers to participate in extended or\nrenewed quoting periods for ongoing RFQs.\n\n### Stream Name Format\n- For all markets: `account.rfqUpdate`\n- For single market: `account.rfqUpdate.`\n\n### Example Messages\n\n**RFQ Accepted** (sent to requester)\n```\n{\n \"e\": \"rfqAccepted\", // Event type\n \"E\": 1730225420369829, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"q\": \"10\", // Quantity (if quantity in base asset)\n \"w\": 1730225480368, // Submission time in milliseconds\n \"W\": 1730225540368, // Expiry time in milliseconds\n \"X\": \"New\", // RFQ status\n \"T\": 1730225420368765 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Active** (broadcast to all rfq listeners)\n```\n{\n \"e\": \"rfqActive\", // Event type\n \"E\": 1730225420369829, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"q\": \"10\", // Quantity (optional) (if quantity in base asset)\n \"w\": 1730225480368, // Submission time in milliseconds\n \"W\": 1730225540368, // Expiry time in milliseconds\n \"X\": \"New\", // RFQ status\n \"T\": 1730225420368765 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Refreshed** (sent to requester)\n```\n{\n \"e\": \"rfqRefreshed\", // Event type\n \"E\": 1730225450369829, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"q\": \"10\", // Quantity (optional) (if quantity in base asset)\n \"w\": 1730225480368, // Submission time in milliseconds\n \"W\": 1730225540368, // Expiry time in milliseconds\n \"X\": \"New\", // RFQ status\n \"T\": 1730225450368765 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Cancelled** (sent to taker only)\n```\n{\n \"e\": \"rfqCancelled\", // Event type\n \"E\": 1730225460369829, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"Q\": \"150\", // Quote quantity (optional) (if quantity in quote asset)\n \"w\": 1730225480368, // Submission time in milliseconds\n \"W\": 1730225540368, // Expiry time in milliseconds\n \"X\": \"Cancelled\", // RFQ status\n \"T\": 1730225460368765 // Engine timestamp in microseconds\n}\n```\n\n**Quote Accepted** (sent to quoter)\n```\n{\n \"e\": \"quoteAccepted\", // Event type\n \"E\": 1730225434631394, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"u\": 113392054083780608, // Quote ID\n \"C\": \"123\", // Client Quote ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"X\": \"New\", // Quote status\n \"T\": 1730225434629778 // Engine timestamp in microseconds\n}\n```\n\n**Quote Cancelled** (sent to quoter)\n```\n{\n \"e\": \"quoteCancelled\", // Event type\n \"E\": 1730225583761963, // Event time in microseconds\n \"R\": 113392061354344448, // RFQ ID\n \"u\": 113392062870847488, // Quote ID\n \"C\": \"123\", // Client Quote ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"X\": \"Cancelled\", // Quote status\n \"T\": 1730225583753811 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Candidate** (sent to requester with quote details)\n```\n{\n \"e\": \"rfqCandidate\", // Event type\n \"E\": 1730225490648996, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"u\": 113392054083780608, // Quote ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"q\": \"10\", // RFQ quantity (in base asset)\n \"Q\": \"150\", // RFQ quote quantity (in quote asset)\n \"p\": \"15.50\", // Taker price (quote price + fee)\n \"X\": \"New\", // RFQ status\n \"T\": 1730225490647080 // Engine timestamp in microseconds\n}\n```\n\n**RFQ Filled** (sent to both requester and quoter)\n```\n// To requester\n{\n \"e\": \"rfqFilled\", // Event type\n \"E\": 1730225497648996, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"u\": 113392054083780608, // Quote ID\n \"C\": \"123\", // Client RFQ ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"S\": \"Bid\", // RFQ side\n \"Q\": \"150\", // RFQ quote quantity (optional) (if quantity in quote asset)\n \"p\": \"15.50\", // Taker price (quote price + fee)\n \"X\": \"Filled\", // RFQ status\n \"T\": 1730225497647080 // Engine timestamp in microseconds\n}\n\n// To quoter\n{\n \"e\": \"rfqFilled\", // Event type\n \"E\": 1730225497648996, // Event time in microseconds\n \"R\": 113392053149171712, // RFQ ID\n \"u\": 113392054083780608, // Quote ID\n \"C\": \"123\", // Client Quote ID\n \"s\": \"SOL_USDC_RFQ\", // Symbol\n \"p\": \"15.00\", // Price\n \"X\": \"Filled\", // Quote status\n \"T\": 1730225497647080 // Engine timestamp in microseconds\n}\n```\n\n### Field Descriptions\n\n- `e` - Event type (e.g., `rfqActive`, `rfqAccepted`, `rfqRefreshed`,\n`rfqCancelled`, `quoteAccepted`, `quoteCancelled`, `rfqCandidate`,\n`rfqFilled`).\n- `E` - Event time in microseconds.\n- `R` - RFQ ID, identifying the request for quote.\n- `u` - Quote ID, identifying the specific quote.\n- `C` - Client ID (either Client RFQ ID or Client Quote ID depending on\ncontext).\n- `s` - Symbol the RFQ is for.\n- `S` - Side of the RFQ, either \"Bid\" or \"Ask\".\n- `q` - Quantity for the RFQ (in base asset, if quantity in base asset).\n- `Q` - Quote quantity for the RFQ (in quote asset, if quantity in quote\nasset).\n- `p` - Price associated with the quote/fill event.\n- `w` - Submission time for the RFQ in milliseconds.\n- `W` - Expiry time for the RFQ in milliseconds.\n- `X` - Order status (e.g., `New`, `Cancelled`, `Filled`).\n- `T` - Engine timestamp in microseconds.\n\nSome fields are conditional and may be present only in specific events.\n\n# Public\n\n## Book ticker\n\nStream name format: `bookTicker.`\n```\n{\n \"e\": \"bookTicker\", // Event type\n \"E\": 1694687965941000, // Event time in microseconds\n \"s\": \"SOL_USDC\", // Symbol\n \"a\": \"18.70\", // Inside ask price\n \"A\": \"1.000\", // Inside ask quantity\n \"b\": \"18.67\", // Inside bid price\n \"B\": \"2.000\", // Inside bid quantity\n \"u\": \"111063070525358080\", // Update ID of event\n \"T\": 1694687965940999 // Engine timestamp in microseconds\n}\n```\n\n## Depth\n\nContains incremental depth updates. Each depth update has the absolute\nvalue of the depths at the given levels, and only changes when the\ndepth has changed.\n\nTo obtain an initial snapshot of the depth, the client should query the\n[REST API](https://docs.backpack.exchange/#tag/Markets/operation/get_depth).\n\nThe depth stream will push updates as quickly as possible, but under\nload it may aggregate more than one update into a single event. In\nthis case the `U` and `u` fields will not be the same. The `U` field\nis the first update ID in the event, and the `u` field is the final\nupdate ID in the event.\n\nThere are alternative depth streams that aggregates updates into a\nsingle message over a 200ms, 600ms or 1000ms period instead of pushing\nupdates in realtime. This is useful for reducing network traffic.\n\nUpdates are sequential, so `U` will always be `u + 1` from the previous\nmessage. If this is not the case, the client should assume that the\ndepth has been invalidated and requery the REST API.\n\nStream name format: `depth.` (realtime)\nStream name format: `depth.200ms.` (aggregated)\nStream name format: `depth.600ms.` (aggregated)\nStream name format: `depth.1000ms.` (aggregated)\n```\n{\n \"e\": \"depth\", // Event type\n \"E\": 1694687965941000, // Event time in microseconds\n \"s\": \"SOL_USDC\", // Symbol\n \"a\": [ // Asks\n [\n \"18.70\",\n \"0.000\"\n ]\n ],\n \"b\": [ // Bids\n [\n \"18.67\",\n \"0.832\"\n ],\n [\n \"18.68\",\n \"0.000\"\n ]\n ],\n \"U\": 94978271, // First update ID in event\n \"u\": 94978271, // Last update ID in event\n \"T\": 1694687965940999 // Engine timestamp in microseconds\n}\n```\n\n## K-Line\n\nStream name format: `kline..`\n\n```\n{\n \"e\": \"kline\", // Event type\n \"E\": 1694687692980000, // Event time in microseconds\n \"s\": \"SOL_USD\", // Symbol\n \"t\": \"2024-09-11T12:00:00\", // K-Line start time (ISO 8601 format)\n \"T\": \"2024-09-11T12:01:00\", // K-Line close time (ISO 8601 format)\n \"o\": \"18.75\", // Open price\n \"c\": \"19.25\", // Close price\n \"h\": \"19.80\", // High price\n \"l\": \"18.50\", // Low price\n \"v\": \"32123\", // Base asset volume\n \"n\": 93828, // Number of trades\n \"X\": false // Is this k-line closed?\n}\n```\n\n## Liquidation\n\nContains updates for liquidation events for all liquidation types.\n\nStream name format: `liquidation`\n\n```\n{\n \"e\": \"liquidation\", // Event type\n \"E\": 1694688638091000, // Event time in microseconds\n \"q\": \"10\", // Quantity\n \"p\": \"18.70\", // Price\n \"S\": \"Bid\", // Side\n \"s\": \"SOL_USDC\", // Symbol\n \"T\": 567, // Engine timestamp in microseconds\n}\n```\n\n## Mark price\n\nStream name format: `markPrice.`\n\n```\n{\n \"e\": \"markPrice\", // Event type\n \"E\": 1694687965941000, // Event time in microseconds\n \"s\": \"SOL_USDC\", // Symbol\n \"p\": \"18.70\", // Mark price\n \"f\": \"1.70\", // Estimated funding rate\n \"i\": \"19.70\", // Index price\n \"n\": 1694687965941, // Next funding timestamp in milliseconds\n \"T\": 1694687965940999 // Engine timestamp in microseconds\n}\n```\n\n## Ticker\n\nThe ticker stream pushes 24hr rolling statistics for a single symbol\nevery second.\n\nStream name format: `ticker.`\n\n```\n{\n \"e\": \"ticker\", // Event type\n \"E\": 1694687692980000, // Event time in microseconds\n \"s\": \"SOL_USD\", // Symbol\n \"o\": \"18.75\", // First price\n \"c\": \"19.24\", // Last price\n \"h\": \"19.80\", // High price\n \"l\": \"18.50\", // Low price\n \"v\": \"32123\", // Base asset volume\n \"V\": \"928190\", // Quote asset volume\n \"n\": 93828 // Number of trades\n}\n```\n## Open interest\n\nOpen interest updates are pushed to the openInterest stream every 60\nseconds.\n\nStream name format: `openInterest.`\n```\n{\n \"e\": \"openInterest\", // Event type\n \"E\": 1694687965941000, // Event time in microseconds\n \"s\": \"SOL_USDC_PERP\", // Symbol\n \"o\": \"100\", // Open interest in contracts\n}\n```\n\n## Trade\n\nContains public trade data for a single symbol. The trade ID is a\nsequential number specific to the symbol. This stream includes updates\nfor trades executed as a result of liquidations.\n\nStream name format: `trade.`\n```\n{\n \"e\": \"trade\", // Event type\n \"E\": 1694688638091000, // Event time in microseconds\n \"s\": \"SOL_USDC\", // Symbol\n \"p\": \"18.68\", // Price\n \"q\": \"0.122\", // Quantity\n \"b\": \"111063114377265150\", // Buyer order ID\n \"a\": \"111063114585735170\", // Seller order ID\n \"t\": 12345, // Trade ID\n \"T\": 1694688638089000, // Engine timestamp in microseconds\n \"m\": true // Is the buyer the maker?\n}\n```"
},
{
"name": "System",
@@ -7745,7 +7745,15 @@
"ZEC",
"PAXG",
"MORPHO",
- "ATH"
+ "ATH",
+ "KGEN",
+ "XAUT",
+ "FOGO",
+ "SPX",
+ "ETHFI",
+ "APR",
+ "PIPE",
+ "MET"
]
},
"Balance": {
@@ -7865,7 +7873,8 @@
"Solana",
"Story",
"Tron",
- "XRP"
+ "XRP",
+ "Zcash"
]
},
"BorrowLendBookState": {
@@ -8708,6 +8717,7 @@
"story",
"tron",
"xRP",
+ "zcash",
"equalsMoney",
"banxa",
"internal"
@@ -12444,4 +12454,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index eef18f8..d57477c 100644
--- a/package.json
+++ b/package.json
@@ -24,8 +24,9 @@
},
"dependencies": {
"@grvt/client": "^1.6.4",
+ "@noble/ed25519": "^3.0.0",
"axios": "^1.12.2",
- "ccxt": "^4.5.5",
+ "ccxt": "^4.5.12",
"dotenv": "^17.2.2",
"ethereum-cryptography": "^2.1.3",
"ink": "^6.3.1",
diff --git a/src/exchanges/backpack/adapter.ts b/src/exchanges/backpack/adapter.ts
index 4dae7d6..22c5a1e 100644
--- a/src/exchanges/backpack/adapter.ts
+++ b/src/exchanges/backpack/adapter.ts
@@ -122,7 +122,13 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
- this.clearRetry();
+ if (process.env.BACKPACK_DEBUG === "1") {
+ console.error(`[BackpackExchangeAdapter] initialize succeeded`);
+ }
+ if (process.env.BACKPACK_DEBUG === "1") {
+ console.error(`[BackpackExchangeAdapter] initialize succeeded`);
+ }
+ this.clearRetry();
return value;
})
.catch((error) => {
diff --git a/src/exchanges/backpack/gateway.ts b/src/exchanges/backpack/gateway.ts
index ad88b2a..e51d621 100644
--- a/src/exchanges/backpack/gateway.ts
+++ b/src/exchanges/backpack/gateway.ts
@@ -1,4 +1,13 @@
-import ccxt, { type Balances, type Order as CcxtOrder, type OrderBook as CcxtOrderBook, type Ticker as CcxtTicker } from "ccxt";
+import ccxt, {
+ type Balances,
+ type Order as CcxtOrder,
+ type OrderBook as CcxtOrderBook,
+ type Ticker as CcxtTicker,
+} from "ccxt";
+import NodeWebSocket from "ws";
+import { sign, utils as edUtils, hashes as edHashes } from "@noble/ed25519";
+import { sha512 } from "@noble/hashes/sha512";
+import { randomBytes } from "crypto";
import type {
AsterAccountSnapshot,
AsterAccountPosition,
@@ -17,6 +26,40 @@ import type {
KlineListener,
} from "../adapter";
+const WebSocketCtor: typeof globalThis.WebSocket =
+ typeof globalThis.WebSocket !== "undefined"
+ ? globalThis.WebSocket
+ : ((NodeWebSocket as unknown) as typeof globalThis.WebSocket);
+
+edUtils.sha512 = sha512 as unknown as typeof edUtils.sha512;
+edHashes.sha512 = sha512;
+
+const ORDER_STATUS_MAP: Record = {
+ NEW: "NEW",
+ OPEN: "OPEN",
+ FILLED: "FILLED",
+ CLOSED: "FILLED",
+ CANCELLED: "CANCELLED",
+ CANCELED: "CANCELLED",
+ EXPIRED: "EXPIRED",
+ PARTIALLY_FILLED: "PARTIALLY_FILLED",
+ PARTIAL: "PARTIALLY_FILLED",
+ TRIGGER_PENDING: "TRIGGER_PENDING",
+ TRIGGERPENDING: "TRIGGER_PENDING",
+ TRIGGER_FAILED: "TRIGGER_FAILED",
+ TRIGGERFAILED: "TRIGGER_FAILED",
+ REJECTED: "REJECTED",
+};
+
+const TRIGGER_TOPICS = {
+ orders: (marketId: string) => `account.orderUpdate.${marketId}`,
+ positions: (marketId: string) => `account.positionUpdate.${marketId}`,
+};
+
+const DEFAULT_WS_WINDOW = "5000";
+const WS_PING_INTERVAL = 25_000;
+const WS_RECONNECT_DELAY = 2_000;
+
export interface BackpackGatewayOptions {
apiKey?: string;
apiSecret?: string;
@@ -36,50 +79,58 @@ export class BackpackGateway {
private readonly logger: (context: string, error: unknown) => void;
private initialized = false;
private initPromise: Promise | null = null;
-
- // Event listeners
- private accountListeners = new Set();
- private orderListeners = new Set();
- private depthListeners = new Set();
- private tickerListeners = new Set();
- private klineListeners = new Set<{ interval: string; callback: KlineListener }>();
-
- // Polling intervals
- private accountPollTimer: NodeJS.Timeout | null = null;
- private orderPollTimer: NodeJS.Timeout | null = null;
- private depthPollTimer: NodeJS.Timeout | null = null;
- private tickerPollTimer: NodeJS.Timeout | null = null;
- private klinePollTimers = new Map();
-
- // WebSocket streams
- private wsOrderBook: any = null;
- private wsTicker: any = null;
- private wsKlines = new Map();
- private wsOrders: any = null;
- private wsBalance: any = null;
+
+ private readonly accountListeners = new Set();
+ private readonly orderListeners = new Set();
+ private readonly depthListeners = new Set();
+ private readonly tickerListeners = new Set();
+ private readonly klineListeners = new Set<{ interval: string; callback: KlineListener }>();
+
+ private accountPollTimer: ReturnType | null = null;
+ private orderPollTimer: ReturnType | null = null;
+ private depthPollTimer: ReturnType | null = null;
+ private tickerPollTimer: ReturnType | null = null;
+ private readonly klinePollTimers = new Map>();
+
+ private readonly localOrders = new Map();
+ private lastBalanceSnapshot: AsterAccountSnapshot | null = null;
+ private marketId = "";
+
+ private ws: WebSocket | null = null;
+ private wsReady = false;
+ private wsPingTimer: ReturnType | null = null;
+ private wsReconnectTimer: ReturnType | null = null;
+ private readonly wsTopics = new Set();
+ private wsConnecting = false;
+ private readonly wsWindow: string;
+ private wsCleanup: (() => void) | null = null;
+
+ private readonly apiKey: string;
+ private readonly apiSecret: string;
constructor(options: BackpackGatewayOptions) {
this.symbol = options.symbol.toUpperCase();
this.marketSymbol = this.symbol;
this.logger = options.logger ?? ((context, error) => console.error(`[BackpackGateway] ${context}:`, error));
-
- // dynamic constructor for specific exchange
+ this.apiKey = options.apiKey ?? process.env.BACKPACK_API_KEY ?? "";
+ this.apiSecret = options.apiSecret ?? process.env.BACKPACK_API_SECRET ?? "";
+ this.wsWindow = process.env.BACKPACK_WS_WINDOW ?? DEFAULT_WS_WINDOW;
+
this.exchange = new (ccxt as any).backpack({
- apiKey: options.apiKey,
- secret: options.apiSecret,
- password: options.password,
- subaccount: options.subaccount,
- sandbox: options.sandbox ?? false,
+ apiKey: this.apiKey,
+ secret: this.apiSecret,
+ password: options.password ?? process.env.BACKPACK_PASSWORD,
+ subaccount: options.subaccount ?? process.env.BACKPACK_SUBACCOUNT,
+ sandbox: options.sandbox ?? (process.env.BACKPACK_SANDBOX === "true"),
enableRateLimit: true,
- timeout: 30000,
+ timeout: 30_000,
});
+
}
async ensureInitialized(symbol?: string): Promise {
if (this.initialized) return;
-
if (this.initPromise) return this.initPromise;
-
this.initPromise = this.doInitialize(symbol);
return this.initPromise;
}
@@ -87,98 +138,82 @@ export class BackpackGateway {
private async doInitialize(symbol?: string): Promise {
try {
await this.exchange.loadMarkets();
-
- // Verify symbol exists
const requested = (symbol ?? this.symbol).toUpperCase();
- const resolved = this.resolveMarketSymbol(requested);
- if (!resolved) {
+ const market = this.findMarket(requested);
+ if (!market) {
throw new Error(`Symbol ${requested} not found in Backpack markets`);
}
- this.marketSymbol = resolved;
- this.market = this.exchange.market(this.marketSymbol);
- this.isContractMarket = Boolean(this.market?.contract);
-
+ this.market = market;
+ this.marketSymbol = market.symbol;
+ this.marketId = market.id;
+ this.isContractMarket = Boolean(market.contract);
+ if (process.env.BACKPACK_DEBUG === "1") {
+ console.debug("[BackpackGateway] marketInfo", {
+ userSymbol: this.symbol,
+ ccxtSymbol: this.marketSymbol,
+ marketId: this.marketId,
+ });
+ }
this.initialized = true;
- this.logger("initialize", `Backpack gateway initialized for ${this.marketSymbol}`);
} catch (error) {
- this.logger("initialize", error);
throw error;
}
}
- private resolveMarketSymbol(requested: string): string | null {
- // normalize helpers (strip non-alphanumerics for robust comparisons)
- const strip = (v: string | undefined | null) => (v ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
-
- // Backpack uses USDC quote; accept common USD/USDT aliases in user input
- const normalizeUsdAlias = (v: string) => {
- const up = v.toUpperCase();
- // Replace ...USD... or ...USDT... (optionally before _ or PERP or end) with USDC
- // Examples: BTCUSDPERP -> BTCUSDCPERP, BTCUSD -> BTCUSDC, BTC_USDT_PERP -> BTC_USDC_PERP
- return up
- .replace(/USDT(?=(?:[_-]?PERP)?$)/, "USDC")
- .replace(/USD(?=(?:[_-]?PERP)?$)/, "USDC");
- };
-
- const requestedWithUsdc = normalizeUsdAlias(requested);
- const compactRequested = strip(requestedWithUsdc);
-
- // 1) exact key in markets (e.g. "BTC/USDC" or "BTC/USDC:USDC")
- if (this.exchange.markets[requestedWithUsdc]) return requestedWithUsdc;
-
- // 2) direct markets_by_id lookup by exact id
- const byId = (this.exchange as any).markets_by_id ?? {};
- if (byId[requestedWithUsdc]) return byId[requestedWithUsdc].symbol;
-
- // 3) flexible lookup: compare compacted forms against ids, symbols, and base+quote
+ private findMarket(requested: string): any | null {
+ const normalize = (value: string | undefined | null): string =>
+ (value ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
+ const normalized = normalize(requested);
const markets = Object.values(this.exchange.markets) as Array;
- for (const m of markets) {
- const idCompact = strip(m.id as string);
- const symbolCompact = strip(m.symbol as string);
- const baseQuoteCompact = strip((m.base as string) + (m.quote as string));
- if (idCompact === compactRequested) return m.symbol;
- if (symbolCompact === compactRequested) return m.symbol;
- if (baseQuoteCompact === compactRequested) return m.symbol;
- }
- // 4) try matching against markets_by_id keys by compacted form
- for (const key of Object.keys(byId)) {
- if (strip(key) === compactRequested) return byId[key].symbol;
+ for (const market of markets) {
+ if (normalize(market.id) === normalized) return market;
+ }
+ for (const market of markets) {
+ if (normalize(market.symbol) === normalized) return market;
+ }
+ for (const market of markets) {
+ const combo = `${market.base ?? ""}${market.quote ?? ""}${market.contract ? "PERP" : ""}`;
+ if (normalize(combo) === normalized) return market;
}
-
return null;
}
- private normalizeTimeframe(interval: string): string {
- const timeframeMap: Record = {
- "1m": "1m",
- "5m": "5m",
- "15m": "15m",
- "1h": "1h",
- "4h": "4h",
- "1d": "1d",
- };
- return timeframeMap[interval] || "1m";
- }
+ // ---- Subscription APIs -------------------------------------------------
- // Event subscription methods
onAccount(callback: AccountListener): void {
this.accountListeners.add(callback);
+ if (this.lastBalanceSnapshot) {
+ try {
+ callback(this.lastBalanceSnapshot);
+ } catch (error) {
+ this.logger("accountReplay", error);
+ }
+ }
+ void this.subscribeOnce("accountSubscribe", () => this.buildPositionTopic(true));
this.startAccountPolling();
}
onOrders(callback: OrderListener): void {
this.orderListeners.add(callback);
+ if (this.localOrders.size) {
+ try {
+ callback(Array.from(this.localOrders.values()).map((order) => ({ ...order })));
+ } catch (error) {
+ this.logger("ordersReplay", error);
+ }
+ }
+ void this.subscribeOnce("ordersSubscribe", () => this.buildOrderTopic(true));
this.startOrderPolling();
}
- onDepth(callback: DepthListener): void {
- this.depthListeners.add(callback);
+ onDepth(_callback: DepthListener): void {
+ this.depthListeners.add(_callback);
this.startDepthPolling();
}
- onTicker(callback: TickerListener): void {
- this.tickerListeners.add(callback);
+ onTicker(_callback: TickerListener): void {
+ this.tickerListeners.add(_callback);
this.startTickerPolling();
}
@@ -188,199 +223,201 @@ export class BackpackGateway {
this.startKlinePolling(normalizedInterval);
}
- // Polling implementations
+ // ---- Polling -----------------------------------------------------------
+
private startAccountPolling(): void {
if (this.accountPollTimer) return;
-
const poll = async () => {
try {
- const accountSnapshot = await this.fetchAccountSnapshot();
-
- for (const listener of this.accountListeners) {
- listener(accountSnapshot);
- }
+ const snapshot = await this.fetchAccountSnapshot();
+ this.lastBalanceSnapshot = snapshot;
+ this.emitAccount(snapshot);
} catch (error) {
this.logger("accountPoll", error);
}
};
-
- poll(); // Initial fetch
- this.accountPollTimer = setInterval(poll, 5000); // Poll every 5 seconds
+ void poll();
+ this.accountPollTimer = setInterval(poll, 5_000);
}
private startOrderPolling(): void {
if (this.orderPollTimer) return;
-
const poll = async () => {
try {
- const [openOrders, closedOrders] = await Promise.all([
+ const [openOrders, allOrders] = await Promise.all([
this.exchange.fetchOpenOrders(this.marketSymbol),
- this.exchange.fetchClosedOrders(this.marketSymbol, undefined, 50), // Last 50 closed orders
+ this.exchange.fetchOrders(this.marketSymbol, undefined, 200, {}),
]);
-
- const allOrders = [...openOrders, ...closedOrders];
- const mappedOrders = allOrders.map(order => this.mapOrderToAsterOrder(order));
-
- for (const listener of this.orderListeners) {
- listener(mappedOrders);
+ const active = new Map();
+ for (const entry of [...openOrders, ...allOrders]) {
+ const status = this.normalizeStatus(entry.status ?? (entry.info?.status as string));
+ if (this.isTerminalStatus(status)) continue;
+ const mapped = this.mapRestOrder(entry);
+ active.set(mapped.orderId, mapped);
}
+ this.localOrders.clear();
+ for (const [id, order] of active.entries()) {
+ this.localOrders.set(id, order);
+ }
+ this.emitOrders();
} catch (error) {
this.logger("orderPoll", error);
}
};
-
- poll(); // Initial fetch
- this.orderPollTimer = setInterval(poll, 2000); // Poll every 2 seconds
+ void poll();
+ this.orderPollTimer = setInterval(poll, 3_000);
}
private startDepthPolling(): void {
if (this.depthPollTimer) return;
-
const poll = async () => {
try {
const orderbook = await this.exchange.fetchOrderBook(this.marketSymbol, 20);
const depth = this.mapOrderBookToDepth(orderbook);
-
- for (const listener of this.depthListeners) {
- listener(depth);
- }
+ for (const listener of this.depthListeners) listener(depth);
} catch (error) {
this.logger("depthPoll", error);
}
};
-
- poll(); // Initial fetch
- this.depthPollTimer = setInterval(poll, 1000); // Poll every 1 second
+ void poll();
+ this.depthPollTimer = setInterval(poll, 1_000);
}
private startTickerPolling(): void {
if (this.tickerPollTimer) return;
-
const poll = async () => {
try {
const ticker = await this.exchange.fetchTicker(this.marketSymbol);
- const asterTicker = this.mapTickerToAsterTicker(ticker);
-
- for (const listener of this.tickerListeners) {
- listener(asterTicker);
- }
+ const mapped = this.mapTickerToAsterTicker(ticker);
+ for (const listener of this.tickerListeners) listener(mapped);
} catch (error) {
this.logger("tickerPoll", error);
}
};
-
- poll(); // Initial fetch
- this.tickerPollTimer = setInterval(poll, 2000); // Poll every 2 seconds
+ void poll();
+ this.tickerPollTimer = setInterval(poll, 2_000);
}
private startKlinePolling(interval: string): void {
if (this.klinePollTimers.has(interval)) return;
-
const poll = async () => {
try {
const ohlcv = await this.exchange.fetchOHLCV(this.marketSymbol, interval, undefined, 100);
const klines = (ohlcv as number[][])
- .filter((c) => Array.isArray(c) && c.length >= 6)
- .map((c) => this.mapOHLCVToKline([c[0], c[1], c[2], c[3], c[4], c[5]] as [number, number, number, number, number, number], interval));
-
- for (const listener of this.klineListeners) {
- if (listener.interval === interval) {
- listener.callback(klines);
- }
+ .filter((row) => Array.isArray(row) && row.length >= 6)
+ .map((row) => this.mapOHLCVToKline(row as [number, number, number, number, number, number], interval));
+ for (const { interval: key, callback } of this.klineListeners) {
+ if (key === interval) callback(klines);
}
} catch (error) {
- this.logger("klinePoll", error);
+ this.logger(`klinePoll:${interval}`, error);
}
};
-
- poll(); // Initial fetch
- this.klinePollTimers.set(interval, setInterval(poll, 5000)); // Poll every 5 seconds
+ void poll();
+ this.klinePollTimers.set(interval, setInterval(poll, 5_000));
}
- // Order management
+ // ---- Order actions -----------------------------------------------------
+
async createOrder(params: CreateOrderParams): Promise {
await this.ensureInitialized();
-
- // Only pass exchange-specific params in the last argument so we don't
- // override ccxt's internal request mapping (e.g. side mapping for Backpack).
const symbol = this.marketSymbol;
- const type = this.mapOrderTypeToCcxt(params.type);
+ const normalizedType = this.normalizeOrderType(params.type);
const side = params.side.toLowerCase();
const amount = params.quantity;
- const price = params.price;
+ let price = params.price;
const extraParams: Record = {};
- if (params.stopPrice !== undefined) extraParams.stopPrice = params.stopPrice;
- // Map GTX (post-only) to Backpack's postOnly boolean and use GTC as TIF
if (params.timeInForce === "GTX") {
extraParams.postOnly = true;
extraParams.timeInForce = "GTC";
- } else if (params.timeInForce !== undefined) {
- extraParams.timeInForce = params.timeInForce; // GTC, IOC, FOK
+ } else if (params.timeInForce) {
+ extraParams.timeInForce = params.timeInForce;
}
- // Reduce-only string boolean -> boolean per OpenAPI
if (params.reduceOnly !== undefined) {
extraParams.reduceOnly = params.reduceOnly === "true";
}
- if (params.closePosition !== undefined) {
- extraParams.closePosition = params.closePosition === "true";
+
+ let ccxtType: string;
+ if (normalizedType === "STOP_MARKET") {
+ ccxtType = "market";
+ price = undefined;
+ if (params.stopPrice !== undefined) {
+ extraParams.triggerPrice = params.stopPrice;
+ if (extraParams.triggerBy === undefined) {
+ extraParams.triggerBy = "MarkPrice";
+ }
+ }
+ } else if (normalizedType === "MARKET") {
+ ccxtType = "market";
+ price = undefined;
+ } else {
+ ccxtType = "limit";
}
- const order = await this.exchange.createOrder(
- symbol,
- type,
- side,
- amount,
- price,
- extraParams
- );
-
- return this.mapOrderToAsterOrder(order);
+ if (params.stopPrice !== undefined && normalizedType !== "STOP_MARKET") {
+ extraParams.stopPrice = params.stopPrice;
+ }
+
+ const order = await this.exchange.createOrder(symbol, ccxtType, side, amount, price, extraParams);
+ const mapped = this.mapRestOrder(order as CcxtOrder);
+ this.localOrders.set(mapped.orderId, mapped);
+ this.emitOrders();
+ return mapped;
}
async cancelOrder(params: { orderId: number | string }): Promise {
await this.exchange.cancelOrder(params.orderId.toString(), this.marketSymbol);
+ this.localOrders.delete(String(params.orderId));
+ this.emitOrders();
}
async cancelOrders(params: { orderIdList: Array }): Promise {
await Promise.all(
- params.orderIdList.map(orderId =>
- this.exchange.cancelOrder(orderId.toString(), this.marketSymbol)
- )
+ params.orderIdList.map((orderId) => this.exchange.cancelOrder(String(orderId), this.marketSymbol))
);
+ for (const orderId of params.orderIdList) {
+ this.localOrders.delete(String(orderId));
+ }
+ this.emitOrders();
}
async cancelAllOrders(): Promise {
try {
if (typeof (this.exchange as any).cancelAllOrders === "function") {
await (this.exchange as any).cancelAllOrders(this.marketSymbol);
+ this.localOrders.clear();
+ this.emitOrders();
return;
}
} catch {
// fall through to manual cancel
}
const open = await this.exchange.fetchOpenOrders(this.marketSymbol);
- for (const o of open) {
- await this.exchange.cancelOrder(o.id as string, this.marketSymbol);
+ for (const order of open) {
+ await this.exchange.cancelOrder(order.id as string, this.marketSymbol);
+ this.localOrders.delete(String(order.id));
}
+ this.emitOrders();
}
- // Mapping functions
+ // ---- Mapping helpers ---------------------------------------------------
+
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot {
return this.mapBalanceToAccountSnapshotWithPositions(balance, []);
}
private async fetchAccountSnapshot(): Promise {
await this.ensureInitialized();
- const balancePromise = this.exchange.fetchBalance();
- const positionsPromise = this.isContractMarket
- ? this.exchange.fetchPositions([this.marketSymbol]).catch((error: unknown) => {
- this.logger("fetchPositions", error);
- return [];
- })
- : Promise.resolve([]);
-
- const [balance, positions] = await Promise.all([balancePromise, positionsPromise]);
+ const [balance, positions] = await Promise.all([
+ this.exchange.fetchBalance(),
+ this.isContractMarket
+ ? this.exchange.fetchPositions([this.marketSymbol]).catch((error: unknown) => {
+ this.logger("fetchPositions", error);
+ return [];
+ })
+ : Promise.resolve([]),
+ ]);
return this.mapBalanceToAccountSnapshotWithPositions(balance, positions ?? []);
}
@@ -388,9 +425,8 @@ export class BackpackGateway {
const now = Date.now();
const assets = this.normalizeAssets(balance, now);
const positions = this.normalizePositions(rawPositions, now);
-
const totalWalletBalance = this.sumStrings(assets.map((asset) => asset.walletBalance));
- const totalUnrealizedProfit = this.sumStrings(positions.map((position) => position.unrealizedProfit ?? "0"));
+ const totalUnrealized = this.sumStrings(positions.map((position) => position.unrealizedProfit ?? "0"));
const availableBalance = this.sumStrings(assets.map((asset) => asset.availableBalance));
const snapshot: AsterAccountSnapshot = {
@@ -399,170 +435,75 @@ export class BackpackGateway {
canWithdraw: true,
updateTime: now,
totalWalletBalance,
- totalUnrealizedProfit,
+ totalUnrealizedProfit: totalUnrealized,
positions,
assets,
+ availableBalance,
+ maxWithdrawAmount: availableBalance,
};
- snapshot.availableBalance = availableBalance;
- snapshot.maxWithdrawAmount = availableBalance;
-
if (this.isContractMarket) {
- const totalMarginBalance = this.addStrings(totalWalletBalance, totalUnrealizedProfit);
+ const totalMarginBalance = this.addStrings(totalWalletBalance, totalUnrealized);
snapshot.totalMarginBalance = totalMarginBalance;
snapshot.totalCrossWalletBalance = totalWalletBalance;
- snapshot.totalCrossUnPnl = totalUnrealizedProfit;
+ snapshot.totalCrossUnPnl = totalUnrealized;
}
-
return snapshot;
}
private normalizeAssets(balance: Balances, now: number): AsterAccountSnapshot["assets"] {
const metaKeys = new Set(["free", "used", "total", "info", "timestamp", "datetime", "debt"]);
const assets: AsterAccountSnapshot["assets"] = [];
-
for (const [currency, value] of Object.entries(balance)) {
if (metaKeys.has(currency)) continue;
if (!value || typeof value !== "object") continue;
-
const walletBalance = this.toStringAmount((value as any).total ?? (value as any).free ?? "0");
const availableBalance = this.toStringAmount((value as any).free ?? "0");
-
- assets.push({
- asset: currency,
- walletBalance,
- availableBalance,
- updateTime: now,
- });
+ assets.push({ asset: currency, walletBalance, availableBalance, updateTime: now });
}
-
return assets;
}
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] {
if (!Array.isArray(rawPositions)) return [];
-
const positions: AsterAccountSnapshot["positions"] = [];
-
- for (const position of rawPositions) {
- const info = position?.info ?? position ?? {};
- const rawSymbol = position?.symbol ?? info.symbol ?? this.marketSymbol;
- const rawContracts = position?.contracts ?? info.netExposureQuantity;
- const derivedSide = (position?.side ?? info.side ?? this.deriveSideFromExposure(info)) ?? "long";
- const rawSide = derivedSide.toString().toLowerCase();
- const quantity = this.toNumber(rawContracts);
+ for (const raw of rawPositions) {
+ const info = raw?.info ?? raw ?? {};
+ const quantity = this.toNumber(raw?.contracts ?? info.netExposureQuantity ?? info.netQuantity);
if (!quantity) continue;
-
- const side = rawSide === "short" ? "short" : "long";
- const signedQuantity = side === "short" ? -Math.abs(quantity) : Math.abs(quantity);
-
- const normalized: AsterAccountPosition = {
- symbol: rawSymbol,
- positionAmt: signedQuantity.toString(),
- entryPrice: this.toStringAmount(position?.entryPrice ?? info.entryPrice ?? "0"),
- unrealizedProfit: this.toStringAmount(position?.unrealizedPnl ?? info.pnlUnrealized ?? "0"),
- positionSide: side === "short" ? "SHORT" : "LONG",
+ const sideRaw = String(raw?.side ?? info.side ?? this.deriveSideFromExposure(info)).toLowerCase();
+ const isShort = sideRaw.includes("short") || quantity < 0;
+ const positionAmt = isShort ? -Math.abs(quantity) : Math.abs(quantity);
+ const entryPrice = this.toStringAmount(raw?.entryPrice ?? info.entryPrice ?? "0");
+ const unrealized = this.toStringAmount(raw?.unrealizedPnl ?? info.pnlUnrealized ?? "0");
+ const markPrice = this.toOptionalString(raw?.markPrice ?? info.markPrice);
+ const liquidationPrice = this.toOptionalString(raw?.liquidationPrice ?? info.estLiquidationPrice);
+ const leverage = this.toOptionalString(raw?.leverage ?? info.leverage);
+ positions.push({
+ symbol: this.symbol,
+ positionAmt: positionAmt.toString(),
+ entryPrice,
+ unrealizedProfit: unrealized,
+ positionSide: "BOTH",
updateTime: now,
- };
-
- const markPrice = this.toOptionalString(position?.markPrice ?? info.markPrice);
- if (markPrice !== undefined) normalized.markPrice = markPrice;
-
- const liquidationPrice = this.toOptionalString(position?.liquidationPrice ?? info.estLiquidationPrice);
- if (liquidationPrice !== undefined) normalized.liquidationPrice = liquidationPrice;
-
- const initialMargin = this.toOptionalString(position?.initialMargin ?? info.initialMargin);
- if (initialMargin !== undefined) normalized.initialMargin = initialMargin;
-
- const maintMargin = this.toOptionalString(position?.maintenanceMargin ?? info.maintenanceMargin);
- if (maintMargin !== undefined) normalized.maintMargin = maintMargin;
-
- const leverage = this.toOptionalString(position?.leverage ?? info.leverage);
- if (leverage !== undefined) normalized.leverage = leverage;
-
- normalized.marginType = "CROSSED";
-
- positions.push(normalized);
+ markPrice,
+ liquidationPrice,
+ leverage,
+ marginType: "CROSSED",
+ });
}
-
return positions;
}
- private deriveSideFromExposure(info: Record): "long" | "short" | "flat" {
- const exposure = this.toNumber(info?.netExposureNotional ?? info?.netCost ?? info?.netQuantity);
- if (!exposure) return "flat";
- return exposure < 0 ? "short" : "long";
- }
-
- private toStringAmount(value: unknown): string {
- if (value === undefined || value === null) return "0";
- if (typeof value === "string") {
- if (value.trim() === "") return "0";
- return value;
- }
- if (typeof value === "number") {
- if (!Number.isFinite(value)) return "0";
- return value.toString();
- }
- return "0";
- }
-
- private toOptionalString(value: unknown): string | undefined {
- const normalized = this.toStringAmount(value);
- return normalized === "0" ? undefined : normalized;
- }
-
- private toNumber(value: unknown): number {
- const asString = this.toStringAmount(value);
- const parsed = Number(asString);
- if (!Number.isFinite(parsed)) return 0;
- return parsed;
- }
-
- private sumStrings(values: string[]): string {
- let total = 0;
- for (const value of values) {
- const parsed = Number(value);
- if (!Number.isFinite(parsed)) continue;
- total += parsed;
- }
- return total.toString();
- }
-
- private addStrings(a: string, b: string): string {
- const sum = Number(a) + Number(b);
- if (!Number.isFinite(sum)) return "0";
- return sum.toString();
- }
-
- private mapOrderToAsterOrder(order: CcxtOrder): AsterOrder {
- const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
- const mappedType = this.mapCcxtOrderTypeToAster(order.type);
- return {
- orderId: String(order.id ?? ""),
- clientOrderId: (order.clientOrderId as any as string) || "",
- symbol: order.symbol || this.marketSymbol,
- side,
- type: mappedType,
- status: (order.status as any as string) || "",
- price: order.price?.toString() || "0",
- origQty: order.amount?.toString() || "0",
- executedQty: order.filled?.toString() || "0",
- stopPrice: order.stopPrice?.toString() || "0",
- time: order.timestamp || Date.now(),
- updateTime: order.lastUpdateTimestamp || Date.now(),
- reduceOnly: false,
- closePosition: false,
- avgPrice: order.average?.toString(),
- cumQuote: order.cost?.toString(),
- };
- }
-
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth {
return {
lastUpdateId: orderbook.nonce || Date.now(),
- bids: (orderbook.bids || []).filter((t) => t && t.length >= 2).map(([price, amount]) => [String(price ?? 0), String(amount ?? 0)]),
- asks: (orderbook.asks || []).filter((t) => t && t.length >= 2).map(([price, amount]) => [String(price ?? 0), String(amount ?? 0)]),
+ bids: (orderbook.bids ?? [])
+ .filter((row) => row && row.length >= 2)
+ .map(([price, amount]) => [String(price ?? 0), String(amount ?? 0)]),
+ asks: (orderbook.asks ?? [])
+ .filter((row) => row && row.length >= 2)
+ .map(([price, amount]) => [String(price ?? 0), String(amount ?? 0)]),
eventTime: orderbook.timestamp,
};
}
@@ -570,21 +511,24 @@ export class BackpackGateway {
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker {
return {
symbol: ticker.symbol,
- lastPrice: ticker.last?.toString() || "0",
- openPrice: ticker.open?.toString() || "0",
- highPrice: ticker.high?.toString() || "0",
- lowPrice: ticker.low?.toString() || "0",
- volume: ticker.baseVolume?.toString() || "0",
- quoteVolume: ticker.quoteVolume?.toString() || "0",
+ lastPrice: ticker.last?.toString() ?? "0",
+ openPrice: ticker.open?.toString() ?? "0",
+ highPrice: ticker.high?.toString() ?? "0",
+ lowPrice: ticker.low?.toString() ?? "0",
+ volume: ticker.baseVolume?.toString() ?? "0",
+ quoteVolume: ticker.quoteVolume?.toString() ?? "0",
eventTime: ticker.timestamp,
};
}
- private mapOHLCVToKline(candle: [number, number, number, number, number, number], interval: string): AsterKline {
- const [timestamp, open, high, low, close, volume] = candle;
+ private mapOHLCVToKline(
+ candle: [number, number, number, number, number, number],
+ interval: string
+ ): AsterKline {
+ const [openTime, open, high, low, close, volume] = candle;
return {
- openTime: timestamp,
- closeTime: timestamp + this.getIntervalMs(interval),
+ openTime,
+ closeTime: openTime + this.intervalToMs(interval),
open: open.toString(),
high: high.toString(),
low: low.toString(),
@@ -594,60 +538,575 @@ export class BackpackGateway {
};
}
- private mapOrderTypeToCcxt(type: string): string {
- const typeMap: Record = {
- "LIMIT": "limit",
- "MARKET": "market",
- "STOP_MARKET": "stop",
- "TRAILING_STOP_MARKET": "trailing-stop",
+ private mapRestOrder(order: CcxtOrder): AsterOrder {
+ const info = (order.info ?? {}) as Record;
+ const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
+ let type = this.normalizeOrderType(order.type ?? (info.o as string));
+ if (
+ order.triggerPrice != null ||
+ info.triggerPrice != null ||
+ info.stopLossTriggerPrice != null ||
+ info.Y != null ||
+ info.y != null
+ ) {
+ type = "STOP_MARKET";
+ }
+ const status = this.normalizeStatus(order.status ?? (info.status as string));
+ const price = this.pickString([order.price, info.price, info.p]);
+ const quantity = this.pickString([order.amount, info.quantity, info.triggerQuantity, info.q, info.Y]);
+ const executed = this.pickString([order.filled, info.executedQuantity, info.executedBaseQuantity, info.z]);
+ const stopPrice = this.pickString([order.stopPrice, info.triggerPrice, info.stopLossTriggerPrice, info.P]);
+ const avgPrice = this.pickString([order.average, info.avgPrice, info.L]);
+ const cumQuote = this.pickString([order.cost, info.executedQuoteQuantity, info.Z]);
+ const timestamp = order.timestamp ?? Date.now();
+ const reduceOnly = Boolean(order.reduceOnly ?? info.reduceOnly ?? info.r ?? false);
+
+ return {
+ orderId: String(order.id ?? ""),
+ clientOrderId: (order.clientOrderId as any as string) || "",
+ symbol: this.symbol,
+ side,
+ type,
+ status,
+ price,
+ origQty: quantity,
+ executedQty: executed,
+ stopPrice,
+ time: timestamp,
+ updateTime: order.lastUpdateTimestamp ?? timestamp,
+ reduceOnly,
+ closePosition: false,
+ avgPrice,
+ cumQuote,
};
- return typeMap[type] || "limit";
}
- private mapCcxtOrderTypeToAster(type: string | undefined): OrderType {
- const typeMap: Record = {
- "limit": "LIMIT",
- "market": "MARKET",
- "stop": "STOP_MARKET",
- "trailing-stop": "TRAILING_STOP_MARKET",
+ private mapWsOrder(data: Record): AsterOrder {
+ const sideRaw = String(data.S ?? "").toUpperCase();
+ const side: "BUY" | "SELL" = sideRaw === "BID" ? "BUY" : "SELL";
+ const triggerPresent = data.P != null || data.B != null;
+ const type = triggerPresent ? "STOP_MARKET" : String(data.o ?? "LIMIT").toUpperCase();
+ const status = this.normalizeStatus(data.X as string);
+ const price = this.pickString([data.p, data.P, "0"]);
+ const quantity = this.pickString([data.q, data.Y]);
+ const executed = this.pickString([data.z, data.l, data.Z]);
+ const stopPrice = this.pickString([data.P]);
+ const timestampMicro = Number(data.E ?? data.T ?? Date.now() * 1000);
+ const timestamp = Number.isFinite(timestampMicro) ? Math.floor(timestampMicro / 1000) : Date.now();
+ const reduceOnly = Boolean(data.r);
+ const avgPrice = this.pickString([data.L]);
+ const cumQuote = this.pickString([data.Z]);
+
+ return {
+ orderId: String(data.i ?? ""),
+ clientOrderId: data.c ? String(data.c) : "",
+ symbol: this.symbol,
+ side,
+ type,
+ status,
+ price,
+ origQty: quantity,
+ executedQty: executed,
+ stopPrice,
+ time: timestamp,
+ updateTime: timestamp,
+ reduceOnly,
+ closePosition: false,
+ avgPrice,
+ cumQuote,
};
- return type ? (typeMap[type] ?? "LIMIT") : "LIMIT";
}
- private getIntervalMs(interval: string): number {
- const intervalMap: Record = {
- "1m": 60 * 1000,
- "5m": 5 * 60 * 1000,
- "15m": 15 * 60 * 1000,
- "1h": 60 * 60 * 1000,
- "4h": 4 * 60 * 60 * 1000,
- "1d": 24 * 60 * 60 * 1000,
- };
- return intervalMap[interval] || 60 * 1000;
+ private emitOrders(): void {
+ const snapshot = Array.from(this.localOrders.values()).map((order) => ({ ...order }));
+ for (const listener of this.orderListeners) {
+ try {
+ listener(snapshot);
+ } catch (error) {
+ this.logger("emitOrders", error);
+ }
+ }
}
- // Cleanup
- destroy(): void {
- if (this.accountPollTimer) {
- clearInterval(this.accountPollTimer);
- this.accountPollTimer = null;
+ private mapWsPosition(data: Record): AsterAccountPosition | null {
+ const quantityRaw = data.q ?? data.Q;
+ const qty = Number(this.toStringAmount(quantityRaw));
+ if (!Number.isFinite(qty)) return null;
+ const entryPrice = this.toStringAmount(data.B ?? data.entryPrice ?? "0");
+ const unrealized = this.toStringAmount(data.P ?? "0");
+ const markPrice = this.toOptionalString(data.M ?? data.markPrice);
+ const leverage = this.toOptionalString(data.f ?? data.leverage);
+ const updateTime = Number(data.E ?? data.T ?? Date.now());
+ const isShort = qty < 0;
+ const positionAmt = isShort ? (-Math.abs(qty)).toString() : Math.abs(qty).toString();
+
+ return {
+ symbol: this.symbol,
+ positionAmt,
+ entryPrice,
+ unrealizedProfit: unrealized,
+ positionSide: "BOTH",
+ updateTime: Number.isFinite(updateTime) ? Math.floor(updateTime / 1000) : Date.now(),
+ markPrice,
+ leverage,
+ marginType: "CROSSED",
+ };
+ }
+
+ private mergeWsPosition(position: AsterAccountPosition): void {
+ const snapshot: AsterAccountSnapshot = this.lastBalanceSnapshot
+ ? {
+ ...this.lastBalanceSnapshot,
+ positions: this.lastBalanceSnapshot.positions ? [...this.lastBalanceSnapshot.positions] : [],
+ }
+ : {
+ canTrade: true,
+ canDeposit: true,
+ canWithdraw: true,
+ updateTime: Date.now(),
+ totalWalletBalance: "0",
+ totalUnrealizedProfit: "0",
+ positions: [],
+ assets: [],
+ availableBalance: "0",
+ maxWithdrawAmount: "0",
+ };
+
+ const positions = snapshot.positions ?? [];
+ const idx = positions.findIndex((p) => p.symbol === position.symbol);
+ if (this.isNearlyZero(position.positionAmt)) {
+ if (idx >= 0) positions.splice(idx, 1);
+ } else if (idx >= 0) {
+ positions[idx] = position;
+ } else {
+ positions.push(position);
+ }
+
+ snapshot.positions = positions;
+ snapshot.totalUnrealizedProfit = this.sumStrings(
+ positions.map((p) => p.unrealizedProfit ?? "0")
+ );
+ snapshot.updateTime = Date.now();
+ this.lastBalanceSnapshot = snapshot;
+ if (process.env.BACKPACK_DEBUG === "1") {
+ console.debug("[BackpackGateway] positions", snapshot.positions);
+ }
+ this.emitAccount(snapshot);
+ }
+
+ private emitAccount(snapshot: AsterAccountSnapshot): void {
+ for (const listener of this.accountListeners) {
+ try {
+ listener(snapshot);
+ } catch (error) {
+ this.logger("emitAccount", error);
+ }
}
- if (this.orderPollTimer) {
- clearInterval(this.orderPollTimer);
- this.orderPollTimer = null;
+ }
+
+ // ---- WebSocket ---------------------------------------------------------
+
+ private ensurePrivateSocket(): void {
+ if (!this.apiKey || !this.apiSecret) return;
+ if (!this.marketId) return;
+ if (this.ws && (this.ws.readyState === WebSocketCtor.OPEN || this.ws.readyState === WebSocketCtor.CONNECTING)) {
+ return;
}
- if (this.depthPollTimer) {
- clearInterval(this.depthPollTimer);
- this.depthPollTimer = null;
+ if (this.wsConnecting) return;
+ this.connectPrivateSocket();
+ }
+
+ private connectPrivateSocket(): void {
+ this.wsConnecting = true;
+ this.detachWebSocket();
+ const socket = new WebSocketCtor("wss://ws.backpack.exchange");
+ this.ws = socket;
+
+ if ("addEventListener" in socket && typeof socket.addEventListener === "function") {
+ socket.addEventListener("open", this.handleWsOpen);
+ socket.addEventListener("close", this.handleWsClose);
+ socket.addEventListener("error", this.handleWsError);
+ socket.addEventListener("message", this.handleWsMessage);
+ this.wsCleanup = () => {
+ socket.removeEventListener("open", this.handleWsOpen);
+ socket.removeEventListener("close", this.handleWsClose);
+ socket.removeEventListener("error", this.handleWsError);
+ socket.removeEventListener("message", this.handleWsMessage);
+ };
+ } else if ("on" in socket && typeof (socket as any).on === "function") {
+ const nodeSocket = socket as any;
+ const off =
+ typeof nodeSocket.off === "function"
+ ? (event: string, handler: (...args: any[]) => void) => nodeSocket.off(event, handler)
+ : (event: string, handler: (...args: any[]) => void) => nodeSocket.removeListener(event, handler);
+ const wrappedMessage = (data: any) => this.handleWsMessage({ data });
+ nodeSocket.on("open", this.handleWsOpen);
+ nodeSocket.on("close", this.handleWsClose);
+ nodeSocket.on("error", this.handleWsError);
+ nodeSocket.on("message", wrappedMessage);
+ this.wsCleanup = () => {
+ off("open", this.handleWsOpen);
+ off("close", this.handleWsClose);
+ off("error", this.handleWsError);
+ off("message", wrappedMessage);
+ };
+ } else {
+ (socket as any).onopen = this.handleWsOpen;
+ (socket as any).onclose = this.handleWsClose;
+ (socket as any).onerror = this.handleWsError;
+ (socket as any).onmessage = this.handleWsMessage;
+ this.wsCleanup = () => {
+ (socket as any).onopen = null;
+ (socket as any).onclose = null;
+ (socket as any).onerror = null;
+ (socket as any).onmessage = null;
+ };
}
- if (this.tickerPollTimer) {
- clearInterval(this.tickerPollTimer);
- this.tickerPollTimer = null;
+ }
+
+ private handleWsOpen = (): void => {
+ this.wsConnecting = false;
+ this.wsReady = true;
+ this.startPing();
+ void this.resubscribeAllTopics();
+ };
+
+ private handleWsClose = (event: any): void => {
+ if (this.wsCleanup) {
+ try {
+ this.wsCleanup();
+ } catch {
+ /* ignore */
+ }
+ this.wsCleanup = null;
}
-
- for (const timer of this.klinePollTimers.values()) {
- clearInterval(timer);
+ this.wsConnecting = false;
+ this.wsReady = false;
+ this.ws = null;
+ this.stopPing();
+ this.scheduleReconnect();
+ };
+
+ private handleWsError = (_event: any): void => {
+ /* swallow non-fatal errors; reconnect handled by close */
+ };
+
+ private handleWsMessage = (event: any): void => {
+ try {
+ const payload = event?.data ?? event;
+ const raw =
+ typeof payload === "string"
+ ? payload
+ : Buffer.isBuffer(payload)
+ ? payload.toString("utf8")
+ : payload?.toString?.() ?? "";
+ if (!raw) return;
+ const message = JSON.parse(raw) as Record;
+ const stream = String(message.stream ?? "");
+ const data = message.data as Record | undefined;
+ if (process.env.BACKPACK_DEBUG === "1") {
+ console.debug("[BackpackGateway] wsMessage", {
+ stream,
+ data,
+ result: message.result,
+ symbol: this.symbol,
+ marketId: this.marketId,
+ });
}
- this.klinePollTimers.clear();
+ if (!data) return;
+ if (stream.startsWith("account.orderUpdate")) {
+ this.handleWsOrder(data);
+ } else if (stream.startsWith("account.positionUpdate")) {
+ this.handleWsPosition(data);
+ } else if (stream === "ping") {
+ this.sendPong();
+ }
+ } catch (error) {
+ this.logger("wsMessageParse", error);
+ }
+ };
+
+ private handleWsOrder(data: Record): void {
+ const mapped = this.mapWsOrder(data);
+ if (process.env.BACKPACK_DEBUG === "1") {
+ console.debug("[BackpackGateway] wsOrder", mapped);
+ }
+ const status = mapped.status;
+ const id = mapped.orderId;
+ if (this.isTerminalStatus(status)) {
+ if (this.localOrders.delete(id)) {
+ this.emitOrders();
+ }
+ return;
+ }
+ this.localOrders.set(id, mapped);
+ this.emitOrders();
+ }
+
+ private handleWsPosition(data?: Record): void {
+ if (data) {
+ const mapped = this.mapWsPosition(data);
+ if (mapped) {
+ this.mergeWsPosition(mapped);
+ return;
+ }
+ }
+ void this.refreshAccountSnapshot();
+ }
+
+ private async refreshAccountSnapshot(): Promise {
+ try {
+ const snapshot = await this.fetchAccountSnapshot();
+ this.lastBalanceSnapshot = snapshot;
+ this.emitAccount(snapshot);
+ } catch (error) {
+ this.logger("refreshAccountSnapshot", error);
+ }
+ }
+
+ private async resubscribeAllTopics(): Promise {
+ if (!this.ws || this.ws.readyState !== WebSocketCtor.OPEN) return;
+ if (!this.wsTopics.size) return;
+ for (const topic of this.wsTopics) {
+ await this.sendWsSubscription([topic], "SUBSCRIBE");
+ }
+ }
+
+ private subscribePrivateTopic(topic: string): void {
+ if (!topic || !this.apiKey || !this.apiSecret) return;
+ if (this.wsTopics.has(topic)) return;
+ this.wsTopics.add(topic);
+ if (this.wsReady && this.ws && this.ws.readyState === WebSocketCtor.OPEN) {
+ void this.sendWsSubscription([topic], "SUBSCRIBE");
+ }
+ }
+
+ private async sendWsSubscription(topics: string[], method: "SUBSCRIBE" | "UNSUBSCRIBE"): Promise {
+ if (!this.ws || this.ws.readyState !== WebSocketCtor.OPEN) return;
+ if (!topics.length) return;
+ try {
+ const timestamp = Date.now().toString();
+ const payload = `instruction=${method.toLowerCase()}×tamp=${timestamp}&window=${this.wsWindow}`;
+ const signature = await this.createSignature(payload);
+ const message = {
+ method,
+ params: topics,
+ signature: [this.apiKey, signature, timestamp, this.wsWindow],
+ };
+ if (process.env.BACKPACK_DEBUG === "1") {
+ this.logger("wsSubscribe", message);
+ }
+ this.ws.send(JSON.stringify(message));
+ } catch (error) {
+ this.logger("wsSubscribe", error);
+ }
+ }
+
+ private async subscribeOnce(context: string, topicFactory: () => string): Promise {
+ try {
+ await this.ensureInitialized();
+ if (!this.apiKey || !this.apiSecret) return;
+ this.ensurePrivateSocket();
+ const baseTopic = topicFactory();
+ const idTopic = baseTopic === "account.orderUpdate" ? this.buildOrderTopic() : this.buildPositionTopic();
+ this.subscribePrivateTopic(baseTopic);
+ this.subscribePrivateTopic(idTopic);
+ } catch (error) {
+ this.logger(context, error);
+ }
+ }
+
+ private startPing(): void {
+ this.stopPing();
+ if (!this.ws || typeof this.ws.send !== "function") return;
+ this.wsPingTimer = setInterval(() => {
+ try {
+ if (this.ws && this.ws.readyState === WebSocketCtor.OPEN) {
+ this.ws.send(JSON.stringify({ method: "PING" }));
+ }
+ } catch (error) {
+ this.logger("wsPing", error);
+ }
+ }, WS_PING_INTERVAL);
+ }
+
+ private stopPing(): void {
+ if (this.wsPingTimer) {
+ clearInterval(this.wsPingTimer);
+ this.wsPingTimer = null;
+ }
+ }
+
+ private sendPong(): void {
+ try {
+ if (this.ws && this.ws.readyState === WebSocketCtor.OPEN) {
+ this.ws.send(JSON.stringify({ method: "PONG" }));
+ }
+ } catch (error) {
+ this.logger("wsPong", error);
+ }
+ }
+
+ private scheduleReconnect(): void {
+ if (this.wsReconnectTimer) return;
+ this.wsReconnectTimer = setTimeout(() => {
+ this.wsReconnectTimer = null;
+ this.ensurePrivateSocket();
+ }, WS_RECONNECT_DELAY);
+ }
+
+ private detachWebSocket(): void {
+ if (this.wsCleanup) {
+ try {
+ this.wsCleanup();
+ } catch {
+ /* ignore */
+ }
+ this.wsCleanup = null;
+ }
+ if (this.ws) {
+ try {
+ this.ws.close();
+ } catch {
+ /* ignore */
+ }
+ }
+ }
+
+ private buildOrderTopic(symbolOnly = false): string {
+ const id = this.marketId || this.symbol.replace(/[^A-Z0-9_]/g, "_");
+ return symbolOnly ? "account.orderUpdate" : TRIGGER_TOPICS.orders(id);
+ }
+
+ private buildPositionTopic(symbolOnly = false): string {
+ const id = this.marketId || this.symbol.replace(/[^A-Z0-9_]/g, "_");
+ return symbolOnly ? "account.positionUpdate" : TRIGGER_TOPICS.positions(id);
+ }
+
+ private async createSignature(payload: string): Promise {
+ if (!this.apiSecret) {
+ throw new Error("Backpack API secret is required for websocket authentication");
+ }
+ let secretBytes: Buffer;
+ try {
+ secretBytes = Buffer.from(this.apiSecret, "base64");
+ } catch {
+ secretBytes = Buffer.alloc(0);
+ }
+ if (!secretBytes.length) {
+ throw new Error("Backpack API secret must be base64 encoded 32-byte key");
+ }
+ if (secretBytes.length < 32) {
+ throw new Error("Backpack API secret must be base64 encoded 32-byte key");
+ }
+ const seed = secretBytes.length === 32 ? secretBytes : secretBytes.subarray(0, 32);
+ const signature = await sign(new TextEncoder().encode(payload), seed);
+ return Buffer.from(signature).toString("base64");
+ }
+
+ // ---- Utility methods ---------------------------------------------------
+
+ private normalizeTimeframe(interval: string): string {
+ const map: Record = {
+ "1m": "1m",
+ "5m": "5m",
+ "15m": "15m",
+ "1h": "1h",
+ "4h": "4h",
+ "1d": "1d",
+ };
+ return map[interval] ?? "1m";
+ }
+
+ private intervalToMs(interval: string): number {
+ const map: Record = {
+ "1m": 60_000,
+ "5m": 300_000,
+ "15m": 900_000,
+ "1h": 3_600_000,
+ "4h": 14_400_000,
+ "1d": 86_400_000,
+ };
+ return map[interval] ?? 60_000;
+ }
+
+ private deriveSideFromExposure(info: Record): "long" | "short" | "flat" {
+ const exposure = this.toNumber(info.netExposureNotional ?? info.netCost ?? info.netQuantity);
+ if (!exposure) return "flat";
+ return exposure < 0 ? "short" : "long";
+ }
+
+ private toStringAmount(value: unknown): string {
+ if (value === undefined || value === null) return "0";
+ if (typeof value === "string") {
+ return value.trim() === "" ? "0" : value;
+ }
+ if (typeof value === "number") {
+ return Number.isFinite(value) ? value.toString() : "0";
+ }
+ return "0";
+ }
+
+ private toOptionalString(value: unknown): string | undefined {
+ const result = this.toStringAmount(value);
+ return result === "0" ? undefined : result;
+ }
+
+ private toNumber(value: unknown): number {
+ const parsed = Number(this.toStringAmount(value));
+ return Number.isFinite(parsed) ? parsed : 0;
+ }
+
+ private isNearlyZero(value: string, epsilon = 1e-9): boolean {
+ return Math.abs(Number(value)) < epsilon;
+ }
+
+ private sumStrings(values: string[]): string {
+ let total = 0;
+ for (const value of values) {
+ const parsed = Number(value);
+ if (Number.isFinite(parsed)) total += parsed;
+ }
+ return total.toString();
+ }
+
+ private addStrings(a: string, b: string): string {
+ const sum = Number(a) + Number(b);
+ return Number.isFinite(sum) ? sum.toString() : "0";
+ }
+
+ private normalizeStatus(status?: string): string {
+ if (!status) return "UNKNOWN";
+ const key = status.replace(/[^a-zA-Z]/g, "").toUpperCase();
+ return (
+ ORDER_STATUS_MAP[key] ??
+ status
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
+ .replace(/\s+/g, "_")
+ .toUpperCase()
+ );
+ }
+
+ private isTerminalStatus(status?: string): boolean {
+ if (!status) return false;
+ const normalized = status.toUpperCase();
+ return normalized === "FILLED" || normalized === "CANCELLED" || normalized === "EXPIRED" || normalized === "REJECTED" || normalized === "TRIGGER_FAILED";
+ }
+
+ private normalizeOrderType(type?: string): string {
+ if (!type) return "LIMIT";
+ const upper = type.toUpperCase();
+ if (upper.includes("STOP")) return "STOP_MARKET";
+ if (upper === "MARKET" || upper === "LIMIT") return upper;
+ return upper;
+ }
+
+ private pickString(values: Array): string {
+ for (const value of values) {
+ if (value === undefined || value === null) continue;
+ const asString = this.toStringAmount(value);
+ if (asString !== "0" || Number(value) === 0) return asString;
+ }
+ return "0";
}
}
diff --git a/src/exchanges/backpack/order.ts b/src/exchanges/backpack/order.ts
index 0e31e8e..25768bf 100644
--- a/src/exchanges/backpack/order.ts
+++ b/src/exchanges/backpack/order.ts
@@ -19,9 +19,6 @@ function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent):
if (intent.reduceOnly !== undefined) {
params.reduceOnly = toStringBoolean(intent.reduceOnly);
}
- if (intent.closePosition !== undefined) {
- params.closePosition = toStringBoolean(intent.closePosition);
- }
return params;
}
@@ -85,4 +82,3 @@ export async function createClosePositionOrder(intent: ClosePositionIntent): Pro
);
return intent.adapter.createOrder(params);
}
-