openbroker 1.0.82 → 1.0.87

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 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.82", "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)"}]}}
7
+ metadata: {"author": "monemetrics", "version": "1.0.87", "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_spot_buy ob_spot_sell 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
 
@@ -46,28 +46,52 @@ Or with the `ob_search` plugin tool: `{ "query": "gold" }` or `{ "query": "oil",
46
46
 
47
47
  **HIP-3 assets use `dex:COIN` format** — e.g., `xyz:CL` not just `CL`. If you get an error like "No market data found", search for the asset to find the correct prefixed ticker. Common HIP-3 dexes: `xyz`, `flx`, `km`, `hyna`, `vntl`, `cash`.
48
48
 
49
+ ### Asset IDs (disambiguation)
50
+
51
+ Every info JSON output includes an `assetId` field — the canonical Hyperliquid asset index. Prefer it over the coin name when persisting references, because the same ticker can exist on multiple providers (e.g. `HYPE` perp, `hyna:HYPE` HIP-3, and `HYPE/USDC` spot all coexist).
52
+
53
+ | Scope | Formula | Example |
54
+ |-------|---------|---------|
55
+ | Main perps | universe index | `HYPE` → `159` |
56
+ | HIP-3 perps | `100000 + dexIdx * 10000 + assetIdx` | `hyna:HYPE` → `140002` |
57
+ | Spot | `10000 + pair.index` | `HYPE/USDC` → `10107` |
58
+
59
+ ```bash
60
+ openbroker search HYPE --json | jq '.[] | {coin, assetId, type, provider}'
61
+ ```
62
+
63
+ Trading commands still take `--coin <name>` (including HIP-3 `dex:COIN`) — `assetId` is for queries, comparisons, and agent state, not order placement.
64
+
49
65
  ## Troubleshooting: CLI Fallback
50
66
 
51
67
  If an `ob_*` plugin tool returns unexpected errors, empty results, or crashes, **fall back to the equivalent CLI command** via Bash. The CLI and plugin tools share the same core code, but the CLI has more mature error handling and output.
52
68
 
69
+ **Every info command supports `--json`** for structured output. The table below covers the commands with dedicated plugin tools; any other info command (e.g. `spot`, `trades`, `fees`, `order-status`, `rate-limit`, `funding-history`, `all-markets`) can be run as `openbroker <command> --json` for the same effect.
70
+
53
71
  | Plugin Tool | CLI Equivalent |
54
72
  |-------------|---------------|
55
73
  | `ob_account` | `openbroker account --json` |
56
74
  | `ob_positions` | `openbroker positions --json` |
57
75
  | `ob_funding` | `openbroker funding --json --include-hip3` |
58
76
  | `ob_markets` | `openbroker markets --json --include-hip3` |
59
- | `ob_search` | `openbroker search --query <QUERY>` |
77
+ | `ob_search` | `openbroker search --query <QUERY> --json` |
78
+ | `ob_spot` | `openbroker spot --json` (or `--balances --json`) |
79
+ | `ob_fills` | `openbroker fills --json` |
80
+ | `ob_orders` | `openbroker orders --json` |
81
+ | `ob_order_status` | `openbroker order-status --oid <OID> --json` |
82
+ | `ob_fees` | `openbroker fees --json` |
83
+ | `ob_candles` | `openbroker candles --coin <COIN> --json` |
84
+ | `ob_funding_history` | `openbroker funding-history --coin <COIN> --json` |
85
+ | `ob_trades` | `openbroker trades --coin <COIN> --json` |
86
+ | `ob_rate_limit` | `openbroker rate-limit --json` |
87
+ | `ob_funding_scan` | `openbroker funding-scan --json` |
60
88
  | `ob_buy` | `openbroker buy --coin <COIN> --size <SIZE>` |
61
89
  | `ob_sell` | `openbroker sell --coin <COIN> --size <SIZE>` |
62
90
  | `ob_limit` | `openbroker limit --coin <COIN> --side <SIDE> --size <SIZE> --price <PRICE>` |
63
91
  | `ob_tpsl` | `openbroker tpsl --coin <COIN> --tp <PRICE> --sl <PRICE>` |
64
92
  | `ob_cancel` | `openbroker cancel --all` or `--coin <COIN>` |
65
- | `ob_fills` | `openbroker fills --json` |
66
- | `ob_orders` | `openbroker orders --json` |
67
- | `ob_funding_scan` | `openbroker funding-scan --json` |
68
- | `ob_candles` | `openbroker candles --coin <COIN> --json` |
69
93
  | `ob_auto_run` | `openbroker auto run <script> [--dry]` |
70
- | `ob_auto_stop` | (stop via SIGINT when using CLI) |
94
+ | `ob_auto_stop` | `openbroker auto stop <id>` (or SIGINT if run in foreground) |
71
95
  | `ob_auto_list` | `openbroker auto list` |
72
96
 
73
97
  **When to use CLI fallback:**
@@ -137,12 +161,14 @@ openbroker positions --address 0xabc... # Another account's positions
137
161
  ```bash
138
162
  openbroker funding --top 20 # Top 20 by funding rate
139
163
  openbroker funding --coin ETH # Specific coin
164
+ openbroker funding --top 20 --json # JSON (includes assetId)
140
165
  ```
141
166
 
142
167
  ### Markets
143
168
  ```bash
144
169
  openbroker markets --top 30 # Top 30 main perps
145
170
  openbroker markets --coin BTC # Specific coin
171
+ openbroker markets --coin BTC --json # JSON (includes assetId)
146
172
  ```
147
173
 
148
174
  ### All Markets (Perps + Spot + HIP-3)
@@ -152,6 +178,7 @@ openbroker all-markets --type perp # Main perps only
152
178
  openbroker all-markets --type hip3 # HIP-3 perps only
153
179
  openbroker all-markets --type spot # Spot markets only
154
180
  openbroker all-markets --top 20 # Top 20 by volume
181
+ openbroker all-markets --json # JSON (includes assetId)
155
182
  ```
156
183
 
157
184
  ### Search Markets (Find assets across providers)
@@ -159,6 +186,7 @@ openbroker all-markets --top 20 # Top 20 by volume
159
186
  openbroker search --query GOLD # Find all GOLD markets
160
187
  openbroker search --query BTC # Find BTC across all providers
161
188
  openbroker search --query ETH --type perp # ETH perps only
189
+ openbroker search HYPE --json # JSON with assetId per result
162
190
  ```
163
191
 
164
192
  ### Spot Markets
@@ -168,6 +196,7 @@ openbroker spot --coin PURR # Show PURR market info
168
196
  openbroker spot --balances # Show your spot balances
169
197
  openbroker spot --balances --address 0xabc... # Another account's spot balances
170
198
  openbroker spot --top 20 # Top 20 by volume
199
+ openbroker spot --json # JSON (includes assetId, base, quote)
171
200
  ```
172
201
 
173
202
  ### Trade Fills
@@ -193,12 +222,14 @@ openbroker orders --address 0xabc... --open # Another account's open orders
193
222
  openbroker order-status --oid 123456789 # Check specific order
194
223
  openbroker order-status --oid 0x1234... # By client order ID
195
224
  openbroker order-status --oid 123456789 --address 0xabc... # On another account
225
+ openbroker order-status --oid 123456789 --json
196
226
  ```
197
227
 
198
228
  ### Fee Schedule
199
229
  ```bash
200
230
  openbroker fees # Fee tier, rates, and volume
201
231
  openbroker fees --address 0xabc... # Another account's fees
232
+ openbroker fees --json
202
233
  ```
203
234
 
204
235
  ### Candle Data (OHLCV)
@@ -206,23 +237,27 @@ openbroker fees --address 0xabc... # Another account's fees
206
237
  openbroker candles --coin ETH # 24 hourly candles
207
238
  openbroker candles --coin BTC --interval 4h --bars 48 # 48 four-hour bars
208
239
  openbroker candles --coin SOL --interval 1d --bars 30 # 30 daily bars
240
+ openbroker candles --coin ETH --json # JSON (coin, assetId, interval, candles)
209
241
  ```
210
242
 
211
243
  ### Funding History
212
244
  ```bash
213
245
  openbroker funding-history --coin ETH # Last 24h
214
246
  openbroker funding-history --coin BTC --hours 168 # Last 7 days
247
+ openbroker funding-history --coin ETH --json # JSON (coin, assetId, history)
215
248
  ```
216
249
 
217
250
  ### Recent Trades (Tape)
218
251
  ```bash
219
252
  openbroker trades --coin ETH # Last 30 trades
220
253
  openbroker trades --coin BTC --top 50 # Last 50 trades
254
+ openbroker trades --coin ETH --json # JSON (coin, assetId, trades)
221
255
  ```
222
256
 
223
257
  ### Rate Limit
224
258
  ```bash
225
259
  openbroker rate-limit # API usage and capacity
260
+ openbroker rate-limit --json
226
261
  ```
227
262
 
228
263
  ### Funding Rate Scanner (Cross-Dex)
@@ -231,6 +266,7 @@ openbroker funding-scan # Scan all dexes, >25% threshol
231
266
  openbroker funding-scan --threshold 50 --pairs # Show opposing funding pairs
232
267
  openbroker funding-scan --hip3-only --top 20 # HIP-3 only
233
268
  openbroker funding-scan --watch --interval 120 # Re-scan every 2 minutes
269
+ openbroker funding-scan --json # JSON (includes assetId per result)
234
270
  ```
235
271
 
236
272
  ## Trading Commands
@@ -293,6 +329,27 @@ openbroker cancel --coin ETH # Cancel ETH orders only
293
329
  openbroker cancel --oid 123456 # Cancel specific order
294
330
  ```
295
331
 
332
+ ### Spot Orders
333
+ Spot trading uses a separate order path with its own asset indices (see Asset IDs section). Pass the base token symbol as `--coin` — quote is always USDC.
334
+
335
+ ```bash
336
+ # Market orders (shortcuts)
337
+ openbroker spot-buy --coin PURR --size 1000
338
+ openbroker spot-sell --coin HYPE --size 5
339
+
340
+ # Full form (specify --side)
341
+ openbroker spot-order --coin PURR --side buy --size 1000
342
+
343
+ # Limit orders (add --price)
344
+ openbroker spot-order --coin HYPE --side sell --size 50 --price 25.50
345
+ openbroker spot-order --coin PURR --side buy --size 500 --price 0.20 --tif Alo
346
+
347
+ # Preview without executing
348
+ openbroker spot-buy --coin PURR --size 500 --dry
349
+ ```
350
+
351
+ **Spot flags:** `--coin`, `--side`, `--size`, `--price` (omit → market order), `--tif` (`Gtc`/`Ioc`/`Alo`, default `Gtc`), `--slippage` (bps, market orders only), `--dry`, `--verbose`.
352
+
296
353
  ## Advanced Execution
297
354
 
298
355
  ### TWAP (Native Exchange Order)
@@ -303,6 +360,9 @@ openbroker twap --coin ETH --side buy --size 1 --duration 30
303
360
  # Sell 0.5 BTC over 2 hours without randomized timing
304
361
  openbroker twap --coin BTC --side sell --size 0.5 --duration 120 --randomize false
305
362
 
363
+ # Reduce-only (close position with TWAP). Note: TWAP uses `--reduce-only`, not `--reduce`
364
+ openbroker twap --coin ETH --side sell --size 1 --duration 30 --reduce-only
365
+
306
366
  # Cancel a running TWAP
307
367
  openbroker twap-cancel --coin ETH --twap-id 77738308
308
368
 
@@ -407,6 +467,7 @@ Run `openbroker setup` to create the global config interactively.
407
467
  | `HYPERLIQUID_PRIVATE_KEY` | Yes | Wallet private key (0x...) |
408
468
  | `HYPERLIQUID_NETWORK` | No | `mainnet` (default) or `testnet` |
409
469
  | `HYPERLIQUID_ACCOUNT_ADDRESS` | No | Master account address (required for API wallets) |
470
+ | `OB_DASHBOARD_URL` | No | Dashboard API URL for forwarding audit notes, metrics, and agent actions (e.g. `http://localhost:3001`) |
410
471
 
411
472
  The builder fee (1 bps / 0.01%) is hardcoded and not configurable.
412
473
 
@@ -591,6 +652,8 @@ export default function(api) {
591
652
 
592
653
  Automations now write a local audit trail automatically to `~/.openbroker/automation-audit.sqlite`. The runtime records run config, logs, state changes, write actions, order updates, fills, user events, and per-poll account snapshots so you can generate performance reports later.
593
654
 
655
+ **Dashboard Forwarding:** When `OB_DASHBOARD_URL` is set (e.g. `http://localhost:3001`), audit notes, metrics, and trade actions are automatically forwarded to the OpenBroker Vaults dashboard API in real time. The vault address is read from `HYPERSTABLE_VAULT_ADDRESS` or `VAULT`. Forwarding is fire-and-forget — it never blocks the automation or causes errors if the dashboard is unreachable.
656
+
594
657
  ### Events
595
658
 
596
659
  | Event | Payload | When |
@@ -1053,10 +1116,14 @@ export default function(api) {
1053
1116
  openbroker auto run my-strategy --dry # Test without trading
1054
1117
  openbroker auto run ./funding-scalp.ts # Run from path
1055
1118
  openbroker auto run my-strategy --poll 5000 # Poll every 5s
1119
+ openbroker auto run my-strategy --no-ws # Disable WebSocket, pure REST polling
1056
1120
  openbroker auto run --example dca --set coin=HYPE --set amount=50 --dry # Run bundled example
1057
1121
  openbroker auto examples # List bundled examples with config
1058
1122
  openbroker auto list # Show available scripts
1059
1123
  openbroker auto status # Show running automations
1124
+ openbroker auto stop <id> # Unregister an automation (won't auto-restart)
1125
+ openbroker auto report <id> # Read the local audit report (logs, trades, metrics) for an automation
1126
+ openbroker auto clean # Remove stale entries from the registry
1060
1127
  ```
1061
1128
 
1062
1129
  **Plugin tools (for OpenClaw agents):**
@@ -1072,9 +1139,14 @@ openbroker auto status # Show running automations
1072
1139
  | `--verbose` | Show debug output | false |
1073
1140
  | `--id <name>` | Custom automation ID | filename |
1074
1141
  | `--poll <ms>` | Poll interval in milliseconds | 10000 |
1142
+ | `--no-ws` | Disable WebSocket; fall back to REST-only polling | WebSocket on |
1075
1143
  | `--example <name>` | Run a bundled example automation | - |
1076
1144
  | `--set key=value` | Set config values (repeatable) | - |
1077
1145
 
1146
+ **Inspecting automations after they run:**
1147
+ - `openbroker auto report <id>` — reads the local SQLite audit trail at `~/.openbroker/automation-audit.sqlite` and prints a summary of logs, write actions, fills, PnL, and custom metrics recorded via `api.audit.record()` / `api.audit.metric()`. Use this to review what a strategy actually did.
1148
+ - `openbroker auto clean` — prunes registry entries for automations that are no longer running or whose script file is gone. Safe to run anytime.
1149
+
1078
1150
  **Guidelines for agents writing automations:**
1079
1151
 
1080
1152
  **Risk & Safety (mandatory):**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.82",
4
+ "version": "1.0.87",
5
5
  "description": "Trade on Hyperliquid DEX with background position monitoring",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.0.82",
3
+ "version": "1.0.87",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@ Options (for run):
32
32
  --verbose Show debug output
33
33
  --id <name> Custom automation ID (default: filename)
34
34
  --poll <ms> Poll interval in milliseconds (default: 10000)
35
+ --no-ws Disable WebSocket; fall back to REST-only polling
35
36
 
36
37
  Scripts are loaded from:
37
38
  1. Absolute or relative path
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Dashboard audit forwarder.
3
+ *
4
+ * When OB_DASHBOARD_URL is set, wraps the AutomationAudit API to also POST
5
+ * audit notes, metrics, and agent action logs to the ob-app backend.
6
+ *
7
+ * Fires HTTP requests in the background — never blocks the automation loop.
8
+ */
9
+
10
+ import type { AutomationAudit } from './types.js';
11
+
12
+ const DASHBOARD_URL = process.env.OB_DASHBOARD_URL; // e.g. "http://localhost:3001"
13
+ const DASHBOARD_API_KEY = process.env.OB_DASHBOARD_API_KEY || '';
14
+ const VAULT_ADDRESS = process.env.HYPERSTABLE_VAULT_ADDRESS || process.env.VAULT || '';
15
+
16
+ function postJSON(path: string, body: unknown): void {
17
+ if (!DASHBOARD_URL || !VAULT_ADDRESS) return;
18
+
19
+ const url = `${DASHBOARD_URL}/api/vaults/${VAULT_ADDRESS.toLowerCase()}${path}`;
20
+
21
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
22
+ if (DASHBOARD_API_KEY) {
23
+ headers['Authorization'] = `Bearer ${DASHBOARD_API_KEY}`;
24
+ }
25
+
26
+ fetch(url, {
27
+ method: 'POST',
28
+ headers,
29
+ body: JSON.stringify(body),
30
+ signal: AbortSignal.timeout(5_000),
31
+ }).catch(() => {
32
+ // Silently ignore — dashboard may be down, automation must not be affected.
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Wrap an existing AutomationAudit to forward calls to the dashboard API.
38
+ * If OB_DASHBOARD_URL is not set, returns the original audit object unchanged.
39
+ */
40
+ export function withDashboardForwarder(audit: AutomationAudit): AutomationAudit {
41
+ if (!DASHBOARD_URL || !VAULT_ADDRESS) return audit;
42
+
43
+ return {
44
+ record(kind: string, payload?: unknown): void {
45
+ audit.record(kind, payload);
46
+ postJSON('/audit/notes', {
47
+ category: kind,
48
+ label: typeof payload === 'object' && payload !== null && 'reason' in payload
49
+ ? String((payload as Record<string, unknown>).reason)
50
+ : kind,
51
+ data: payload ?? {},
52
+ });
53
+ },
54
+
55
+ metric(name: string, value: number, tags?: Record<string, unknown>): void {
56
+ audit.metric(name, value, tags);
57
+ postJSON('/audit/metrics', {
58
+ name,
59
+ value,
60
+ tags: tags ?? {},
61
+ });
62
+ },
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Forward an agent action log to the dashboard.
68
+ * Call this from audited client wrappers or directly from automation code.
69
+ */
70
+ export function forwardAgentAction(
71
+ action: string,
72
+ status: 'success' | 'error' | 'pending',
73
+ details: Record<string, unknown>,
74
+ txHash?: string,
75
+ ): void {
76
+ postJSON('/agent/logs', { action, status, details, txHash });
77
+ }
@@ -22,8 +22,10 @@ export function resolveScriptPath(nameOrPath: string): string {
22
22
  return nameOrPath;
23
23
  }
24
24
 
25
- // Relative to cwd
26
- const cwdPath = path.resolve(process.cwd(), nameOrPath);
25
+ // Relative to the user's original cwd (not the openbroker package root that
26
+ // `bin/openbroker.js` chdirs to before spawning tsx).
27
+ const userCwd = process.env.OPENBROKER_CWD || process.cwd();
28
+ const cwdPath = path.resolve(userCwd, nameOrPath);
27
29
  if (existsSync(cwdPath)) return cwdPath;
28
30
 
29
31
  // Relative to ~/.openbroker/automations/
@@ -338,12 +338,12 @@ function renderTextReport(data: ReturnType<typeof loadReport>, watchMode = false
338
338
  console.log('\nEquity');
339
339
  console.log('------');
340
340
  if (report.equity.first) {
341
- console.log(`First snapshot: ${formatUsd(report.equity.first.equity)} @ ${formatTimestamp(Number(report.equity.first.timestamp))}`);
341
+ console.log(`First snapshot: ${formatUsd(Number(report.equity.first.equity))} @ ${formatTimestamp(Number(report.equity.first.timestamp))}`);
342
342
  } else {
343
343
  console.log('First snapshot: -');
344
344
  }
345
345
  if (report.equity.latest) {
346
- console.log(`Latest snapshot:${formatUsd(report.equity.latest.equity)} @ ${formatTimestamp(Number(report.equity.latest.timestamp))}`);
346
+ console.log(`Latest snapshot:${formatUsd(Number(report.equity.latest.equity))} @ ${formatTimestamp(Number(report.equity.latest.timestamp))}`);
347
347
  } else {
348
348
  console.log('Latest snapshot:-');
349
349
  }
@@ -15,6 +15,7 @@ import { AutomationEventBus } from './events.js';
15
15
  import { loadAutomation } from './loader.js';
16
16
  import { registerAutomation, unregisterAutomation, getRegisteredAutomations as getRegisteredFromFile } from './registry.js';
17
17
  import { createAutomationAudit, toSerializable, type AutomationAuditSink } from './audit.js';
18
+ import { withDashboardForwarder, forwardAgentAction } from './dashboard-forwarder.js';
18
19
  import type {
19
20
  AutomationAPI,
20
21
  AutomationEventPayloads,
@@ -178,6 +179,11 @@ function createAuditedClient(
178
179
  result,
179
180
  dryRun,
180
181
  });
182
+ forwardAgentAction(
183
+ prop,
184
+ 'success',
185
+ { args: toSerializable(args), result: toSerializable(result), dryRun },
186
+ );
181
187
  return result;
182
188
  } catch (error) {
183
189
  audit.recordAction({
@@ -187,6 +193,11 @@ function createAuditedClient(
187
193
  error,
188
194
  dryRun,
189
195
  });
196
+ forwardAgentAction(
197
+ prop,
198
+ 'error',
199
+ { args: toSerializable(args), error: String(error), dryRun },
200
+ );
190
201
  throw error;
191
202
  }
192
203
  };
@@ -424,10 +435,10 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
424
435
 
425
436
  // Build the API object
426
437
  const publish = createAuditedPublish(createPublish(id, log, gatewayPort, hooksToken), audit);
427
- const auditApi: AutomationAudit = {
438
+ const auditApi: AutomationAudit = withDashboardForwarder({
428
439
  record: (kind: string, payload?: unknown) => audit.recordNote(kind, payload),
429
440
  metric: (name: string, value: number, tags?: Record<string, unknown>) => audit.recordMetric(name, value, tags),
430
- };
441
+ });
431
442
  const api: AutomationAPI = {
432
443
  client,
433
444
  utils: { roundPrice, roundSize, sleep, normalizeCoin, formatUsd, formatPercent, annualizeFundingRate },
@@ -558,21 +569,19 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
558
569
  }, 'ws');
559
570
  }
560
571
 
561
- // Also emit order_filled for backward compatibility
562
- if (update.status === 'filled' && eventBus.has('order_filled')) {
563
- void emitAutomationEvent('order_filled', {
564
- coin: update.order.coin,
565
- oid: update.order.oid,
566
- side: update.order.side === 'B' ? 'buy' : 'sell',
567
- size: parseFloat(update.order.sz),
568
- price: parseFloat(update.order.limitPx),
569
- }, 'ws');
570
- }
572
+ // NOTE: order_filled is emitted from the userFill handler below, not from
573
+ // here. The previous implementation fired it from orderUpdate.status
574
+ // === 'filled' using update.order.sz as the size, but that field is the
575
+ // REMAINING size (0 on a terminal fill), not the fill delta — so every
576
+ // consumer saw size=0. Additionally, Hyperliquid does not emit
577
+ // orderUpdate events for pure partial fills that don't transition
578
+ // status, so partial fills were silently dropped entirely. Sourcing
579
+ // order_filled from userFill fixes both issues: sz there IS the fill
580
+ // delta, and the userFills stream fires on every fill (partial and
581
+ // terminal).
571
582
  });
572
583
 
573
584
  ws.on('userFill', (fill) => {
574
- // userFill events are already covered by order_update with status=filled
575
- // But this provides the realized PnL and fee data that order_update doesn't have
576
585
  audit.recordFill({
577
586
  coin: fill.coin,
578
587
  side: fill.side === 'B' ? 'buy' : 'sell',
@@ -585,6 +594,25 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
585
594
  crossed: fill.crossed,
586
595
  }, fill.time);
587
596
  log.debug(`Fill: ${fill.side === 'B' ? 'BUY' : 'SELL'} ${fill.sz} ${fill.coin} @ ${fill.px} (PnL: ${fill.closedPnl})`);
597
+
598
+ // Emit order_filled with the authoritative fill delta + fee/pnl from
599
+ // the userFills WS stream. Covers both partial and terminal fills.
600
+ if (eventBus.has('order_filled')) {
601
+ const size = parseFloat(fill.sz);
602
+ const price = parseFloat(fill.px);
603
+ const fee = parseFloat(fill.fee);
604
+ const closedPnl = parseFloat(fill.closedPnl);
605
+ void emitAutomationEvent('order_filled', {
606
+ coin: fill.coin,
607
+ oid: fill.oid,
608
+ side: fill.side === 'B' ? 'buy' : 'sell',
609
+ size,
610
+ price,
611
+ fee: Number.isFinite(fee) ? fee : undefined,
612
+ closedPnl: Number.isFinite(closedPnl) ? closedPnl : undefined,
613
+ crossed: fill.crossed,
614
+ }, 'ws');
615
+ }
588
616
  });
589
617
 
590
618
  ws.on('userEvent', (event) => {
@@ -46,7 +46,23 @@ export interface AutomationEventPayloads {
46
46
  position_changed: { coin: string; oldSize: number; newSize: number; entryPrice: number };
47
47
  pnl_threshold: { coin: string; unrealizedPnl: number; changePct: number; positionValue: number };
48
48
  margin_warning: { marginUsedPct: number; equity: number; marginUsed: number };
49
- order_filled: { coin: string; oid: number; side: 'buy' | 'sell'; size: number; price: number };
49
+ /**
50
+ * Fires on every trade fill — partial and terminal — sourced from the
51
+ * Hyperliquid `userFills` WS stream. `size` is the fill delta (NOT remaining
52
+ * size of the order). `fee` and `closedPnl` are in USD; `crossed` is true
53
+ * when this side was the taker. Fee/pnl/crossed are optional so that older
54
+ * consumers that only read coin/oid/side/size/price keep working.
55
+ */
56
+ order_filled: {
57
+ coin: string;
58
+ oid: number;
59
+ side: 'buy' | 'sell';
60
+ size: number;
61
+ price: number;
62
+ fee?: number;
63
+ closedPnl?: number;
64
+ crossed?: boolean;
65
+ };
50
66
  /** Real-time order lifecycle event via WebSocket (filled, canceled, rejected, triggered, etc.) */
51
67
  order_update: {
52
68
  coin: string;