feat: enhance WebSocket documentation and improve LighterGateway position handling for better account management

This commit is contained in:
discountry
2025-11-06 18:47:29 +08:00
parent 078b201d15
commit 0f70c6b6aa
2 changed files with 466 additions and 208 deletions
+253 -158
View File
@@ -1,18 +1,16 @@
WebSocket # WebSocket
This page will help you get started with zkLighter WebSocket server. URL: `wss://mainnet.zklighter.elliot.ai/stream`
Connection
URL: wss://mainnet.zklighter.elliot.ai/stream
You can directly connect to the WebSocket server using wscat: You can directly connect to the WebSocket server using wscat:
```
wscat -c 'wss://mainnet.zklighter.elliot.ai/stream' wscat -c 'wss://mainnet.zklighter.elliot.ai/stream'
Send Tx ```
You can send transactions using the websocket as follows: You can send transactions using the websocket as follows:
JSON ```
{ {
"type": "jsonapi/sendtx", "type": "jsonapi/sendtx",
"data": { "data": {
@@ -20,32 +18,33 @@ JSON
"tx_info": ... "tx_info": ...
} }
} }
The tx_type options can be found in the SignerClient file, while tx_info can be generated using the sign methods in the SignerClient. ```
Example: ws_send_tx.py
Send Batch Tx The _tx\_type_ options can be found in the [SignerClient](https://github.com/elliottech/lighter-python/blob/main/lighter/signer_client.py) file, while _tx\_info_ can be generated using the sign methods in the SignerClient.
Example: [ws\_send\_tx.py](https://github.com/elliottech/lighter-python/blob/main/examples/ws_send_tx.py)
You can send batch transactions to execute up to 50 transactions in a single message. You can send batch transactions to execute up to 50 transactions in a single message.
JSON ```
{ {
"type": "jsonapi/sendtxbatch", "type": "jsonapi/sendtxbatch",
"data": { "data": {
"tx_types": [INTEGER], "tx_types": "[INTEGER]",
"tx_infos": [tx_info] "tx_infos": "[tx_info]"
} }
} }
The tx_type options can be found in the SignerClient file, while tx_info can be generated using the sign methods in the SignerClient. ```
Example: ws_send_batch_tx.py
Types The _tx\_type_ options can be found in the [SignerClient](https://github.com/elliottech/lighter-python/blob/main/lighter/signer_client.py) file, while _tx\_info_ can be generated using the sign methods in the SignerClient.
Example: [ws\_send\_batch\_tx.py](https://github.com/elliottech/lighter-python/blob/main/examples/ws_send_batch_tx.py)
We first need to define some types that appear often in the JSONs. We first need to define some types that appear often in the JSONs.
Transaction JSON ```
JSON
Transaction = { Transaction = {
"hash": STRING, "hash": STRING,
"type": INTEGER, "type": INTEGER,
@@ -63,10 +62,12 @@ Transaction = {
"sequence_index": INTEGER, "sequence_index": INTEGER,
"parent_hash": STRING "parent_hash": STRING
} }
```
Example: Example:
JSON ```
{ {
"hash": "0xabc123456789def", "hash": "0xabc123456789def",
"type": 15, "type": 15,
@@ -84,11 +85,12 @@ JSON
"sequence_index": 5678, "sequence_index": 5678,
"parent_hash": "0xparenthash123456" "parent_hash": "0xparenthash123456"
} }
Used in: Transaction, Executed Transaction, Account Tx. ```
Order JSON
JSON
Used in: [Transaction](https://apibetadocs.lighter.xyz/docs/websocket-reference#transaction), [Executed Transaction](https://apibetadocs.lighter.xyz/docs/websocket-reference#executed-transaction), [Account Tx](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-tx).
```
Order = { Order = {
"order_index": INTEGER, "order_index": INTEGER,
"client_order_index": INTEGER, "client_order_index": INTEGER,
@@ -122,11 +124,12 @@ Order = {
"block_height": INTEGER, "block_height": INTEGER,
"timestamp": INTEGER, "timestamp": INTEGER,
} }
Used in: Account Market, Account All Orders, Account Orders. ```
Trade JSON
JSON
Used in: [Account Market](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-market), [Account All Orders](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-all-orders), [Account Orders](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-orders).
```
Trade = { Trade = {
"trade_id": INTEGER, "trade_id": INTEGER,
"tx_hash": STRING, "tx_hash": STRING,
@@ -153,10 +156,12 @@ Trade = {
"maker_initial_margin_fraction_before": INTEGER (omitted when zero), "maker_initial_margin_fraction_before": INTEGER (omitted when zero),
"maker_position_sign_changed": BOOL (omitted when false), "maker_position_sign_changed": BOOL (omitted when false),
} }
```
Example: Example:
JSON ```
{ {
"trade_id": 401, "trade_id": 401,
"tx_hash": "0xabc123456789", "tx_hash": "0xabc123456789",
@@ -179,11 +184,12 @@ JSON
"maker_entry_quote_before":"3075.396750", "maker_entry_quote_before":"3075.396750",
"maker_initial_margin_fraction_before":400 "maker_initial_margin_fraction_before":400
} }
Used in: Trade, Account All, Account Market, Account All Trades. ```
Position JSON
JSON
Used in: [Trade](https://apibetadocs.lighter.xyz/docs/websocket-reference#trade), [Account All](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-all), [Account Market](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-market), [Account All Trades](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-all-trades).
```
Position = { Position = {
"market_id": INTEGER, "market_id": INTEGER,
"symbol": STRING, "symbol": STRING,
@@ -202,10 +208,12 @@ Position = {
"margin_mode": INT, "margin_mode": INT,
"allocated_margin": STRING, "allocated_margin": STRING,
} }
```
Example: Example:
JSON ```
{ {
"market_id": 101, "market_id": 101,
"symbol": "BTC-USD", "symbol": "BTC-USD",
@@ -224,49 +232,56 @@ JSON
"margin_mode": 1, "margin_mode": 1,
"allocated_margin": "46342", "allocated_margin": "46342",
} }
Used in: Account All, Account Market, Account All Positions. ```
PoolShares JSON
JSON
Used in: [Account All](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-all), [Account Market](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-market), [Account All Positions](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-all-positions).
```
PoolShares = { PoolShares = {
"public_pool_index": INTEGER, "public_pool_index": INTEGER,
"shares_amount": INTEGER, "shares_amount": INTEGER,
"entry_usdc": STRING "entry_usdc": STRING
} }
```
Example: Example:
JSON ```
{ {
"public_pool_index": 1, "public_pool_index": 1,
"shares_amount": 100, "shares_amount": 100,
"entry_usdc": "1000.00" "entry_usdc": "1000.00"
} }
Used in: Account All, Account All Positions. ```
Used in: [Account All](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-all), [Account All Positions](https://apibetadocs.lighter.xyz/docs/websocket-reference#account-all-positions).
Channels
Order Book
The order book channel sends the new ask and bid orders for the given market. The order book channel sends the new ask and bid orders for the given market.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "order_book/{MARKET_INDEX}" "channel": "order_book/{MARKET_INDEX}"
} }
Example Subscription ```
JSON
**Example Subscription**
```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "order_book/0" "channel": "order_book/0"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "order_book:{MARKET_INDEX}", "channel": "order_book:{MARKET_INDEX}",
"offset": INTEGER, "offset": INTEGER,
@@ -288,10 +303,12 @@ JSON
}, },
"type": "update/order_book" "type": "update/order_book"
} }
Example Response ```
JSON
**Example Response**
```
{ {
"channel": "order_book:0", "channel": "order_book:0",
"offset": 41692864, "offset": 41692864,
@@ -313,35 +330,42 @@ JSON
}, },
"type": "update/order_book" "type": "update/order_book"
} }
Market Stats ```
The market stats channel sends the market stat data for the given market. The market stats channel sends the market stat data for the given market.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "market_stats/{MARKET_INDEX}" "channel": "market_stats/{MARKET_INDEX}"
} }
```
or or
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "market_stats/all" "channel": "market_stats/all"
} }
Example Subscription ```
JSON
**Example Subscription**
```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "market_stats/0" "channel": "market_stats/0"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "market_stats:{MARKET_INDEX}", "channel": "market_stats:{MARKET_INDEX}",
"market_stats": { "market_stats": {
@@ -361,10 +385,12 @@ JSON
}, },
"type": "update/market_stats" "type": "update/market_stats"
} }
Example Response ```
JSON
**Example Response**
```
{ {
"channel": "market_stats:0", "channel": "market_stats:0",
"market_stats": { "market_stats": {
@@ -384,37 +410,44 @@ JSON
}, },
"type": "update/market_stats" "type": "update/market_stats"
} }
Trade ```
The trade channel sends the new trade data for the given market. The trade channel sends the new trade data for the given market.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "trade/{MARKET_INDEX}" "channel": "trade/{MARKET_INDEX}"
} }
Example Subscription ```
JSON
**Example Subscription**
```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "trade/0" "channel": "trade/0"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "trade:{MARKET_INDEX}", "channel": "trade:{MARKET_INDEX}",
"trades": [Trade] "trades": [Trade]
], ],
"type": "update/trade" "type": "update/trade"
} }
Example Response ```
JSON
**Example Response**
```
{ {
"channel": "trade:0", "channel": "trade:0",
"trades": [ "trades": [
@@ -437,27 +470,32 @@ JSON
], ],
"type": "update/trade" "type": "update/trade"
} }
Account All ```
The account all channel sends specific account market data for all markets. The account all channel sends specific account market data for all markets.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "account_all/{ACCOUNT_ID}" "channel": "account_all/{ACCOUNT_ID}"
} }
Example Subscription ```
JSON
**Example Subscription**
```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "account_all/1" "channel": "account_all/1"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"account": INTEGER, "account": INTEGER,
"channel": "account_all:{ACCOUNT_ID}", "channel": "account_all:{ACCOUNT_ID}",
@@ -491,10 +529,12 @@ JSON
}, },
"type": "update/account_all" "type": "update/account_all"
} }
Example Response ```
JSON
**Example Response**
```
{ {
"account": 10, "account": 10,
"channel": "account_all:10", "channel": "account_all:10",
@@ -574,29 +614,34 @@ JSON
}, },
"type": "update/account" "type": "update/account"
} }
Account Market ```
The account market channel sends specific account market data for a market. The account market channel sends specific account market data for a market.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "account_market/{MARKET_ID}/{ACCOUNT_ID}", "channel": "account_market/{MARKET_ID}/{ACCOUNT_ID}",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Example Subscription ```
JSON
**Example Subscription**
```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "account_market/0/40", "channel": "account_market/0/40",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"account": INTEGER, "account": INTEGER,
"channel": "account_all/{MARKET_ID}/{ACCOUNT_ID}", "channel": "account_all/{MARKET_ID}/{ACCOUNT_ID}",
@@ -614,27 +659,32 @@ JSON
"trades": [Trade], "trades": [Trade],
"type": "update/account_market" "type": "update/account_market"
} }
Account Stats ```
The account stats channel sends account stats data for the specific account. The account stats channel sends account stats data for the specific account.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "user_stats/{ACCOUNT_ID}" "channel": "user_stats/{ACCOUNT_ID}"
} }
Example Subscription ```
JSON
**Example Subscription**
```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "user_stats/0" "channel": "user_stats/0"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "user_stats:{ACCOUNT_ID}", "channel": "user_stats:{ACCOUNT_ID}",
"stats": { "stats": {
@@ -664,10 +714,12 @@ JSON
}, },
"type": "update/user_stats" "type": "update/user_stats"
} }
Example Response ```
JSON
**Example Response**
```
{ {
"channel": "user_stats:10", "channel": "user_stats:10",
"stats": { "stats": {
@@ -696,57 +748,65 @@ JSON
}, },
"type": "update/user_stats" "type": "update/user_stats"
} }
Transaction ```
The transaction channel sends all new transactions. The transaction channel sends all new transactions.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "transaction" "channel": "transaction"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "transaction", "channel": "transaction",
"txs": [Transaction], "txs": [Transaction],
"type": "update/transaction" "type": "update/transaction"
} }
Executed Transaction ```
The structure is the same as with Transaction channel. But this channel sends only executed transactions.
JSON
The structure is the same as with [Transaction](#transaction) channel. But this channel sends only executed transactions.
```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "executed_transaction" "channel": "executed_transaction"
} }
Account Tx ```
The structure is the same as with Transaction channel. But this channel sends only transactions related to a specific account.
JSON
The structure is the same as with [Transaction](#transaction) channel. But this channel sends only transactions related to a specific account.
```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "account_tx/{ACCOUNT_ID}", "channel": "account_tx/{ACCOUNT_ID}",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Account All Orders ```
The account all orders channel sends data about all the orders of an account. The account all orders channel sends data about all the orders of an account.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "account_all_orders/{ACCOUNT_ID}", "channel": "account_all_orders/{ACCOUNT_ID}",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "account_all_orders:{ACCOUNT_ID}", "channel": "account_all_orders:{ACCOUNT_ID}",
"orders": { "orders": {
@@ -754,38 +814,44 @@ JSON
}, },
"type": "update/account_all_orders" "type": "update/account_all_orders"
} }
Height ```
Blockchain height updates Blockchain height updates
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "height", "channel": "height",
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "height", "channel": "height",
"height": INTEGER, "height": INTEGER,
"type": "update/height" "type": "update/height"
} }
Pool data ```
Provides data about pool activities: trades, orders, positions, shares and funding histories. Provides data about pool activities: trades, orders, positions, shares and funding histories.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "pool_data/{ACCOUNT_ID}", "channel": "pool_data/{ACCOUNT_ID}",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "pool_data:{ACCOUNT_ID}", "channel": "pool_data:{ACCOUNT_ID}",
"account": INTEGER, "account": INTEGER,
@@ -804,20 +870,23 @@ JSON
}, },
"type": "subscribed/pool_data" "type": "subscribed/pool_data"
} }
Pool info ```
Provides information about pools. Provides information about pools.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "pool_info/{ACCOUNT_ID}", "channel": "pool_info/{ACCOUNT_ID}",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "pool_info:{ACCOUNT_ID}", "channel": "pool_info:{ACCOUNT_ID}",
"pool_info": { "pool_info": {
@@ -842,20 +911,23 @@ JSON
}, },
"type": "subscribed/pool_info" "type": "subscribed/pool_info"
} }
Notification ```
Provides notifications received by an account. Notifications can be of three kinds: liquidation, deleverage, or announcement. Each kind has a different content structure. Provides notifications received by an account. Notifications can be of three kinds: liquidation, deleverage, or announcement. Each kind has a different content structure.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "notification/{ACCOUNT_ID}", "channel": "notification/{ACCOUNT_ID}",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "notification:{ACCOUNT_ID}", "channel": "notification:{ACCOUNT_ID}",
"notifs": [ "notifs": [
@@ -872,10 +944,12 @@ JSON
], ],
"type": "subscribed/notification" "type": "subscribed/notification"
} }
Liquidation Notification Content ```
JSON
**Liquidation Notification Content**
```
{ {
"id": STRING, "id": STRING,
"is_ask": BOOL, "is_ask": BOOL,
@@ -886,10 +960,12 @@ JSON
"timestamp": INTEGER, "timestamp": INTEGER,
"avg_price": STRING "avg_price": STRING
} }
Deleverage Notification Content ```
JSON
**Deleverage Notification Content**
```
{ {
"id": STRING, "id": STRING,
"usdc_amount": STRING, "usdc_amount": STRING,
@@ -898,19 +974,23 @@ JSON
"settlement_price": STRING, "settlement_price": STRING,
"timestamp": INTEGER "timestamp": INTEGER
} }
Announcement Notification Content ```
JSON
**Announcement Notification Content**
```
{ {
"title": STRING, "title": STRING,
"content": STRING, "content": STRING,
"created_at": INTEGER "created_at": INTEGER
} }
Example response ```
JSON
**Example response**
```
{ {
"channel": "notification:12345", "channel": "notification:12345",
"notifs": [ "notifs": [
@@ -953,20 +1033,23 @@ JSON
], ],
"type": "update/notification" "type": "update/notification"
} }
Account Orders ```
The account all orders channel sends data about the orders of an account on a certain market. The account all orders channel sends data about the orders of an account on a certain market.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "account_orders/{MARKET_INDEX}/{ACCOUNT_ID}", "channel": "account_orders/{MARKET_INDEX}/{ACCOUNT_ID}",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"account": {ACCOUNT_INDEX}, "account": {ACCOUNT_INDEX},
"channel": "account_orders:{MARKET_INDEX}", "channel": "account_orders:{MARKET_INDEX}",
@@ -976,20 +1059,23 @@ JSON
}, },
"type": "update/account_orders" "type": "update/account_orders"
} }
Account All Trades ```
The account all trades channel sends data about all the trades of an account. The account all trades channel sends data about all the trades of an account.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "account_all_trades/{ACCOUNT_ID}", "channel": "account_all_trades/{ACCOUNT_ID}",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "account_all_trades:{ACCOUNT_ID}", "channel": "account_all_trades:{ACCOUNT_ID}",
"trades": { "trades": {
@@ -1001,20 +1087,23 @@ JSON
"daily_volume": FLOAT, "daily_volume": FLOAT,
"type": "update/account_all_trades" "type": "update/account_all_trades"
} }
Account All Positions ```
The account all orders channel sends data about all the order of an account. The account all orders channel sends data about all the order of an account.
JSON ```
{ {
"type": "subscribe", "type": "subscribe",
"channel": "account_all_positions/{ACCOUNT_ID}", "channel": "account_all_positions/{ACCOUNT_ID}",
"auth": "{AUTH_TOKEN}" "auth": "{AUTH_TOKEN}"
} }
Response Structure ```
JSON
**Response Structure**
```
{ {
"channel": "account_all_positions:{ACCOUNT_ID}", "channel": "account_all_positions:{ACCOUNT_ID}",
"positions": { "positions": {
@@ -1022,4 +1111,10 @@ JSON
}, },
"shares": [PoolShares], "shares": [PoolShares],
"type": "update/account_all_positions" "type": "update/account_all_positions"
} }
```
Updated 30 days ago
* * *
+213 -50
View File
@@ -118,6 +118,7 @@ const DEFAULT_TICKER_POLL_MS = 3000;
const DEFAULT_KLINE_POLL_MS = 15000; const DEFAULT_KLINE_POLL_MS = 15000;
const WS_HEARTBEAT_INTERVAL_MS = 5_000; const WS_HEARTBEAT_INTERVAL_MS = 5_000;
const WS_STALE_TIMEOUT_MS = 20_000; const WS_STALE_TIMEOUT_MS = 20_000;
const POSITION_EPSILON = 1e-12;
const RESOLUTION_MS: Record<string, number> = { const RESOLUTION_MS: Record<string, number> = {
"1m": 60_000, "1m": 60_000,
@@ -128,6 +129,8 @@ const RESOLUTION_MS: Record<string, number> = {
"1d": 86_400_000, "1d": 86_400_000,
}; };
const TERMINAL_ORDER_STATUSES = new Set(["filled", "canceled", "cancelled", "expired"]);
export interface LighterGatewayOptions { export interface LighterGatewayOptions {
symbol: string; // display symbol used by strategy logging symbol: string; // display symbol used by strategy logging
marketSymbol?: string; // actual Lighter order book symbol (e.g., BTC) marketSymbol?: string; // actual Lighter order book symbol (e.g., BTC)
@@ -418,6 +421,14 @@ export class LighterGateway {
} }
if (details) { if (details) {
this.accountDetails = details; this.accountDetails = details;
if (Object.prototype.hasOwnProperty.call(details, "positions")) {
const initialPositions = this.normalizePositions(details.positions);
if (initialPositions.length) {
this.replacePositions(initialPositions);
} else if (this.isEmptyPositionsPayload(details.positions)) {
this.positions = [];
}
}
this.emitAccount(); this.emitAccount();
} else { } else {
// Fallback: emit an empty account snapshot so strategies can proceed // Fallback: emit an empty account snapshot so strategies can proceed
@@ -657,77 +668,225 @@ export class LighterGateway {
private handleAccountAll(message: any): void { private handleAccountAll(message: any): void {
if (!message) return; if (!message) return;
// account_all may be partial; merge provided markets into existing positions
if (Object.prototype.hasOwnProperty.call(message, "positions")) { if (Object.prototype.hasOwnProperty.call(message, "positions")) {
const positionsObject = message.positions ?? {}; const positionsObject = message.positions ?? {};
const incoming: LighterPosition[] = (Array.isArray(positionsObject) const incoming = this.normalizePositions(positionsObject);
? (positionsObject as LighterPosition[]) if (!incoming.length && this.isEmptyPositionsPayload(positionsObject)) {
: (Object.values(positionsObject) as LighterPosition[])) as LighterPosition[]; this.positions = [];
} else if (incoming.length) {
const byMarket = new Map<number, LighterPosition>(); this.mergePositions(incoming);
for (const p of this.positions ?? []) {
const mid = Number(p.market_id);
if (Number.isFinite(mid)) byMarket.set(mid, p);
} }
for (const p of incoming) {
const mid = Number(p.market_id);
if (!Number.isFinite(mid)) continue;
const sign = Number(p.sign ?? 0);
const size = Number(p.position ?? 0);
if (sign === 0 || Math.abs(size) < 1e-12) {
byMarket.delete(mid);
} else {
byMarket.set(mid, p);
}
}
this.positions = Array.from(byMarket.values());
} }
this.emitAccount(); this.emitAccount();
} }
private handleAccountMarket(message: any): void { private handleAccountMarket(message: any): void {
if (!message) return; if (!message) return;
const type = typeof message.type === "string" ? message.type : "";
const position: LighterPosition | undefined = message.position as LighterPosition | undefined; const position: LighterPosition | undefined = message.position as LighterPosition | undefined;
if (!position || !Number.isFinite(Number(position.market_id))) return; const channelMarketId = this.extractMarketIdFromChannel(message.channel);
const marketId = Number(position.market_id); if (position && Number.isFinite(Number(position.market_id))) {
const sign = Number(position.sign ?? 0); this.mergePositions([position]);
const size = Number(position.position ?? 0); } else if (type === "subscribed/account_market" && channelMarketId != null) {
const shouldRemove = sign === 0 || Math.abs(size) < 1e-12; this.removePositionsForMarkets([channelMarketId]);
if (shouldRemove) { }
this.positions = (this.positions ?? []).filter((p) => Number(p.market_id) !== marketId); if (Array.isArray(message.orders) && message.orders.length) {
} else { const marketId = Number(position?.market_id ?? channelMarketId ?? this.marketId ?? NaN);
let updated = false; this.applyOrderList(message.orders, Number.isFinite(marketId) ? Number(marketId) : null, type === "subscribed/account_market");
this.positions = (this.positions ?? []).map((p) => { } else if (type === "subscribed/account_market" && channelMarketId != null) {
if (Number(p.market_id) === marketId) { this.clearOrdersForMarket(channelMarketId);
updated = true; this.emitOrders();
return position;
}
return p;
});
if (!updated) this.positions.push(position);
} }
this.emitAccount(); this.emitAccount();
} }
private handleAccountOrders(message: any): void { private handleAccountOrders(message: any): void {
if (!message) return; if (!message) return;
const snapshot = message.type === "subscribed/account_all_orders";
const ordersObject = message.orders ?? {}; const ordersObject = message.orders ?? {};
const buckets = Object.values(ordersObject) as unknown[]; this.applyOrderBuckets(ordersObject, snapshot);
const allOrders: LighterOrder[] = buckets.flatMap((entry) => Array.isArray(entry) ? (entry as LighterOrder[]) : []); }
const terminalStatuses = new Set(["filled", "canceled", "cancelled", "expired"]);
for (const order of allOrders) { private normalizePositions(source: unknown): LighterPosition[] {
const key = String(order.order_index ?? order.order_id ?? order.client_order_index ?? ""); if (!source) return [];
const status = (order.status ?? "").toLowerCase(); if (Array.isArray(source)) {
if (!key) continue; return source.filter((entry): entry is LighterPosition => this.isPosition(entry));
if (terminalStatuses.has(status)) { }
this.orderMap.delete(key); if (isPlainObject(source)) {
return Object.values(source).filter((entry): entry is LighterPosition => this.isPosition(entry));
}
if (this.isPosition(source)) return [source];
return [];
}
private isPosition(value: unknown): value is LighterPosition {
return typeof value === "object" && value != null && Number.isFinite(Number((value as LighterPosition).market_id));
}
private mergePositions(updates: LighterPosition[]): void {
if (!updates.length) return;
const byMarket = new Map<number, LighterPosition>();
for (const existing of this.positions ?? []) {
const mid = Number(existing.market_id);
if (Number.isFinite(mid)) {
byMarket.set(mid, existing);
}
}
for (const update of updates) {
const marketId = Number(update.market_id);
if (!Number.isFinite(marketId)) continue;
if (this.shouldRemovePosition(update)) {
byMarket.delete(marketId);
} else { } else {
this.orderMap.set(key, order); byMarket.set(marketId, update);
}
}
this.positions = Array.from(byMarket.values());
}
private replacePositions(positions: LighterPosition[]): void {
if (!positions.length) {
this.positions = [];
return;
}
const filtered = this.filterPositions(positions);
this.positions = filtered;
}
private filterPositions(positions: LighterPosition[]): LighterPosition[] {
const byMarket = new Map<number, LighterPosition>();
for (const entry of positions) {
const marketId = Number(entry.market_id);
if (!Number.isFinite(marketId)) continue;
if (this.shouldRemovePosition(entry)) {
byMarket.delete(marketId);
} else {
byMarket.set(marketId, entry);
}
}
return Array.from(byMarket.values());
}
private shouldRemovePosition(position: LighterPosition): boolean {
const size = Number(position.position ?? 0);
return !Number.isFinite(size) || Math.abs(size) < POSITION_EPSILON;
}
private removePositionsForMarkets(markets: number[]): void {
if (!markets.length) return;
const targets = new Set(markets.filter((value) => Number.isFinite(value)).map((value) => Number(value)));
if (!targets.size) return;
this.positions = (this.positions ?? []).filter((position) => !targets.has(Number(position.market_id)));
}
private applyOrderBuckets(rawOrders: unknown, snapshot: boolean): void {
const ordersObject = isPlainObject(rawOrders) ? (rawOrders as Record<string, unknown>) : {};
const marketKeys = Object.keys(ordersObject);
if (snapshot && marketKeys.length === 0) {
this.orderMap.clear();
this.orders = [];
this.emitOrders();
return;
}
if (snapshot) {
this.orderMap.clear();
}
for (const [market, bucket] of Object.entries(ordersObject)) {
const marketId = Number(market);
const normalized = this.normalizeOrders(bucket);
if (Number.isFinite(marketId)) {
this.clearOrdersForMarket(marketId);
}
if (!normalized.length) continue;
for (const order of normalized) {
this.applyOrderUpdate(order);
} }
} }
this.orders = Array.from(this.orderMap.values()); this.orders = Array.from(this.orderMap.values());
const mapped = toOrders(this.displaySymbol, this.orders); this.emitOrders();
this.ordersEvent.emit(mapped); }
private normalizeOrders(source: unknown): LighterOrder[] {
if (!source) return [];
if (Array.isArray(source)) {
return (source as unknown[]).filter((entry): entry is LighterOrder => this.isOrder(entry));
}
if (isPlainObject(source) && this.isOrder(source)) {
return [source];
}
return [];
}
private isOrder(value: unknown): value is LighterOrder {
return typeof value === "object" && value != null;
}
private applyOrderList(rawOrders: unknown, marketId: number | null, snapshot: boolean): void {
const orders = this.normalizeOrders(rawOrders);
if (marketId != null) {
this.clearOrdersForMarket(marketId);
} else if (snapshot) {
this.orderMap.clear();
}
for (const order of orders) {
this.applyOrderUpdate(order);
}
this.orders = Array.from(this.orderMap.values());
this.emitOrders();
}
private applyOrderUpdate(order: LighterOrder): void {
const key = String(order.order_index ?? order.order_id ?? order.client_order_index ?? "");
if (!key) return;
const status = (order.status ?? "").toLowerCase();
if (TERMINAL_ORDER_STATUSES.has(status)) {
this.orderMap.delete(key);
return;
}
if (order.client_order_index != null || order.order_index != null) {
for (const [existingKey, existingOrder] of Array.from(this.orderMap.entries())) {
if (existingKey === key) continue;
const sameOrderIndex =
order.order_index != null &&
existingOrder.order_index != null &&
Number(existingOrder.order_index) === Number(order.order_index);
const sameClientIndex =
order.client_order_index != null &&
existingOrder.client_order_index != null &&
Number(existingOrder.client_order_index) === Number(order.client_order_index);
if (sameOrderIndex || sameClientIndex) {
this.orderMap.delete(existingKey);
}
}
}
this.orderMap.set(key, order);
}
private clearOrdersForMarket(marketId: number): void {
const normalized = Number(marketId);
if (!Number.isFinite(normalized)) return;
for (const [key, existing] of Array.from(this.orderMap.entries())) {
if (Number(existing.market_index) === normalized) {
this.orderMap.delete(key);
}
}
}
private extractMarketIdFromChannel(channel: unknown): number | null {
if (typeof channel !== "string") return null;
const match = channel.match(/account_market:(\d+)/);
if (match && match[1]) {
const value = Number(match[1]);
return Number.isFinite(value) ? value : null;
}
return null;
}
private isEmptyPositionsPayload(value: unknown): boolean {
if (value == null) return true;
if (Array.isArray(value)) return value.length === 0;
if (isPlainObject(value)) return Object.keys(value).length === 0;
return false;
} }
private emitDepth(): void { private emitDepth(): void {
@@ -1050,3 +1209,7 @@ function decimalsToStep(decimals: number): number {
const step = Number(`1e-${decimals}`); const step = Number(`1e-${decimals}`);
return Number.isFinite(step) ? step : Math.pow(10, -decimals); return Number.isFinite(step) ? step : Math.pow(10, -decimals);
} }
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value != null && !Array.isArray(value);
}