openbroker 1.0.72 → 1.0.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +59 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/auto/cli.ts +4 -2
- package/scripts/auto/examples/price-alert.ts +96 -0
- package/scripts/auto/runtime.ts +130 -5
- package/scripts/auto/types.ts +22 -1
- package/scripts/core/ws.ts +283 -0
- package/scripts/info/account.ts +69 -27
- package/scripts/info/fees.ts +7 -1
- package/scripts/info/fills.ts +14 -5
- package/scripts/info/order-status.ts +7 -3
- package/scripts/info/orders.ts +15 -2
- package/scripts/info/positions.ts +11 -3
- package/scripts/info/spot.ts +12 -7
package/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring and
|
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Requires Node.js 22+, network access to api.hyperliquid.xyz
|
|
6
6
|
homepage: https://www.npmjs.com/package/openbroker
|
|
7
|
-
metadata: {"author": "monemetrics", "version": "1.0.
|
|
7
|
+
metadata: {"author": "monemetrics", "version": "1.0.75", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
|
|
8
8
|
allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_twap ob_twap_cancel ob_twap_status ob_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -127,8 +127,10 @@ HYPERLIQUID_NETWORK=mainnet
|
|
|
127
127
|
```bash
|
|
128
128
|
openbroker account # Balance, equity, margin
|
|
129
129
|
openbroker account --orders # Include open orders
|
|
130
|
+
openbroker account --address 0xabc... # Look up another account
|
|
130
131
|
openbroker positions # Open positions with PnL
|
|
131
132
|
openbroker positions --coin ETH # Specific coin
|
|
133
|
+
openbroker positions --address 0xabc... # Another account's positions
|
|
132
134
|
```
|
|
133
135
|
|
|
134
136
|
### Funding Rates
|
|
@@ -164,6 +166,7 @@ openbroker search --query ETH --type perp # ETH perps only
|
|
|
164
166
|
openbroker spot # Show all spot markets
|
|
165
167
|
openbroker spot --coin PURR # Show PURR market info
|
|
166
168
|
openbroker spot --balances # Show your spot balances
|
|
169
|
+
openbroker spot --balances --address 0xabc... # Another account's spot balances
|
|
167
170
|
openbroker spot --top 20 # Top 20 by volume
|
|
168
171
|
```
|
|
169
172
|
|
|
@@ -172,6 +175,7 @@ openbroker spot --top 20 # Top 20 by volume
|
|
|
172
175
|
openbroker fills # Recent fills
|
|
173
176
|
openbroker fills --coin ETH # ETH fills only
|
|
174
177
|
openbroker fills --coin BTC --side buy --top 50
|
|
178
|
+
openbroker fills --address 0xabc... # Another account's fills
|
|
175
179
|
```
|
|
176
180
|
|
|
177
181
|
### Order History
|
|
@@ -181,17 +185,20 @@ openbroker orders --open # Currently open orders only
|
|
|
181
185
|
openbroker orders --open --coin ETH # Open orders for a specific coin
|
|
182
186
|
openbroker orders --coin ETH --status filled
|
|
183
187
|
openbroker orders --top 50
|
|
188
|
+
openbroker orders --address 0xabc... --open # Another account's open orders
|
|
184
189
|
```
|
|
185
190
|
|
|
186
191
|
### Order Status
|
|
187
192
|
```bash
|
|
188
193
|
openbroker order-status --oid 123456789 # Check specific order
|
|
189
194
|
openbroker order-status --oid 0x1234... # By client order ID
|
|
195
|
+
openbroker order-status --oid 123456789 --address 0xabc... # On another account
|
|
190
196
|
```
|
|
191
197
|
|
|
192
198
|
### Fee Schedule
|
|
193
199
|
```bash
|
|
194
200
|
openbroker fees # Fee tier, rates, and volume
|
|
201
|
+
openbroker fees --address 0xabc... # Another account's fees
|
|
195
202
|
```
|
|
196
203
|
|
|
197
204
|
### Candle Data (OHLCV)
|
|
@@ -521,11 +528,11 @@ To view bundled examples and their config schemas:
|
|
|
521
528
|
openbroker auto examples # List examples with config fields
|
|
522
529
|
```
|
|
523
530
|
|
|
524
|
-
Available examples: `dca`, `grid`, `funding-arb`, `mm-spread`, `mm-maker`
|
|
531
|
+
Available examples: `dca`, `grid`, `funding-arb`, `mm-spread`, `mm-maker`, `price-alert`
|
|
525
532
|
|
|
526
533
|
### How Automations Work
|
|
527
534
|
|
|
528
|
-
An automation is a `.ts` file that exports a default function. The function receives an `AutomationAPI` with the full Hyperliquid client, typed event subscriptions, persistent state, and a logger. The runtime
|
|
535
|
+
An automation is a `.ts` file that exports a default function. The function receives an `AutomationAPI` with the full Hyperliquid client, typed event subscriptions, persistent state, and a logger. The runtime connects a WebSocket for real-time price and order events, with REST polling every 30s as a heartbeat for position/margin data. Use `--no-ws` to disable WebSocket and fall back to pure REST polling (every 10s).
|
|
529
536
|
|
|
530
537
|
### Writing an Automation
|
|
531
538
|
|
|
@@ -773,18 +780,66 @@ api.on('margin_warning', async ({ marginUsedPct, equity }) => {
|
|
|
773
780
|
});
|
|
774
781
|
```
|
|
775
782
|
|
|
783
|
+
#### `order_update` — Real-time order lifecycle (WebSocket)
|
|
784
|
+
Fires instantly when any order changes status: `open`, `filled`, `canceled`, `triggered`, `rejected`, `marginCanceled`, `liquidatedCanceled`, `badAloPxRejected`, and 20+ other statuses. Requires WebSocket (enabled by default).
|
|
785
|
+
|
|
786
|
+
**Payload:** `{ coin: string, oid: number, side: 'buy' | 'sell', size: number, price: number, origSize: number, status: string, statusTimestamp: number }`
|
|
787
|
+
|
|
788
|
+
**Example:**
|
|
789
|
+
```typescript
|
|
790
|
+
api.on('order_update', async ({ coin, oid, status, side, size, price }) => {
|
|
791
|
+
if (status === 'filled') {
|
|
792
|
+
api.log.info(`Order ${oid} filled: ${side} ${size} ${coin} @ $${price}`);
|
|
793
|
+
} else if (status === 'canceled' || status.includes('Rejected')) {
|
|
794
|
+
api.log.warn(`Order ${oid} ${status}: ${coin}`);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
#### `liquidation` — Liquidation alert (WebSocket only)
|
|
800
|
+
Fires when the account is liquidated. This event is **only available via WebSocket** — there is no REST polling equivalent.
|
|
801
|
+
|
|
802
|
+
**Payload:** `{ lid: number, liquidator: string, liquidatedUser: string, liquidatedNtlPos: number, liquidatedAccountValue: number }`
|
|
803
|
+
|
|
804
|
+
**Example:**
|
|
805
|
+
```typescript
|
|
806
|
+
api.on('liquidation', async ({ liquidatedNtlPos, liquidatedAccountValue }) => {
|
|
807
|
+
await api.publish(
|
|
808
|
+
`LIQUIDATED: $${liquidatedNtlPos.toFixed(2)} notional, account value: $${liquidatedAccountValue.toFixed(2)}`,
|
|
809
|
+
{ name: 'liquidation-alert' },
|
|
810
|
+
);
|
|
811
|
+
});
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### WebSocket Real-Time Data
|
|
815
|
+
|
|
816
|
+
Automations use **WebSocket by default** for real-time market and account events. The runtime subscribes to:
|
|
817
|
+
- **allMids** — price updates for all assets (drives `price_change` events in real-time)
|
|
818
|
+
- **orderUpdates** — order lifecycle events (drives `order_update` and `order_filled`)
|
|
819
|
+
- **userFills** — trade fill details with PnL and fees
|
|
820
|
+
- **userEvents** — liquidation alerts, funding payments, system cancellations
|
|
821
|
+
|
|
822
|
+
REST polling continues as a **heartbeat** (every 60s by default) for position/margin/funding events that aren't covered by WebSocket. If the WebSocket connection fails, the runtime falls back to full REST polling (every 10s) automatically.
|
|
823
|
+
|
|
824
|
+
To disable WebSocket (pure REST polling):
|
|
825
|
+
```bash
|
|
826
|
+
openbroker auto run my-strategy.ts --no-ws
|
|
827
|
+
```
|
|
828
|
+
|
|
776
829
|
### Choosing the Right Event — Quick Guide
|
|
777
830
|
|
|
778
831
|
| Use case | Best event | Why |
|
|
779
832
|
|----------|-----------|-----|
|
|
780
833
|
| Alert when price crosses a fixed level | `tick` | Fires every poll — no minimum change threshold |
|
|
781
|
-
| React to price momentum/volatility | `price_change` |
|
|
834
|
+
| React to price momentum/volatility | `price_change` | Real-time via WebSocket, provides relative change data |
|
|
782
835
|
| Funding rate strategy | `funding_update` | Gives annualized rate directly |
|
|
783
836
|
| Auto TP/SL on new positions | `position_opened` | Fires exactly when a new position appears |
|
|
784
837
|
| Log when positions close | `position_closed` | Fires when position disappears |
|
|
785
838
|
| Track position scaling | `position_changed` | Fires on size changes only |
|
|
786
839
|
| Risk management — PnL spikes | `pnl_threshold` | Only fires on large moves (≥5% of position value) |
|
|
787
840
|
| Risk management — margin | `margin_warning` | Fires at 80%+ margin usage |
|
|
841
|
+
| React instantly to order fills/rejects | `order_update` | Real-time via WebSocket — sub-second latency |
|
|
842
|
+
| Liquidation alerts | `liquidation` | WebSocket only — no REST equivalent |
|
|
788
843
|
| Periodic task (DCA, rebalance) | `api.every(ms, fn)` | Better than tick for longer intervals |
|
|
789
844
|
|
|
790
845
|
### Client Methods Available
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/scripts/auto/cli.ts
CHANGED
|
@@ -104,10 +104,11 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
|
|
|
104
104
|
const scriptPath = exampleName ? resolveExamplePath(exampleName) : resolveScriptPath(scriptName!);
|
|
105
105
|
const dryRun = args.dry === true;
|
|
106
106
|
const verbose = args.verbose === true;
|
|
107
|
-
const
|
|
107
|
+
const useWebSocket = args['no-ws'] !== true;
|
|
108
|
+
const pollIntervalMs = args.poll ? parseInt(String(args.poll), 10) : undefined;
|
|
108
109
|
const id = args.id ? String(args.id) : undefined;
|
|
109
110
|
|
|
110
|
-
if (isNaN(pollIntervalMs) || pollIntervalMs < 1000) {
|
|
111
|
+
if (pollIntervalMs !== undefined && (isNaN(pollIntervalMs) || pollIntervalMs < 1000)) {
|
|
111
112
|
console.error('Error: --poll must be at least 1000ms');
|
|
112
113
|
process.exit(1);
|
|
113
114
|
}
|
|
@@ -118,6 +119,7 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
|
|
|
118
119
|
dryRun,
|
|
119
120
|
verbose,
|
|
120
121
|
pollIntervalMs,
|
|
122
|
+
useWebSocket,
|
|
121
123
|
initialState: Object.keys(initialState).length > 0 ? initialState : undefined,
|
|
122
124
|
});
|
|
123
125
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Price Alert — Real-time price monitoring via WebSocket
|
|
2
|
+
// Showcases WebSocket-driven price_change and order_update events
|
|
3
|
+
|
|
4
|
+
import type { AutomationAPI, AutomationConfig } from '../types.js';
|
|
5
|
+
|
|
6
|
+
export const config: AutomationConfig = {
|
|
7
|
+
description: 'Real-time price alerts via WebSocket — log price moves and order updates',
|
|
8
|
+
fields: {
|
|
9
|
+
coin: { type: 'string', description: 'Asset to monitor', default: 'BTC' },
|
|
10
|
+
threshold: { type: 'number', description: 'Min price change % to alert on', default: 0.1 },
|
|
11
|
+
above: { type: 'number', description: 'Alert when price goes above this level (0 = disabled)', default: 0 },
|
|
12
|
+
below: { type: 'number', description: 'Alert when price goes below this level (0 = disabled)', default: 0 },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function priceAlert(api: AutomationAPI) {
|
|
17
|
+
const COIN = api.state.get<string>('coin', 'BTC')!;
|
|
18
|
+
const THRESHOLD = api.state.get<number>('threshold', 0.1)!;
|
|
19
|
+
const ABOVE = api.state.get<number>('above', 0)!;
|
|
20
|
+
const BELOW = api.state.get<number>('below', 0)!;
|
|
21
|
+
|
|
22
|
+
let alertCount = 0;
|
|
23
|
+
let lastAlertPrice = 0;
|
|
24
|
+
let aboveTriggered = false;
|
|
25
|
+
let belowTriggered = false;
|
|
26
|
+
|
|
27
|
+
api.onStart(() => {
|
|
28
|
+
api.log.info(`Monitoring ${COIN} via WebSocket`);
|
|
29
|
+
api.log.info(`Threshold: ${THRESHOLD}% change`);
|
|
30
|
+
if (ABOVE > 0) api.log.info(`Alert above: $${ABOVE}`);
|
|
31
|
+
if (BELOW > 0) api.log.info(`Alert below: $${BELOW}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Real-time price changes via WebSocket
|
|
35
|
+
api.on('price_change', ({ coin, oldPrice, newPrice, changePct }) => {
|
|
36
|
+
if (coin !== COIN) return;
|
|
37
|
+
|
|
38
|
+
// Threshold alerts — fires when move exceeds configured %
|
|
39
|
+
if (Math.abs(changePct) >= THRESHOLD) {
|
|
40
|
+
const dir = changePct > 0 ? 'UP' : 'DOWN';
|
|
41
|
+
api.log.info(`${COIN} ${dir} ${changePct.toFixed(3)}%: $${oldPrice.toFixed(2)} -> $${newPrice.toFixed(2)}`);
|
|
42
|
+
alertCount++;
|
|
43
|
+
lastAlertPrice = newPrice;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Level alerts — fires once when price crosses a level, resets when it crosses back
|
|
47
|
+
if (ABOVE > 0) {
|
|
48
|
+
if (newPrice >= ABOVE && !aboveTriggered) {
|
|
49
|
+
aboveTriggered = true;
|
|
50
|
+
api.log.info(`${COIN} ABOVE $${ABOVE}: now $${newPrice.toFixed(2)}`);
|
|
51
|
+
api.publish(`${COIN} broke above $${ABOVE} — now $${newPrice.toFixed(2)}`, { name: 'price-alert' });
|
|
52
|
+
} else if (newPrice < ABOVE) {
|
|
53
|
+
aboveTriggered = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (BELOW > 0) {
|
|
58
|
+
if (newPrice <= BELOW && !belowTriggered) {
|
|
59
|
+
belowTriggered = true;
|
|
60
|
+
api.log.info(`${COIN} BELOW $${BELOW}: now $${newPrice.toFixed(2)}`);
|
|
61
|
+
api.publish(`${COIN} dropped below $${BELOW} — now $${newPrice.toFixed(2)}`, { name: 'price-alert' });
|
|
62
|
+
} else if (newPrice > BELOW) {
|
|
63
|
+
belowTriggered = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Real-time order lifecycle via WebSocket
|
|
69
|
+
api.on('order_update', ({ coin, oid, side, size, price, status }) => {
|
|
70
|
+
if (status === 'filled') {
|
|
71
|
+
api.log.info(`ORDER FILLED: ${side.toUpperCase()} ${size} ${coin} @ $${price.toFixed(2)} (oid: ${oid})`);
|
|
72
|
+
} else if (status === 'canceled' || status.includes('Canceled') || status.includes('Rejected')) {
|
|
73
|
+
api.log.warn(`ORDER ${status.toUpperCase()}: ${side} ${size} ${coin} @ $${price.toFixed(2)} (oid: ${oid})`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Liquidation alerts via WebSocket
|
|
78
|
+
api.on('liquidation', ({ liquidatedNtlPos, liquidatedAccountValue }) => {
|
|
79
|
+
api.log.error(`LIQUIDATION: $${liquidatedNtlPos.toFixed(2)} notional, account value: $${liquidatedAccountValue.toFixed(2)}`);
|
|
80
|
+
api.publish(
|
|
81
|
+
`LIQUIDATED: $${liquidatedNtlPos.toFixed(2)} notional, account value: $${liquidatedAccountValue.toFixed(2)}`,
|
|
82
|
+
{ name: 'liquidation-alert' },
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Periodic summary via REST heartbeat
|
|
87
|
+
api.on('tick', ({ pollCount }) => {
|
|
88
|
+
if (pollCount % 10 === 0 && alertCount > 0) {
|
|
89
|
+
api.log.info(`Summary: ${alertCount} alerts fired, last price: $${lastAlertPrice.toFixed(2)}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
api.onStop(() => {
|
|
94
|
+
api.log.info(`Stopped. Total alerts: ${alertCount}`);
|
|
95
|
+
});
|
|
96
|
+
}
|
package/scripts/auto/runtime.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Automation Runtime — loads scripts, polls market data, dispatches events
|
|
2
|
+
// Supports real-time WebSocket feeds with REST polling as fallback heartbeat
|
|
2
3
|
|
|
3
4
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
5
|
import path from 'path';
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
roundPrice, roundSize, sleep, normalizeCoin,
|
|
10
11
|
formatUsd, formatPercent, annualizeFundingRate,
|
|
11
12
|
} from '../core/utils.js';
|
|
13
|
+
import { WebSocketManager } from '../core/ws.js';
|
|
12
14
|
import { AutomationEventBus } from './events.js';
|
|
13
15
|
import { loadAutomation } from './loader.js';
|
|
14
16
|
import { registerAutomation, unregisterAutomation, markAutomationError, getRegisteredAutomations as getRegisteredFromFile } from './registry.js';
|
|
@@ -233,6 +235,13 @@ export interface RuntimeOptions {
|
|
|
233
235
|
hooksToken?: string;
|
|
234
236
|
/** Pre-seed state before the factory function runs (e.g. from --set key=value) */
|
|
235
237
|
initialState?: Record<string, unknown>;
|
|
238
|
+
/**
|
|
239
|
+
* Enable WebSocket for real-time events (allMids, orderUpdates, userFills, userEvents).
|
|
240
|
+
* When enabled, REST polling interval is relaxed to a heartbeat (default 60s).
|
|
241
|
+
* Falls back gracefully to polling if WebSocket connection fails.
|
|
242
|
+
* @default true
|
|
243
|
+
*/
|
|
244
|
+
useWebSocket?: boolean;
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
/** Registry of all running automations */
|
|
@@ -254,12 +263,16 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
254
263
|
scriptPath,
|
|
255
264
|
dryRun = false,
|
|
256
265
|
verbose = false,
|
|
257
|
-
pollIntervalMs = 10_000,
|
|
258
266
|
gatewayPort,
|
|
259
267
|
hooksToken,
|
|
260
268
|
initialState,
|
|
269
|
+
useWebSocket = true,
|
|
261
270
|
} = options;
|
|
262
271
|
|
|
272
|
+
// When WebSocket is enabled, REST poll becomes a heartbeat (30s default)
|
|
273
|
+
// When disabled, use the original 10s polling interval
|
|
274
|
+
const pollIntervalMs = options.pollIntervalMs ?? (useWebSocket ? 30_000 : 10_000);
|
|
275
|
+
|
|
263
276
|
const id = options.id || path.basename(scriptPath, '.ts');
|
|
264
277
|
|
|
265
278
|
if (registry.has(id)) {
|
|
@@ -317,7 +330,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
317
330
|
}
|
|
318
331
|
}
|
|
319
332
|
|
|
320
|
-
// Polling state
|
|
333
|
+
// Polling state (declared early so WebSocket handlers can reference)
|
|
321
334
|
let previousSnapshot: AutomationSnapshot | null = null;
|
|
322
335
|
let pollCount = 0;
|
|
323
336
|
let eventsEmitted = 0;
|
|
@@ -333,6 +346,110 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
333
346
|
}
|
|
334
347
|
}
|
|
335
348
|
|
|
349
|
+
// ── WebSocket setup ─────────────────────────────────────────────
|
|
350
|
+
let ws: WebSocketManager | null = null;
|
|
351
|
+
let wsConnected = false;
|
|
352
|
+
// Track latest prices from WebSocket for real-time price_change events
|
|
353
|
+
let wsPrices = new Map<string, number>();
|
|
354
|
+
|
|
355
|
+
if (useWebSocket) {
|
|
356
|
+
try {
|
|
357
|
+
ws = new WebSocketManager(verbose);
|
|
358
|
+
|
|
359
|
+
// Wire WebSocket events to the automation event bus
|
|
360
|
+
ws.on('allMids', ({ mids }) => {
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
for (const [coin, mid] of Object.entries(mids)) {
|
|
363
|
+
const newPrice = parseFloat(mid);
|
|
364
|
+
if (isNaN(newPrice) || newPrice === 0) continue;
|
|
365
|
+
const oldPrice = wsPrices.get(coin);
|
|
366
|
+
wsPrices.set(coin, newPrice);
|
|
367
|
+
|
|
368
|
+
if (oldPrice !== undefined && oldPrice !== 0 && eventBus.has('price_change')) {
|
|
369
|
+
const changePct = ((newPrice - oldPrice) / oldPrice) * 100;
|
|
370
|
+
if (Math.abs(changePct) >= 0.01) {
|
|
371
|
+
eventBus.emit('price_change', { coin, oldPrice, newPrice, changePct })
|
|
372
|
+
.then(errors => { if (errors.length) handleErrors(errors); });
|
|
373
|
+
eventsEmitted++;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
ws.on('orderUpdate', (update) => {
|
|
380
|
+
if (eventBus.has('order_update')) {
|
|
381
|
+
eventBus.emit('order_update', {
|
|
382
|
+
coin: update.order.coin,
|
|
383
|
+
oid: update.order.oid,
|
|
384
|
+
side: update.order.side === 'B' ? 'buy' : 'sell',
|
|
385
|
+
size: parseFloat(update.order.sz),
|
|
386
|
+
price: parseFloat(update.order.limitPx),
|
|
387
|
+
origSize: parseFloat(update.order.origSz),
|
|
388
|
+
status: update.status,
|
|
389
|
+
statusTimestamp: update.statusTimestamp,
|
|
390
|
+
}).then(errors => { if (errors.length) handleErrors(errors); });
|
|
391
|
+
eventsEmitted++;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Also emit order_filled for backward compatibility
|
|
395
|
+
if (update.status === 'filled' && eventBus.has('order_filled')) {
|
|
396
|
+
eventBus.emit('order_filled', {
|
|
397
|
+
coin: update.order.coin,
|
|
398
|
+
oid: update.order.oid,
|
|
399
|
+
side: update.order.side === 'B' ? 'buy' : 'sell',
|
|
400
|
+
size: parseFloat(update.order.sz),
|
|
401
|
+
price: parseFloat(update.order.limitPx),
|
|
402
|
+
}).then(errors => { if (errors.length) handleErrors(errors); });
|
|
403
|
+
eventsEmitted++;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
ws.on('userFill', (fill) => {
|
|
408
|
+
// userFill events are already covered by order_update with status=filled
|
|
409
|
+
// But this provides the realized PnL and fee data that order_update doesn't have
|
|
410
|
+
log.debug(`Fill: ${fill.side === 'B' ? 'BUY' : 'SELL'} ${fill.sz} ${fill.coin} @ ${fill.px} (PnL: ${fill.closedPnl})`);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
ws.on('userEvent', (event) => {
|
|
414
|
+
// Handle liquidation events — only available through WebSocket
|
|
415
|
+
if ('liquidation' in event && eventBus.has('liquidation')) {
|
|
416
|
+
const liq = event.liquidation;
|
|
417
|
+
eventBus.emit('liquidation', {
|
|
418
|
+
lid: liq.lid,
|
|
419
|
+
liquidator: liq.liquidator,
|
|
420
|
+
liquidatedUser: liq.liquidated_user,
|
|
421
|
+
liquidatedNtlPos: parseFloat(liq.liquidated_ntl_pos),
|
|
422
|
+
liquidatedAccountValue: parseFloat(liq.liquidated_account_value),
|
|
423
|
+
}).then(errors => { if (errors.length) handleErrors(errors); });
|
|
424
|
+
eventsEmitted++;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
ws.on('error', ({ error }) => {
|
|
429
|
+
log.warn(`WebSocket error: ${error.message}`);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
ws.on('disconnected', () => {
|
|
433
|
+
wsConnected = false;
|
|
434
|
+
log.warn('WebSocket disconnected — falling back to REST polling');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
ws.on('connected', () => {
|
|
438
|
+
wsConnected = true;
|
|
439
|
+
log.info('WebSocket connected — real-time events active');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Connect and subscribe
|
|
443
|
+
const userAddress = rawClient.address as `0x${string}`;
|
|
444
|
+
await ws.subscribeAll(userAddress);
|
|
445
|
+
log.info('WebSocket subscriptions active (allMids, orderUpdates, userFills, userEvents)');
|
|
446
|
+
} catch (err) {
|
|
447
|
+
log.warn(`WebSocket setup failed: ${err instanceof Error ? err.message : String(err)} — using REST polling only`);
|
|
448
|
+
ws = null;
|
|
449
|
+
wsConnected = false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
336
453
|
async function poll() {
|
|
337
454
|
if (isPolling || stopped) return;
|
|
338
455
|
isPolling = true;
|
|
@@ -348,8 +465,8 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
348
465
|
eventsEmitted++;
|
|
349
466
|
|
|
350
467
|
if (previousSnapshot) {
|
|
351
|
-
// Price changes
|
|
352
|
-
if (eventBus.has('price_change')) {
|
|
468
|
+
// Price changes (skip when WebSocket is handling real-time prices)
|
|
469
|
+
if (eventBus.has('price_change') && !wsConnected) {
|
|
353
470
|
for (const [coin, newPrice] of snapshot.prices) {
|
|
354
471
|
const oldPrice = previousSnapshot.prices.get(coin);
|
|
355
472
|
if (oldPrice === undefined || oldPrice === 0) continue;
|
|
@@ -485,7 +602,8 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
485
602
|
}
|
|
486
603
|
|
|
487
604
|
// Start polling
|
|
488
|
-
|
|
605
|
+
const wsLabel = wsConnected ? ', ws=on' : (useWebSocket ? ', ws=failed' : '');
|
|
606
|
+
log.info(`Started (poll every ${pollIntervalMs / 1000}s, dry=${dryRun}${wsLabel})`);
|
|
489
607
|
const timer = setInterval(poll, pollIntervalMs);
|
|
490
608
|
|
|
491
609
|
// Initial poll to seed state
|
|
@@ -497,6 +615,13 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
|
|
|
497
615
|
stopped = true;
|
|
498
616
|
clearInterval(timer);
|
|
499
617
|
|
|
618
|
+
// Close WebSocket
|
|
619
|
+
if (ws) {
|
|
620
|
+
ws.removeAllListeners();
|
|
621
|
+
await ws.close();
|
|
622
|
+
ws = null;
|
|
623
|
+
}
|
|
624
|
+
|
|
500
625
|
for (const hook of stopHooks) {
|
|
501
626
|
try { await hook(); } catch (err) {
|
|
502
627
|
log.error(`onStop hook error: ${err instanceof Error ? err.message : String(err)}`);
|
package/scripts/auto/types.ts
CHANGED
|
@@ -33,7 +33,9 @@ export type AutomationEventType =
|
|
|
33
33
|
| 'position_changed'
|
|
34
34
|
| 'pnl_threshold'
|
|
35
35
|
| 'margin_warning'
|
|
36
|
-
| 'order_filled'
|
|
36
|
+
| 'order_filled'
|
|
37
|
+
| 'order_update'
|
|
38
|
+
| 'liquidation';
|
|
37
39
|
|
|
38
40
|
export interface AutomationEventPayloads {
|
|
39
41
|
tick: { timestamp: number; pollCount: number };
|
|
@@ -45,6 +47,25 @@ export interface AutomationEventPayloads {
|
|
|
45
47
|
pnl_threshold: { coin: string; unrealizedPnl: number; changePct: number; positionValue: number };
|
|
46
48
|
margin_warning: { marginUsedPct: number; equity: number; marginUsed: number };
|
|
47
49
|
order_filled: { coin: string; oid: number; side: 'buy' | 'sell'; size: number; price: number };
|
|
50
|
+
/** Real-time order lifecycle event via WebSocket (filled, canceled, rejected, triggered, etc.) */
|
|
51
|
+
order_update: {
|
|
52
|
+
coin: string;
|
|
53
|
+
oid: number;
|
|
54
|
+
side: 'buy' | 'sell';
|
|
55
|
+
size: number;
|
|
56
|
+
price: number;
|
|
57
|
+
origSize: number;
|
|
58
|
+
status: string;
|
|
59
|
+
statusTimestamp: number;
|
|
60
|
+
};
|
|
61
|
+
/** Liquidation event via WebSocket — only source for liquidation alerts */
|
|
62
|
+
liquidation: {
|
|
63
|
+
lid: number;
|
|
64
|
+
liquidator: string;
|
|
65
|
+
liquidatedUser: string;
|
|
66
|
+
liquidatedNtlPos: number;
|
|
67
|
+
liquidatedAccountValue: number;
|
|
68
|
+
};
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
export type AutomationEventHandler<E extends AutomationEventType> =
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// WebSocket Manager for Hyperliquid real-time data
|
|
2
|
+
// Wraps @nktkas/hyperliquid SubscriptionClient with event-driven API
|
|
3
|
+
|
|
4
|
+
import { WebSocketTransport, SubscriptionClient } from '@nktkas/hyperliquid';
|
|
5
|
+
import type { ISubscription } from '@nktkas/hyperliquid';
|
|
6
|
+
import type {
|
|
7
|
+
AllMidsWsEvent,
|
|
8
|
+
OrderUpdatesWsEvent,
|
|
9
|
+
UserFillsWsEvent,
|
|
10
|
+
UserEventsWsEvent,
|
|
11
|
+
} from '@nktkas/hyperliquid';
|
|
12
|
+
import { isMainnet } from './config.js';
|
|
13
|
+
|
|
14
|
+
// ── Event types ────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface WsEventMap {
|
|
17
|
+
/** Mid prices for all assets updated */
|
|
18
|
+
allMids: { mids: Record<string, string> };
|
|
19
|
+
/** Order status changed (filled, canceled, rejected, etc.) */
|
|
20
|
+
orderUpdate: {
|
|
21
|
+
order: {
|
|
22
|
+
coin: string;
|
|
23
|
+
side: 'B' | 'A';
|
|
24
|
+
limitPx: string;
|
|
25
|
+
sz: string;
|
|
26
|
+
oid: number;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
origSz: string;
|
|
29
|
+
cloid?: string;
|
|
30
|
+
reduceOnly?: boolean;
|
|
31
|
+
};
|
|
32
|
+
status: string;
|
|
33
|
+
statusTimestamp: number;
|
|
34
|
+
};
|
|
35
|
+
/** Trade fill received */
|
|
36
|
+
userFill: {
|
|
37
|
+
coin: string;
|
|
38
|
+
px: string;
|
|
39
|
+
sz: string;
|
|
40
|
+
side: 'B' | 'A';
|
|
41
|
+
time: number;
|
|
42
|
+
closedPnl: string;
|
|
43
|
+
fee: string;
|
|
44
|
+
oid: number;
|
|
45
|
+
crossed: boolean;
|
|
46
|
+
};
|
|
47
|
+
/** User event (fills, funding, liquidation, non-user cancels) */
|
|
48
|
+
userEvent: UserEventsWsEvent;
|
|
49
|
+
/** WebSocket connected */
|
|
50
|
+
connected: undefined;
|
|
51
|
+
/** WebSocket disconnected */
|
|
52
|
+
disconnected: { reason?: string };
|
|
53
|
+
/** WebSocket error */
|
|
54
|
+
error: { error: Error };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type WsEventType = keyof WsEventMap;
|
|
58
|
+
export type WsEventHandler<E extends WsEventType> = (data: WsEventMap[E]) => void;
|
|
59
|
+
|
|
60
|
+
// ── Manager ────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export class WebSocketManager {
|
|
63
|
+
private transport: WebSocketTransport | null = null;
|
|
64
|
+
private client: SubscriptionClient | null = null;
|
|
65
|
+
private subscriptions: ISubscription[] = [];
|
|
66
|
+
private handlers = new Map<WsEventType, Set<Function>>();
|
|
67
|
+
private _connected = false;
|
|
68
|
+
private verbose: boolean;
|
|
69
|
+
|
|
70
|
+
constructor(verbose = false) {
|
|
71
|
+
this.verbose = verbose;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private log(...args: unknown[]) {
|
|
75
|
+
if (this.verbose) console.log('[WS]', ...args);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get connected(): boolean {
|
|
79
|
+
return this._connected;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Connection management ──────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
async connect(): Promise<void> {
|
|
85
|
+
if (this.transport) return; // already connected
|
|
86
|
+
|
|
87
|
+
this.transport = new WebSocketTransport({
|
|
88
|
+
isTestnet: !isMainnet(),
|
|
89
|
+
resubscribe: true, // auto-resubscribe on reconnect
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.client = new SubscriptionClient({ transport: this.transport });
|
|
93
|
+
|
|
94
|
+
await this.transport.ready();
|
|
95
|
+
this._connected = true;
|
|
96
|
+
this.emit('connected', undefined);
|
|
97
|
+
this.log('Connected to', isMainnet() ? 'mainnet' : 'testnet');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async close(): Promise<void> {
|
|
101
|
+
for (const sub of this.subscriptions) {
|
|
102
|
+
try { await sub.unsubscribe(); } catch { /* ignore */ }
|
|
103
|
+
}
|
|
104
|
+
this.subscriptions = [];
|
|
105
|
+
|
|
106
|
+
if (this.transport) {
|
|
107
|
+
try { await this.transport.close(); } catch { /* ignore */ }
|
|
108
|
+
this.transport = null;
|
|
109
|
+
this.client = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this._connected = false;
|
|
113
|
+
this.emit('disconnected', { reason: 'manual close' });
|
|
114
|
+
this.log('Closed');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Event system ───────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
on<E extends WsEventType>(event: E, handler: WsEventHandler<E>): void {
|
|
120
|
+
if (!this.handlers.has(event)) {
|
|
121
|
+
this.handlers.set(event, new Set());
|
|
122
|
+
}
|
|
123
|
+
this.handlers.get(event)!.add(handler);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
off<E extends WsEventType>(event: E, handler: WsEventHandler<E>): void {
|
|
127
|
+
this.handlers.get(event)?.delete(handler);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private emit<E extends WsEventType>(event: E, data: WsEventMap[E]): void {
|
|
131
|
+
const set = this.handlers.get(event);
|
|
132
|
+
if (!set) return;
|
|
133
|
+
for (const handler of set) {
|
|
134
|
+
try {
|
|
135
|
+
handler(data);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
this.log('Handler error:', err instanceof Error ? err.message : String(err));
|
|
138
|
+
this.emit('error' as E, { error: err instanceof Error ? err : new Error(String(err)) } as WsEventMap[E]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
removeAllListeners(): void {
|
|
144
|
+
this.handlers.clear();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Subscription helpers ───────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
private ensureClient(): SubscriptionClient {
|
|
150
|
+
if (!this.client) throw new Error('WebSocket not connected. Call connect() first.');
|
|
151
|
+
return this.client;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private trackSub(sub: ISubscription): ISubscription {
|
|
155
|
+
this.subscriptions.push(sub);
|
|
156
|
+
sub.failureSignal.addEventListener('abort', () => {
|
|
157
|
+
this.log('Subscription failed, removing from tracked list');
|
|
158
|
+
const idx = this.subscriptions.indexOf(sub);
|
|
159
|
+
if (idx >= 0) this.subscriptions.splice(idx, 1);
|
|
160
|
+
});
|
|
161
|
+
return sub;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Market data subscriptions ──────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Subscribe to mid prices for all assets. Fires on every price update.
|
|
168
|
+
*/
|
|
169
|
+
async subscribeAllMids(): Promise<ISubscription> {
|
|
170
|
+
const client = this.ensureClient();
|
|
171
|
+
const sub = await client.allMids((data: AllMidsWsEvent) => {
|
|
172
|
+
this.emit('allMids', { mids: data.mids });
|
|
173
|
+
});
|
|
174
|
+
return this.trackSub(sub);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── User subscriptions ────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Subscribe to order lifecycle events (fill, cancel, reject, etc.).
|
|
181
|
+
* This is the most important subscription for trading automations.
|
|
182
|
+
*/
|
|
183
|
+
async subscribeOrderUpdates(user: `0x${string}`): Promise<ISubscription> {
|
|
184
|
+
const client = this.ensureClient();
|
|
185
|
+
const sub = await client.orderUpdates({ user }, (data: OrderUpdatesWsEvent) => {
|
|
186
|
+
for (const update of data) {
|
|
187
|
+
this.emit('orderUpdate', {
|
|
188
|
+
order: {
|
|
189
|
+
coin: update.order.coin,
|
|
190
|
+
side: update.order.side,
|
|
191
|
+
limitPx: update.order.limitPx,
|
|
192
|
+
sz: update.order.sz,
|
|
193
|
+
oid: update.order.oid,
|
|
194
|
+
timestamp: update.order.timestamp,
|
|
195
|
+
origSz: update.order.origSz,
|
|
196
|
+
cloid: update.order.cloid,
|
|
197
|
+
reduceOnly: update.order.reduceOnly,
|
|
198
|
+
},
|
|
199
|
+
status: update.status,
|
|
200
|
+
statusTimestamp: update.statusTimestamp,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
return this.trackSub(sub);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Subscribe to trade fills for a user.
|
|
209
|
+
*/
|
|
210
|
+
async subscribeUserFills(user: `0x${string}`): Promise<ISubscription> {
|
|
211
|
+
const client = this.ensureClient();
|
|
212
|
+
const sub = await client.userFills({ user }, (data: UserFillsWsEvent) => {
|
|
213
|
+
if (data.isSnapshot) return; // skip initial snapshot
|
|
214
|
+
for (const fill of data.fills) {
|
|
215
|
+
this.emit('userFill', {
|
|
216
|
+
coin: fill.coin,
|
|
217
|
+
px: fill.px,
|
|
218
|
+
sz: fill.sz,
|
|
219
|
+
side: fill.side,
|
|
220
|
+
time: fill.time,
|
|
221
|
+
closedPnl: fill.closedPnl,
|
|
222
|
+
fee: fill.fee,
|
|
223
|
+
oid: fill.oid,
|
|
224
|
+
crossed: fill.crossed,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return this.trackSub(sub);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Subscribe to all user events (fills, funding, liquidations, non-user cancels).
|
|
233
|
+
* This is the only way to get liquidation alerts.
|
|
234
|
+
*/
|
|
235
|
+
async subscribeUserEvents(user: `0x${string}`): Promise<ISubscription> {
|
|
236
|
+
const client = this.ensureClient();
|
|
237
|
+
const sub = await client.userEvents({ user }, (data: UserEventsWsEvent) => {
|
|
238
|
+
this.emit('userEvent', data);
|
|
239
|
+
});
|
|
240
|
+
return this.trackSub(sub);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Convenience: subscribe to all relevant feeds for an automation ──
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Start all subscriptions needed for the automation runtime:
|
|
247
|
+
* - allMids (price feed)
|
|
248
|
+
* - orderUpdates (order lifecycle)
|
|
249
|
+
* - userFills (trade fills)
|
|
250
|
+
* - userEvents (liquidations, funding payments, system cancels)
|
|
251
|
+
*/
|
|
252
|
+
async subscribeAll(user: `0x${string}`): Promise<void> {
|
|
253
|
+
await this.connect();
|
|
254
|
+
this.log('Subscribing to all feeds for', user);
|
|
255
|
+
|
|
256
|
+
await Promise.all([
|
|
257
|
+
this.subscribeAllMids(),
|
|
258
|
+
this.subscribeOrderUpdates(user),
|
|
259
|
+
this.subscribeUserFills(user),
|
|
260
|
+
this.subscribeUserEvents(user),
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
this.log('All subscriptions active');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Singleton ──────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
let wsInstance: WebSocketManager | null = null;
|
|
270
|
+
|
|
271
|
+
export function getWebSocket(verbose = false): WebSocketManager {
|
|
272
|
+
if (!wsInstance) {
|
|
273
|
+
wsInstance = new WebSocketManager(verbose);
|
|
274
|
+
}
|
|
275
|
+
return wsInstance;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function resetWebSocket(): void {
|
|
279
|
+
if (wsInstance) {
|
|
280
|
+
wsInstance.close().catch(() => {});
|
|
281
|
+
wsInstance = null;
|
|
282
|
+
}
|
|
283
|
+
}
|
package/scripts/info/account.ts
CHANGED
|
@@ -7,16 +7,20 @@ import { formatUsd, formatPercent, parseArgs } from '../core/utils.js';
|
|
|
7
7
|
async function main() {
|
|
8
8
|
const args = parseArgs(process.argv.slice(2));
|
|
9
9
|
const jsonOutput = args.json as boolean;
|
|
10
|
+
const targetAddress = args.address as string | undefined;
|
|
10
11
|
const client = getClient();
|
|
11
12
|
|
|
12
13
|
if (args.verbose) {
|
|
13
14
|
client.verbose = true;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
+
const lookupAddress = targetAddress?.toLowerCase() ?? client.address;
|
|
18
|
+
const isOtherAccount = !!targetAddress;
|
|
19
|
+
|
|
20
|
+
const accountMode = await client.getAccountMode(isOtherAccount ? lookupAddress : undefined);
|
|
17
21
|
|
|
18
22
|
try {
|
|
19
|
-
const state = await client.getUserStateAll();
|
|
23
|
+
const state = await client.getUserStateAll(isOtherAccount ? lookupAddress : undefined);
|
|
20
24
|
|
|
21
25
|
const margin = state.crossMarginSummary;
|
|
22
26
|
const accountValue = parseFloat(margin.accountValue);
|
|
@@ -48,12 +52,19 @@ async function main() {
|
|
|
48
52
|
|
|
49
53
|
const totalPnl = positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
|
|
50
54
|
|
|
55
|
+
// Fetch spot balances
|
|
56
|
+
const userParam = isOtherAccount ? lookupAddress : undefined;
|
|
57
|
+
const spotState = await client.getSpotBalances(userParam);
|
|
58
|
+
const spotBalances = (spotState?.balances ?? []).filter(b => parseFloat(b.total) > 0);
|
|
59
|
+
|
|
51
60
|
// JSON output
|
|
52
61
|
if (jsonOutput) {
|
|
53
62
|
const result: Record<string, unknown> = {
|
|
54
|
-
address:
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
address: lookupAddress,
|
|
64
|
+
...(isOtherAccount ? {} : {
|
|
65
|
+
signingWallet: client.walletAddress,
|
|
66
|
+
walletType: client.isApiWallet ? 'api' : 'main',
|
|
67
|
+
}),
|
|
57
68
|
accountMode,
|
|
58
69
|
equity: accountValue,
|
|
59
70
|
totalNotional,
|
|
@@ -62,10 +73,16 @@ async function main() {
|
|
|
62
73
|
marginRatio: totalMarginUsed > 0 && accountValue > 0 ? totalMarginUsed / accountValue : 0,
|
|
63
74
|
totalUnrealizedPnl: totalPnl,
|
|
64
75
|
positions,
|
|
76
|
+
spotBalances: spotBalances.map(b => ({
|
|
77
|
+
coin: b.coin,
|
|
78
|
+
total: b.total,
|
|
79
|
+
hold: b.hold,
|
|
80
|
+
entryNtl: b.entryNtl,
|
|
81
|
+
})),
|
|
65
82
|
};
|
|
66
83
|
|
|
67
84
|
if (args.orders) {
|
|
68
|
-
const orders = await client.getOpenOrders();
|
|
85
|
+
const orders = await client.getOpenOrders(isOtherAccount ? lookupAddress : undefined);
|
|
69
86
|
result.openOrders = orders.map(o => ({
|
|
70
87
|
coin: o.coin,
|
|
71
88
|
oid: o.oid,
|
|
@@ -85,11 +102,17 @@ async function main() {
|
|
|
85
102
|
console.log('Open Broker - Account Info');
|
|
86
103
|
console.log('==========================\n');
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
if (isOtherAccount) {
|
|
106
|
+
console.log('Lookup Address');
|
|
107
|
+
console.log('--------------------');
|
|
108
|
+
console.log(`Address: ${lookupAddress}`);
|
|
109
|
+
} else {
|
|
110
|
+
console.log('Wallet Configuration');
|
|
111
|
+
console.log('--------------------');
|
|
112
|
+
console.log(`Trading Account: ${client.address}`);
|
|
113
|
+
console.log(`Signing Wallet: ${client.walletAddress}`);
|
|
114
|
+
console.log(`Wallet Type: ${client.isApiWallet ? 'API Wallet' : 'Main Wallet'}`);
|
|
115
|
+
}
|
|
93
116
|
|
|
94
117
|
const modeLabel: Record<string, string> = {
|
|
95
118
|
standard: 'Standard (separate balances per dex)',
|
|
@@ -99,22 +122,24 @@ async function main() {
|
|
|
99
122
|
};
|
|
100
123
|
console.log(`Account Mode: ${modeLabel[accountMode] ?? accountMode}`);
|
|
101
124
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
if (!isOtherAccount) {
|
|
126
|
+
// Check builder fee approval
|
|
127
|
+
const builderApproval = await client.getMaxBuilderFee();
|
|
128
|
+
console.log(`Builder Address: ${client.builderAddress}`);
|
|
129
|
+
console.log(`Builder Fee: ${client.builderFeeBps} bps`);
|
|
130
|
+
if (builderApproval) {
|
|
131
|
+
console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`Builder Approved: ❌ No`);
|
|
134
|
+
console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
|
|
135
|
+
}
|
|
112
136
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
// Warn if API wallet setup looks misconfigured
|
|
138
|
+
if (!client.isApiWallet && accountValue === 0 && positions.length === 0) {
|
|
139
|
+
console.log('\n⚠️ No positions and $0 equity.');
|
|
140
|
+
console.log(' If this account is traded via an API wallet, set HYPERLIQUID_ACCOUNT_ADDRESS');
|
|
141
|
+
console.log(' in ~/.openbroker/.env to the master account address (the wallet that holds funds).');
|
|
142
|
+
}
|
|
118
143
|
}
|
|
119
144
|
console.log('');
|
|
120
145
|
|
|
@@ -152,12 +177,29 @@ async function main() {
|
|
|
152
177
|
console.log(`Total Unrealized PnL: ${formatUsd(totalPnl)}`);
|
|
153
178
|
}
|
|
154
179
|
|
|
180
|
+
// Show spot balances
|
|
181
|
+
if (spotBalances.length > 0) {
|
|
182
|
+
console.log('\nSpot Balances');
|
|
183
|
+
console.log('-------------');
|
|
184
|
+
console.log('Token | Total | Hold | Entry Value');
|
|
185
|
+
console.log('-------------|--------------------|--------------------|------------');
|
|
186
|
+
|
|
187
|
+
for (const b of spotBalances) {
|
|
188
|
+
const total = parseFloat(b.total);
|
|
189
|
+
const hold = parseFloat(b.hold);
|
|
190
|
+
const entry = parseFloat(b.entryNtl);
|
|
191
|
+
console.log(
|
|
192
|
+
`${b.coin.padEnd(12)} | ${total.toFixed(6).padStart(18)} | ${hold.toFixed(6).padStart(18)} | ${formatUsd(entry)}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
155
197
|
// Show open orders if requested
|
|
156
198
|
if (args.orders) {
|
|
157
199
|
console.log('\nOpen Orders');
|
|
158
200
|
console.log('-----------');
|
|
159
201
|
|
|
160
|
-
const orders = await client.getOpenOrders();
|
|
202
|
+
const orders = await client.getOpenOrders(isOtherAccount ? lookupAddress : undefined);
|
|
161
203
|
if (orders.length === 0) {
|
|
162
204
|
console.log('No open orders');
|
|
163
205
|
} else {
|
package/scripts/info/fees.ts
CHANGED
|
@@ -24,13 +24,19 @@ async function main() {
|
|
|
24
24
|
process.exit(0);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const targetAddress = args.address as string | undefined;
|
|
27
28
|
const client = getClient();
|
|
29
|
+
const lookupAddress = targetAddress?.toLowerCase();
|
|
28
30
|
|
|
29
31
|
console.log('Open Broker - Fee Schedule');
|
|
30
32
|
console.log('=========================\n');
|
|
31
33
|
|
|
34
|
+
if (targetAddress) {
|
|
35
|
+
console.log(`Lookup: ${lookupAddress}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
try {
|
|
33
|
-
const fees = await client.getUserFees();
|
|
39
|
+
const fees = await client.getUserFees(lookupAddress);
|
|
34
40
|
|
|
35
41
|
// Fee rates
|
|
36
42
|
console.log('Fee Rates');
|
package/scripts/info/fills.ts
CHANGED
|
@@ -9,15 +9,17 @@ function printUsage() {
|
|
|
9
9
|
Usage: openbroker fills [options]
|
|
10
10
|
|
|
11
11
|
Options:
|
|
12
|
-
--coin <symbol>
|
|
13
|
-
--side <buy|sell>
|
|
14
|
-
--top <n>
|
|
15
|
-
--
|
|
12
|
+
--coin <symbol> Filter by coin (e.g. ETH, BTC)
|
|
13
|
+
--side <buy|sell> Filter by side
|
|
14
|
+
--top <n> Show last N fills (default: 20)
|
|
15
|
+
--address <0x...> Look up another account's fills
|
|
16
|
+
--help, -h Show this help
|
|
16
17
|
|
|
17
18
|
Examples:
|
|
18
19
|
openbroker fills
|
|
19
20
|
openbroker fills --coin ETH
|
|
20
21
|
openbroker fills --coin BTC --side buy --top 50
|
|
22
|
+
openbroker fills --address 0xabc... --coin ETH
|
|
21
23
|
`);
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -33,10 +35,13 @@ async function main() {
|
|
|
33
35
|
const filterSide = args.side as string | undefined;
|
|
34
36
|
const top = parseInt(args.top as string) || 20;
|
|
35
37
|
const jsonOutput = args.json as boolean;
|
|
38
|
+
const targetAddress = args.address as string | undefined;
|
|
36
39
|
const client = getClient();
|
|
37
40
|
|
|
41
|
+
const lookupAddress = targetAddress?.toLowerCase();
|
|
42
|
+
|
|
38
43
|
try {
|
|
39
|
-
let fills = await client.getUserFills();
|
|
44
|
+
let fills = await client.getUserFills(lookupAddress);
|
|
40
45
|
|
|
41
46
|
if (filterCoin) {
|
|
42
47
|
fills = fills.filter(f => f.coin === normalizeCoin(filterCoin));
|
|
@@ -66,6 +71,10 @@ async function main() {
|
|
|
66
71
|
console.log('Open Broker - Trade Fills');
|
|
67
72
|
console.log('========================\n');
|
|
68
73
|
|
|
74
|
+
if (targetAddress) {
|
|
75
|
+
console.log(`Lookup: ${lookupAddress}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
if (fills.length === 0) {
|
|
70
79
|
console.log('No fills found');
|
|
71
80
|
return;
|
|
@@ -9,12 +9,14 @@ function printUsage() {
|
|
|
9
9
|
Usage: openbroker order-status --oid <order-id>
|
|
10
10
|
|
|
11
11
|
Options:
|
|
12
|
-
--oid <id>
|
|
13
|
-
--
|
|
12
|
+
--oid <id> Order ID (number) or client order ID (hex string) — required
|
|
13
|
+
--address <0x...> Look up order on another account
|
|
14
|
+
--help, -h Show this help
|
|
14
15
|
|
|
15
16
|
Examples:
|
|
16
17
|
openbroker order-status --oid 123456789
|
|
17
18
|
openbroker order-status --oid 0x1234abcd...
|
|
19
|
+
openbroker order-status --oid 123456789 --address 0xabc...
|
|
18
20
|
`);
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -34,13 +36,15 @@ async function main() {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const oid = oidArg.startsWith('0x') ? oidArg : parseInt(oidArg);
|
|
39
|
+
const targetAddress = args.address as string | undefined;
|
|
37
40
|
const client = getClient();
|
|
41
|
+
const lookupAddress = targetAddress?.toLowerCase();
|
|
38
42
|
|
|
39
43
|
console.log('Open Broker - Order Status');
|
|
40
44
|
console.log('=========================\n');
|
|
41
45
|
|
|
42
46
|
try {
|
|
43
|
-
const result = await client.getOrderStatus(oid);
|
|
47
|
+
const result = await client.getOrderStatus(oid, lookupAddress);
|
|
44
48
|
|
|
45
49
|
if (result.status === 'unknownOid') {
|
|
46
50
|
console.log(`Order ${oidArg} not found`);
|
package/scripts/info/orders.ts
CHANGED
|
@@ -13,6 +13,7 @@ Options:
|
|
|
13
13
|
--status <status> Filter by status (filled, canceled, open, triggered, rejected, etc.)
|
|
14
14
|
--open Show only currently open orders
|
|
15
15
|
--top <n> Show last N orders (default: 20)
|
|
16
|
+
--address <0x...> Look up another account's orders
|
|
16
17
|
--help, -h Show this help
|
|
17
18
|
|
|
18
19
|
Examples:
|
|
@@ -21,6 +22,7 @@ Examples:
|
|
|
21
22
|
openbroker orders --open --coin ETH
|
|
22
23
|
openbroker orders --coin ETH --status filled
|
|
23
24
|
openbroker orders --top 50
|
|
25
|
+
openbroker orders --address 0xabc... --open
|
|
24
26
|
`);
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -37,12 +39,15 @@ async function main() {
|
|
|
37
39
|
const openOnly = args.open as boolean;
|
|
38
40
|
const top = parseInt(args.top as string) || 20;
|
|
39
41
|
const jsonOutput = args.json as boolean;
|
|
42
|
+
const targetAddress = args.address as string | undefined;
|
|
40
43
|
const client = getClient();
|
|
41
44
|
|
|
45
|
+
const lookupAddress = targetAddress?.toLowerCase();
|
|
46
|
+
|
|
42
47
|
try {
|
|
43
48
|
if (openOnly) {
|
|
44
49
|
// Use the dedicated open orders endpoint
|
|
45
|
-
let openOrders = await client.getOpenOrders();
|
|
50
|
+
let openOrders = await client.getOpenOrders(lookupAddress);
|
|
46
51
|
|
|
47
52
|
if (filterCoin) {
|
|
48
53
|
openOrders = openOrders.filter(o => o.coin === normalizeCoin(filterCoin));
|
|
@@ -69,6 +74,10 @@ async function main() {
|
|
|
69
74
|
console.log('Open Broker - Open Orders');
|
|
70
75
|
console.log('=========================\n');
|
|
71
76
|
|
|
77
|
+
if (targetAddress) {
|
|
78
|
+
console.log(`Lookup: ${lookupAddress}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
if (openOrders.length === 0) {
|
|
73
82
|
console.log('No open orders found');
|
|
74
83
|
return;
|
|
@@ -106,7 +115,7 @@ async function main() {
|
|
|
106
115
|
return;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
let orders = await client.getHistoricalOrders();
|
|
118
|
+
let orders = await client.getHistoricalOrders(lookupAddress);
|
|
110
119
|
|
|
111
120
|
if (filterCoin) {
|
|
112
121
|
orders = orders.filter(o => o.order.coin === normalizeCoin(filterCoin));
|
|
@@ -137,6 +146,10 @@ async function main() {
|
|
|
137
146
|
console.log('Open Broker - Order History');
|
|
138
147
|
console.log('==========================\n');
|
|
139
148
|
|
|
149
|
+
if (targetAddress) {
|
|
150
|
+
console.log(`Lookup: ${lookupAddress}\n`);
|
|
151
|
+
}
|
|
152
|
+
|
|
140
153
|
if (orders.length === 0) {
|
|
141
154
|
console.log('No orders found');
|
|
142
155
|
return;
|
|
@@ -8,17 +8,21 @@ async function main() {
|
|
|
8
8
|
const args = parseArgs(process.argv.slice(2));
|
|
9
9
|
const filterCoin = args.coin as string | undefined;
|
|
10
10
|
const jsonOutput = args.json as boolean;
|
|
11
|
+
const targetAddress = args.address as string | undefined;
|
|
11
12
|
const client = getClient();
|
|
12
13
|
|
|
13
14
|
if (args.verbose) {
|
|
14
15
|
client.verbose = true;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
const lookupAddress = targetAddress?.toLowerCase();
|
|
19
|
+
const isOtherAccount = !!targetAddress;
|
|
20
|
+
|
|
17
21
|
try {
|
|
18
22
|
const [state, mids, fundingHistory] = await Promise.all([
|
|
19
|
-
client.getUserStateAll(),
|
|
23
|
+
client.getUserStateAll(lookupAddress),
|
|
20
24
|
client.getAllMids(),
|
|
21
|
-
client.getUserFunding(),
|
|
25
|
+
client.getUserFunding(lookupAddress),
|
|
22
26
|
]);
|
|
23
27
|
|
|
24
28
|
const positions = state.assetPositions.filter(ap => {
|
|
@@ -69,9 +73,13 @@ async function main() {
|
|
|
69
73
|
console.log('Open Broker - Positions');
|
|
70
74
|
console.log('=======================\n');
|
|
71
75
|
|
|
76
|
+
if (isOtherAccount) {
|
|
77
|
+
console.log(`Lookup: ${lookupAddress}\n`);
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
if (positions.length === 0) {
|
|
73
81
|
console.log(filterCoin ? `No position in ${filterCoin}` : 'No open positions');
|
|
74
|
-
if (!filterCoin && !client.isApiWallet) {
|
|
82
|
+
if (!filterCoin && !isOtherAccount && !client.isApiWallet) {
|
|
75
83
|
console.log('\n⚠️ If this account is traded via an API wallet, set HYPERLIQUID_ACCOUNT_ADDRESS');
|
|
76
84
|
console.log(' in ~/.openbroker/.env to the master account address.');
|
|
77
85
|
}
|
package/scripts/info/spot.ts
CHANGED
|
@@ -8,6 +8,7 @@ interface Args {
|
|
|
8
8
|
balances?: boolean;
|
|
9
9
|
top?: number;
|
|
10
10
|
verbose?: boolean;
|
|
11
|
+
address?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
function parseArgs(): Args {
|
|
@@ -21,6 +22,8 @@ function parseArgs(): Args {
|
|
|
21
22
|
args.balances = true;
|
|
22
23
|
} else if (arg === '--top' && process.argv[i + 1]) {
|
|
23
24
|
args.top = parseInt(process.argv[++i], 10);
|
|
25
|
+
} else if (arg === '--address' && process.argv[i + 1]) {
|
|
26
|
+
args.address = process.argv[++i].toLowerCase();
|
|
24
27
|
} else if (arg === '--verbose') {
|
|
25
28
|
args.verbose = true;
|
|
26
29
|
} else if (arg === '--help' || arg === '-h') {
|
|
@@ -30,11 +33,12 @@ Spot Markets - View Hyperliquid spot markets and balances
|
|
|
30
33
|
Usage: npx tsx scripts/info/spot.ts [options]
|
|
31
34
|
|
|
32
35
|
Options:
|
|
33
|
-
--coin <symbol>
|
|
34
|
-
--balances
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
--
|
|
36
|
+
--coin <symbol> Filter by coin symbol
|
|
37
|
+
--balances Show your spot token balances
|
|
38
|
+
--address <0x...> Look up another account's spot balances (with --balances)
|
|
39
|
+
--top <n> Show only top N markets by volume
|
|
40
|
+
--verbose Show detailed output
|
|
41
|
+
--help Show this help
|
|
38
42
|
|
|
39
43
|
Examples:
|
|
40
44
|
npx tsx scripts/info/spot.ts # Show all spot markets
|
|
@@ -80,9 +84,10 @@ async function main() {
|
|
|
80
84
|
|
|
81
85
|
// Show balances
|
|
82
86
|
if (args.balances) {
|
|
83
|
-
|
|
87
|
+
const lookupAddress = args.address ?? client.address;
|
|
88
|
+
console.log(`Fetching spot balances for ${lookupAddress}...\n`);
|
|
84
89
|
|
|
85
|
-
const balances = await client.getSpotBalances();
|
|
90
|
+
const balances = await client.getSpotBalances(args.address);
|
|
86
91
|
|
|
87
92
|
if (!balances.balances || balances.balances.length === 0) {
|
|
88
93
|
console.log('No spot token balances found.');
|