openbroker 1.0.70 → 1.0.72

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/README.md CHANGED
@@ -383,16 +383,19 @@ openbroker cancel --all --dry # Preview what would be cancelled
383
383
 
384
384
  ### Advanced Execution
385
385
 
386
- #### `twap` — Time-Weighted Average Price
386
+ #### `twap` — Native TWAP Order
387
387
 
388
- Split a large order into smaller slices executed at regular intervals to minimize market impact.
388
+ Place a native Hyperliquid TWAP order. The exchange handles order slicing and execution timing server-side returns immediately with a TWAP ID.
389
389
 
390
390
  ```bash
391
- # Buy 1 ETH over 1 hour (auto ~12 slices)
392
- openbroker twap --coin ETH --side buy --size 1 --duration 3600
391
+ # Buy 1 ETH over 30 minutes
392
+ openbroker twap --coin ETH --side buy --size 1 --duration 30
393
393
 
394
- # Sell 0.5 BTC over 30 min, 6 slices, randomized timing
395
- openbroker twap --coin BTC --side sell --size 0.5 --duration 1800 --intervals 6 --randomize 20
394
+ # Sell 0.5 BTC over 2 hours without randomized timing
395
+ openbroker twap --coin BTC --side sell --size 0.5 --duration 120 --randomize false
396
+
397
+ # Preview order details
398
+ openbroker twap --coin ETH --side buy --size 2 --duration 60 --dry
396
399
  ```
397
400
 
398
401
  | Flag | Description | Default |
@@ -400,14 +403,38 @@ openbroker twap --coin BTC --side sell --size 0.5 --duration 1800 --intervals 6
400
403
  | `--coin` | Asset to trade | **required** |
401
404
  | `--side` | `buy` or `sell` | **required** |
402
405
  | `--size` | Total order size in base asset | **required** |
403
- | `--duration` | Total execution time in seconds | **required** |
404
- | `--intervals` | Number of slices | 1 per 5 min |
405
- | `--randomize` | Randomize timing by ±X percent | `0` |
406
- | `--slippage` | Slippage per slice in bps | `50` |
407
- | `--dry` | Show execution plan without trading | — |
406
+ | `--duration` | Duration in minutes (5–1440) | **required** |
407
+ | `--randomize` | Randomize execution timing | `true` |
408
+ | `--reduce-only` | Reduce-only order | `false` |
409
+ | `--leverage` | Set leverage before placing | |
410
+ | `--dry` | Show order details without placing | — |
408
411
  | `--verbose` | Show debug output | — |
409
412
 
410
- Reports VWAP, actual slippage, fill rate, and total execution time.
413
+ #### `twap-cancel` Cancel TWAP Order
414
+
415
+ Cancel a running native TWAP order by its TWAP ID.
416
+
417
+ ```bash
418
+ openbroker twap-cancel --coin ETH --twap-id 77738308
419
+ ```
420
+
421
+ | Flag | Description | Default |
422
+ |------|-------------|---------|
423
+ | `--coin` | Asset symbol | **required** |
424
+ | `--twap-id` | TWAP order ID to cancel | **required** |
425
+
426
+ #### `twap-status` — TWAP Order Status
427
+
428
+ View TWAP order history and currently running TWAP orders.
429
+
430
+ ```bash
431
+ openbroker twap-status # All TWAP history
432
+ openbroker twap-status --active # Only running TWAPs
433
+ ```
434
+
435
+ | Flag | Description | Default |
436
+ |------|-------------|---------|
437
+ | `--active` | Show only active/running TWAP orders | — |
411
438
 
412
439
  #### `scale` — Scale In/Out
413
440
 
@@ -724,7 +751,9 @@ When loaded, the plugin registers these agent tools:
724
751
  | Trading | `ob_trigger` | Trigger order (TP/SL) |
725
752
  | Trading | `ob_tpsl` | Set TP/SL on existing position |
726
753
  | Trading | `ob_cancel` | Cancel orders |
727
- | Advanced | `ob_twap` | TWAP execution |
754
+ | Advanced | `ob_twap` | Native TWAP order (exchange-managed) |
755
+ | Advanced | `ob_twap_cancel` | Cancel a running TWAP order |
756
+ | Advanced | `ob_twap_status` | View TWAP order history/status |
728
757
  | Advanced | `ob_bracket` | Entry + TP + SL |
729
758
  | Advanced | `ob_chase` | Chase price with ALO orders |
730
759
  | Monitoring | `ob_watcher_status` | Background watcher state |
package/SKILL.md CHANGED
@@ -4,8 +4,8 @@ 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.70", "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
- 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_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
7
+ metadata: {"author": "monemetrics", "version": "1.0.72", "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
+ 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
 
11
11
  # Open Broker - Hyperliquid Trading CLI
@@ -73,7 +73,7 @@ If an `ob_*` plugin tool returns unexpected errors, empty results, or crashes, *
73
73
  **When to use CLI fallback:**
74
74
  - Plugin tool returns `null`, empty data, or throws an error
75
75
  - You need data the plugin tool doesn't expose (e.g., `--verbose` debug output)
76
- - Long-running operations (strategies, TWAP) — the CLI handles timeouts and progress better
76
+ - Long-running operations (strategies) — the CLI handles timeouts and progress better
77
77
 
78
78
  Add `--dry` to any trading CLI command to preview without executing. Add `--json` to info commands for structured output.
79
79
 
@@ -288,13 +288,19 @@ openbroker cancel --oid 123456 # Cancel specific order
288
288
 
289
289
  ## Advanced Execution
290
290
 
291
- ### TWAP (Time-Weighted Average Price)
291
+ ### TWAP (Native Exchange Order)
292
292
  ```bash
293
- # Execute 1 ETH buy over 1 hour (auto-calculates slices)
294
- openbroker twap --coin ETH --side buy --size 1 --duration 3600
293
+ # Buy 1 ETH over 30 minutes (exchange handles slicing)
294
+ openbroker twap --coin ETH --side buy --size 1 --duration 30
295
295
 
296
- # Custom intervals with randomization
297
- openbroker twap --coin BTC --side sell --size 0.5 --duration 1800 --intervals 6 --randomize 20
296
+ # Sell 0.5 BTC over 2 hours without randomized timing
297
+ openbroker twap --coin BTC --side sell --size 0.5 --duration 120 --randomize false
298
+
299
+ # Cancel a running TWAP
300
+ openbroker twap-cancel --coin ETH --twap-id 77738308
301
+
302
+ # Check TWAP status
303
+ openbroker twap-status --active
298
304
  ```
299
305
 
300
306
  ### Scale In/Out (Grid Orders)
@@ -430,15 +436,66 @@ The plugin reads wallet credentials from `~/.openbroker/.env` (set up by `openbr
430
436
 
431
437
  ### Webhook setup for watcher alerts
432
438
 
433
- For position alerts to reach the agent, enable hooks in your gateway config:
439
+ For the position watcher and automations to send alerts to the agent, you must enable webhooks in your OpenClaw gateway config and add a hook mapping. This is a manual configuration step — plugins cannot auto-configure gateway settings.
440
+
441
+ **1. Generate a hook token** — any secure random string:
442
+ ```bash
443
+ openssl rand -hex 32
444
+ ```
445
+
446
+ **2. Enable hooks and add a mapping** in your `openclaw.json` (or `openclaw.yaml`) deployment config:
447
+ ```json
448
+ "hooks": {
449
+ "enabled": true,
450
+ "path": "/hooks",
451
+ "token": "<your-generated-token>",
452
+ "allowedAgentIds": ["hooks", "main", "openbroker"],
453
+ "mappings": [
454
+ {
455
+ "id": "main",
456
+ "match": {
457
+ "path": "openbroker"
458
+ },
459
+ "action": "agent",
460
+ "wakeMode": "now",
461
+ "name": "Openbroker",
462
+ "agentId": "main",
463
+ "deliver": true,
464
+ "channel": "last",
465
+ "model": "anthropic/claude-sonnet-4-6"
466
+ }
467
+ ]
468
+ }
469
+ ```
434
470
 
471
+ | Field | Description |
472
+ |-------|-------------|
473
+ | `token` | Shared secret — must match `hooksToken` in the plugin config |
474
+ | `allowedAgentIds` | Agent IDs allowed to receive webhook requests |
475
+ | `mappings[].match.path` | Matches the webhook path sent by the plugin (always `"openbroker"`) |
476
+ | `mappings[].wakeMode` | `"now"` triggers an immediate agent turn. `"next-heartbeat"` queues for the next scheduled heartbeat |
477
+ | `mappings[].deliver` | If `true`, the agent's response is delivered to the user via the configured channel |
478
+ | `mappings[].channel` | Delivery channel: `"last"` (most recent), `"slack"`, `"telegram"`, `"discord"`, `"whatsapp"`, etc. |
479
+ | `mappings[].model` | Model override for webhook-triggered turns. Optional — uses deployment default if omitted |
480
+
481
+ **3. Set the same token in your plugin config:**
435
482
  ```yaml
436
- hooks:
437
- enabled: true
438
- token: "your-hooks-secret" # Must match hooksToken above
483
+ plugins:
484
+ entries:
485
+ openbroker:
486
+ enabled: true
487
+ config:
488
+ hooksToken: "<your-generated-token>" # Same token as hooks.token
489
+ watcher:
490
+ enabled: true
491
+ ```
492
+
493
+ **4. Restart the gateway** and verify:
494
+ ```bash
495
+ openclaw ob status
439
496
  ```
440
497
 
441
- Without hooks, the watcher still runs and tracks state (accessible via `ob_watcher_status`), but it can't wake the agent.
498
+ The watcher sends alerts to `POST /hooks/agent` with `Authorization: Bearer <token>`. The gateway matches the request against the mapping and triggers an agent turn. Without hooks enabled, the watcher still tracks state (accessible via `ob_watcher_status`), but it can't wake the agent.
442
499
 
443
500
  ### Using with or without the plugin
444
501
 
package/bin/cli.ts CHANGED
@@ -41,7 +41,9 @@ const commands: Record<string, { script: string; description: string }> = {
41
41
  'trigger': { script: 'operations/trigger-order.ts', description: 'Trigger order (TP/SL)' },
42
42
  'tpsl': { script: 'operations/set-tpsl.ts', description: 'Set TP/SL on position' },
43
43
  'cancel': { script: 'operations/cancel.ts', description: 'Cancel orders' },
44
- 'twap': { script: 'operations/twap.ts', description: 'TWAP execution' },
44
+ 'twap': { script: 'operations/twap.ts', description: 'Native TWAP order' },
45
+ 'twap-cancel': { script: 'operations/twap-cancel.ts', description: 'Cancel a TWAP order' },
46
+ 'twap-status': { script: 'operations/twap-status.ts', description: 'View TWAP order status' },
45
47
  'scale': { script: 'operations/scale.ts', description: 'Scale in/out orders' },
46
48
  'bracket': { script: 'operations/bracket.ts', description: 'Bracket order (entry + TP + SL)' },
47
49
  'chase': { script: 'operations/chase.ts', description: 'Chase order with ALO' },
@@ -87,7 +89,9 @@ Trading Commands:
87
89
  cancel Cancel orders
88
90
 
89
91
  Advanced Execution:
90
- twap Time-weighted average price execution
92
+ twap Native TWAP order (exchange-managed)
93
+ twap-cancel Cancel a running TWAP order
94
+ twap-status View TWAP order history/status
91
95
  scale Scale in/out with multiple orders
92
96
  bracket Entry with TP and SL
93
97
  chase Chase price with ALO orders
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.70",
4
+ "version": "1.0.72",
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.70",
3
+ "version": "1.0.72",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,6 +35,8 @@
35
35
  "set-tpsl": "tsx scripts/operations/set-tpsl.ts",
36
36
  "cancel": "tsx scripts/operations/cancel.ts",
37
37
  "twap": "tsx scripts/operations/twap.ts",
38
+ "twap-cancel": "tsx scripts/operations/twap-cancel.ts",
39
+ "twap-status": "tsx scripts/operations/twap-status.ts",
38
40
  "scale": "tsx scripts/operations/scale.ts",
39
41
  "bracket": "tsx scripts/operations/bracket.ts",
40
42
  "chase": "tsx scripts/operations/chase.ts",
@@ -1638,6 +1638,90 @@ export class HyperliquidClient {
1638
1638
  throw error;
1639
1639
  }
1640
1640
  }
1641
+
1642
+ /**
1643
+ * Place a native Hyperliquid TWAP order.
1644
+ * The exchange handles slicing and timing server-side.
1645
+ * @param coin Asset symbol (e.g. "ETH")
1646
+ * @param isBuy true for long, false for short
1647
+ * @param size Total size in base currency
1648
+ * @param durationMinutes Duration in minutes (5–1440)
1649
+ * @param randomize Enable random order timing
1650
+ * @param reduceOnly Reduce-only flag
1651
+ * @param leverage Optional leverage to set before placing the TWAP
1652
+ */
1653
+ async twapOrder(
1654
+ coin: string,
1655
+ isBuy: boolean,
1656
+ size: number,
1657
+ durationMinutes: number,
1658
+ randomize: boolean = true,
1659
+ reduceOnly: boolean = false,
1660
+ leverage?: number
1661
+ ) {
1662
+ await this.getMetaAndAssetCtxs();
1663
+
1664
+ if (leverage) {
1665
+ await this.setLeverage(coin, leverage);
1666
+ }
1667
+
1668
+ const assetIndex = this.getAssetIndex(coin);
1669
+ const roundedSize = roundSize(size, this.getSzDecimals(coin));
1670
+
1671
+ this.log(`TWAP order: ${coin} (asset ${assetIndex}) ${isBuy ? 'BUY' : 'SELL'} ${roundedSize} over ${durationMinutes}m, randomize=${randomize}, reduceOnly=${reduceOnly}`);
1672
+
1673
+ try {
1674
+ const response = await this.exchange.twapOrder({
1675
+ twap: {
1676
+ a: assetIndex,
1677
+ b: isBuy,
1678
+ s: String(roundedSize),
1679
+ r: reduceOnly,
1680
+ m: durationMinutes,
1681
+ t: randomize,
1682
+ },
1683
+ });
1684
+ this.log('TWAP order response:', JSON.stringify(response, null, 2));
1685
+ return response;
1686
+ } catch (error) {
1687
+ this.log('TWAP order error:', error);
1688
+ throw error;
1689
+ }
1690
+ }
1691
+
1692
+ /**
1693
+ * Cancel a running TWAP order.
1694
+ * @param coin Asset symbol (e.g. "ETH")
1695
+ * @param twapId The TWAP order ID to cancel
1696
+ */
1697
+ async twapCancel(coin: string, twapId: number) {
1698
+ await this.getMetaAndAssetCtxs();
1699
+
1700
+ const assetIndex = this.getAssetIndex(coin);
1701
+
1702
+ this.log(`TWAP cancel: ${coin} (asset ${assetIndex}) twapId=${twapId}`);
1703
+
1704
+ try {
1705
+ const response = await this.exchange.twapCancel({
1706
+ a: assetIndex,
1707
+ t: twapId,
1708
+ });
1709
+ this.log('TWAP cancel response:', JSON.stringify(response, null, 2));
1710
+ return response;
1711
+ } catch (error) {
1712
+ this.log('TWAP cancel error:', error);
1713
+ throw error;
1714
+ }
1715
+ }
1716
+
1717
+ /**
1718
+ * Get TWAP order history for the current user.
1719
+ */
1720
+ async twapHistory() {
1721
+ const response = await this.info.twapHistory({ user: this.address as `0x${string}` });
1722
+ this.log('TWAP history:', JSON.stringify(response, null, 2));
1723
+ return response;
1724
+ }
1641
1725
  }
1642
1726
 
1643
1727
  // Singleton instance
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env npx tsx
2
+ // Cancel a running TWAP order
3
+
4
+ import { getClient } from '../core/client.js';
5
+ import { parseArgs } from '../core/utils.js';
6
+
7
+ function printUsage() {
8
+ console.log(`
9
+ Open Broker - Cancel TWAP Order
10
+ ================================
11
+
12
+ Cancel a running native Hyperliquid TWAP order.
13
+
14
+ Usage:
15
+ npx tsx scripts/operations/twap-cancel.ts --coin <COIN> --twap-id <ID>
16
+
17
+ Options:
18
+ --coin Asset symbol (e.g., ETH, BTC)
19
+ --twap-id TWAP order ID to cancel
20
+ --verbose Show debug output
21
+
22
+ Examples:
23
+ npx tsx scripts/operations/twap-cancel.ts --coin ETH --twap-id 77738308
24
+ `);
25
+ }
26
+
27
+ async function main() {
28
+ const args = parseArgs(process.argv.slice(2));
29
+
30
+ const coin = args.coin as string;
31
+ const twapId = args['twap-id'] ? parseInt(args['twap-id'] as string) : NaN;
32
+
33
+ if (!coin || isNaN(twapId)) {
34
+ printUsage();
35
+ process.exit(1);
36
+ }
37
+
38
+ const client = getClient();
39
+
40
+ if (args.verbose) {
41
+ client.verbose = true;
42
+ }
43
+
44
+ console.log('Open Broker - Cancel TWAP Order');
45
+ console.log('===============================\n');
46
+
47
+ try {
48
+ console.log(`Cancelling TWAP ${twapId} for ${coin}...`);
49
+
50
+ const response = await client.twapCancel(coin, twapId);
51
+
52
+ const status = response.response.data.status;
53
+ if (typeof status === 'string' && status === 'success') {
54
+ console.log(`\nTWAP order ${twapId} cancelled successfully.`);
55
+ } else if (typeof status === 'object' && 'error' in status) {
56
+ console.error(`\nFailed to cancel TWAP: ${status.error}`);
57
+ process.exit(1);
58
+ }
59
+ } catch (error) {
60
+ console.error('Error:', error instanceof Error ? error.message : error);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ main();
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env npx tsx
2
+ // View TWAP order history and status
3
+
4
+ import { getClient } from '../core/client.js';
5
+ import { formatUsd, parseArgs } from '../core/utils.js';
6
+
7
+ function printUsage() {
8
+ console.log(`
9
+ Open Broker - TWAP Status
10
+ ==========================
11
+
12
+ View your TWAP order history and currently running TWAP orders.
13
+
14
+ Usage:
15
+ npx tsx scripts/operations/twap-status.ts [--active]
16
+
17
+ Options:
18
+ --active Show only active (running) TWAP orders
19
+ --verbose Show debug output
20
+
21
+ Examples:
22
+ npx tsx scripts/operations/twap-status.ts # All TWAP history
23
+ npx tsx scripts/operations/twap-status.ts --active # Only running TWAPs
24
+ `);
25
+ }
26
+
27
+ async function main() {
28
+ const args = parseArgs(process.argv.slice(2));
29
+
30
+ if (args.help) {
31
+ printUsage();
32
+ process.exit(0);
33
+ }
34
+
35
+ const activeOnly = args.active as boolean;
36
+ const client = getClient();
37
+
38
+ if (args.verbose) {
39
+ client.verbose = true;
40
+ }
41
+
42
+ console.log('Open Broker - TWAP Status');
43
+ console.log('=========================\n');
44
+
45
+ try {
46
+ const history = await client.twapHistory();
47
+
48
+ if (history.length === 0) {
49
+ console.log('No TWAP orders found.');
50
+ return;
51
+ }
52
+
53
+ const filtered = activeOnly
54
+ ? history.filter(h => h.status.status === 'activated')
55
+ : history;
56
+
57
+ if (filtered.length === 0) {
58
+ console.log(activeOnly ? 'No active TWAP orders.' : 'No TWAP orders found.');
59
+ return;
60
+ }
61
+
62
+ console.log(`Found ${filtered.length} TWAP order${filtered.length > 1 ? 's' : ''}${activeOnly ? ' (active)' : ''}:\n`);
63
+
64
+ for (const entry of filtered) {
65
+ const { state, status, twapId } = entry;
66
+ const isBuy = state.side === 'B';
67
+ const executedSz = parseFloat(state.executedSz);
68
+ const totalSz = parseFloat(state.sz);
69
+ const executedNtl = parseFloat(state.executedNtl);
70
+ const avgPrice = executedSz > 0 ? executedNtl / executedSz : 0;
71
+ const pctDone = totalSz > 0 ? (executedSz / totalSz) * 100 : 0;
72
+
73
+ const statusLabel = status.status === 'activated' ? 'RUNNING'
74
+ : status.status === 'finished' ? 'FINISHED'
75
+ : status.status === 'terminated' ? 'CANCELLED'
76
+ : status.status === 'error' ? `ERROR: ${'description' in status ? status.description : ''}`
77
+ : status.status;
78
+
79
+ console.log(` ${twapId !== undefined ? `TWAP #${twapId}` : 'TWAP'} — ${state.coin} ${isBuy ? 'BUY' : 'SELL'}`);
80
+ console.log(` Status: ${statusLabel}`);
81
+ console.log(` Size: ${executedSz} / ${totalSz} (${pctDone.toFixed(1)}%)`);
82
+ if (avgPrice > 0) {
83
+ console.log(` Avg Price: ${formatUsd(avgPrice)}`);
84
+ console.log(` Notional: ${formatUsd(executedNtl)}`);
85
+ }
86
+ console.log(` Duration: ${state.minutes}m, Randomize: ${state.randomize ? 'yes' : 'no'}`);
87
+ console.log(` Started: ${new Date(state.timestamp).toLocaleString()}`);
88
+ console.log('');
89
+ }
90
+ } catch (error) {
91
+ console.error('Error:', error instanceof Error ? error.message : error);
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ main();
@@ -1,67 +1,56 @@
1
1
  #!/usr/bin/env npx tsx
2
- // TWAP (Time-Weighted Average Price) execution
2
+ // TWAP (Time-Weighted Average Price) execution using Hyperliquid's native TWAP orders
3
3
 
4
4
  import { getClient } from '../core/client.js';
5
- import { formatUsd, parseArgs, sleep } from '../core/utils.js';
5
+ import { formatUsd, parseArgs } from '../core/utils.js';
6
6
 
7
7
  function printUsage() {
8
8
  console.log(`
9
- Open Broker - TWAP Order
10
- ========================
9
+ Open Broker - TWAP Order (Native)
10
+ ==================================
11
11
 
12
- Execute a large order over time using Time-Weighted Average Price strategy.
13
- Splits the order into smaller chunks and executes at regular intervals.
12
+ Place a native Hyperliquid TWAP order. The exchange handles order slicing
13
+ and execution timing server-side.
14
14
 
15
15
  Usage:
16
- npx tsx scripts/operations/twap.ts --coin <COIN> --side <buy|sell> --size <SIZE> --duration <SECONDS>
16
+ npx tsx scripts/operations/twap.ts --coin <COIN> --side <buy|sell> --size <SIZE> --duration <MINUTES>
17
17
 
18
18
  Options:
19
19
  --coin Asset to trade (e.g., ETH, BTC)
20
20
  --side Order side: buy or sell
21
21
  --size Total order size in base asset
22
- --duration Total execution time in seconds (e.g., 3600 for 1 hour)
23
- --intervals Number of slices (default: calculates based on duration)
24
- --randomize Randomize timing by ±X percent (default: 0)
25
- --slippage Slippage tolerance in bps per slice (default: 50)
26
- --leverage Set leverage (e.g., 10 for 10x). Cross for main perps, isolated for HIP-3
27
- --dry Dry run - show execution plan without trading
22
+ --duration Total execution time in minutes (5–1440, i.e. 5 min to 24 hours)
23
+ --randomize Enable random order timing (default: true)
24
+ --reduce-only Reduce-only order (default: false)
25
+ --leverage Set leverage (e.g., 10 for 10x)
26
+ --dry Dry run - show order plan without executing
27
+ --verbose Show debug output
28
28
 
29
29
  Examples:
30
- # Execute 1 ETH buy over 1 hour (12 slices, every 5 min)
31
- npx tsx scripts/operations/twap.ts --coin ETH --side buy --size 1 --duration 3600
30
+ # Execute 1 ETH buy over 30 minutes
31
+ npx tsx scripts/operations/twap.ts --coin ETH --side buy --size 1 --duration 30
32
32
 
33
- # Execute 0.5 BTC sell over 30 min with 6 slices and 20% timing randomization
34
- npx tsx scripts/operations/twap.ts --coin BTC --side sell --size 0.5 --duration 1800 --intervals 6 --randomize 20
33
+ # Execute 0.5 BTC sell over 2 hours without randomized timing
34
+ npx tsx scripts/operations/twap.ts --coin BTC --side sell --size 0.5 --duration 120 --randomize false
35
35
 
36
36
  # Preview execution plan
37
- npx tsx scripts/operations/twap.ts --coin ETH --side buy --size 2 --duration 7200 --dry
37
+ npx tsx scripts/operations/twap.ts --coin ETH --side buy --size 2 --duration 60 --dry
38
38
  `);
39
39
  }
40
40
 
41
- interface TwapResult {
42
- slice: number;
43
- timestamp: Date;
44
- size: number;
45
- filled: number;
46
- avgPrice: number;
47
- status: 'filled' | 'partial' | 'failed';
48
- error?: string;
49
- }
50
-
51
41
  async function main() {
52
42
  const args = parseArgs(process.argv.slice(2));
53
43
 
54
44
  const coin = args.coin as string;
55
45
  const side = args.side as string;
56
46
  const totalSize = parseFloat(args.size as string);
57
- const duration = parseInt(args.duration as string);
58
- const intervals = args.intervals ? parseInt(args.intervals as string) : Math.max(6, Math.floor(duration / 300)); // default: 1 slice per 5 min
59
- const randomize = args.randomize ? parseInt(args.randomize as string) : 0;
60
- const slippage = args.slippage ? parseInt(args.slippage as string) : undefined;
47
+ const durationMinutes = parseInt(args.duration as string);
48
+ const randomize = args.randomize === 'false' || args.randomize === false ? false : true;
49
+ const reduceOnly = args['reduce-only'] as boolean || false;
61
50
  const leverage = args.leverage ? parseInt(args.leverage as string) : undefined;
62
51
  const dryRun = args.dry as boolean;
63
52
 
64
- if (!coin || !side || isNaN(totalSize) || isNaN(duration)) {
53
+ if (!coin || !side || isNaN(totalSize) || isNaN(durationMinutes)) {
65
54
  printUsage();
66
55
  process.exit(1);
67
56
  }
@@ -71,8 +60,13 @@ async function main() {
71
60
  process.exit(1);
72
61
  }
73
62
 
74
- if (totalSize <= 0 || duration <= 0 || intervals <= 0) {
75
- console.error('Error: size, duration, and intervals must be positive');
63
+ if (totalSize <= 0) {
64
+ console.error('Error: --size must be positive');
65
+ process.exit(1);
66
+ }
67
+
68
+ if (durationMinutes < 5 || durationMinutes > 1440) {
69
+ console.error('Error: --duration must be between 5 and 1440 minutes (5 min to 24 hours)');
76
70
  process.exit(1);
77
71
  }
78
72
 
@@ -83,11 +77,11 @@ async function main() {
83
77
  client.verbose = true;
84
78
  }
85
79
 
86
- console.log('Open Broker - TWAP Execution');
87
- console.log('============================\n');
80
+ console.log('Open Broker - Native TWAP Order');
81
+ console.log('===============================\n');
88
82
 
89
83
  try {
90
- // Get current price for estimates
84
+ // Get current price for display
91
85
  const mids = await client.getAllMids();
92
86
  const midPrice = parseFloat(mids[coin]);
93
87
  if (!midPrice) {
@@ -95,120 +89,53 @@ async function main() {
95
89
  process.exit(1);
96
90
  }
97
91
 
98
- const sliceSize = totalSize / intervals;
99
- const baseInterval = (duration * 1000) / intervals; // ms between slices
100
92
  const notional = midPrice * totalSize;
101
93
 
102
- console.log('Execution Plan');
103
- console.log('--------------');
94
+ console.log('Order Details');
95
+ console.log('-------------');
104
96
  console.log(`Coin: ${coin}`);
105
97
  console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
106
98
  console.log(`Total Size: ${totalSize}`);
107
99
  console.log(`Current Price: ${formatUsd(midPrice)}`);
108
100
  console.log(`Est. Notional: ${formatUsd(notional)}`);
109
- console.log(`Duration: ${formatDuration(duration)}`);
110
- console.log(`Intervals: ${intervals} slices`);
111
- console.log(`Size/Slice: ${sliceSize.toFixed(6)}`);
112
- console.log(`Time/Slice: ${formatDuration(duration / intervals)}`);
113
- if (randomize > 0) {
114
- console.log(`Randomization: ±${randomize}%`);
101
+ console.log(`Duration: ${formatDuration(durationMinutes * 60)}`);
102
+ console.log(`Randomize: ${randomize ? 'yes' : 'no'}`);
103
+ console.log(`Reduce Only: ${reduceOnly ? 'yes' : 'no'}`);
104
+ if (leverage) {
105
+ console.log(`Leverage: ${leverage}x`);
115
106
  }
116
107
 
117
108
  if (dryRun) {
118
- console.log('\n🔍 Dry run - showing execution schedule:\n');
119
- let time = 0;
120
- for (let i = 0; i < intervals; i++) {
121
- const jitter = randomize > 0 ? (Math.random() - 0.5) * 2 * (randomize / 100) : 0;
122
- const interval = baseInterval * (1 + jitter);
123
- console.log(` Slice ${i + 1}/${intervals}: ${sliceSize.toFixed(6)} @ T+${formatDuration(time / 1000)}`);
124
- time += interval;
125
- }
126
- console.log(`\n Total duration: ~${formatDuration(time / 1000)}`);
109
+ console.log('\nDry run - no order placed.');
110
+ console.log('The exchange will handle order slicing and timing automatically.');
127
111
  return;
128
112
  }
129
113
 
130
- console.log('\nExecuting...\n');
131
-
132
- const results: TwapResult[] = [];
133
- let totalFilled = 0;
134
- let totalCost = 0;
135
- let startTime = Date.now();
136
-
137
- for (let i = 0; i < intervals; i++) {
138
- const sliceNum = i + 1;
139
- console.log(`[${sliceNum}/${intervals}] Executing slice: ${sliceSize.toFixed(6)} ${coin}...`);
140
-
141
- const result: TwapResult = {
142
- slice: sliceNum,
143
- timestamp: new Date(),
144
- size: sliceSize,
145
- filled: 0,
146
- avgPrice: 0,
147
- status: 'failed',
148
- };
149
-
150
- try {
151
- const response = await client.marketOrder(coin, isBuy, sliceSize, slippage, leverage);
152
-
153
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
154
- const statuses = response.response.data.statuses;
155
- for (const status of statuses) {
156
- if (status.filled) {
157
- result.filled = parseFloat(status.filled.totalSz);
158
- result.avgPrice = parseFloat(status.filled.avgPx);
159
- result.status = result.filled >= sliceSize * 0.99 ? 'filled' : 'partial';
160
- totalFilled += result.filled;
161
- totalCost += result.filled * result.avgPrice;
162
- console.log(` ✅ Filled ${result.filled} @ ${formatUsd(result.avgPrice)}`);
163
- } else if (status.error) {
164
- result.status = 'failed';
165
- result.error = status.error;
166
- console.log(` ❌ Error: ${status.error}`);
167
- }
168
- }
169
- } else {
170
- result.status = 'failed';
171
- result.error = typeof response.response === 'string' ? response.response : 'Unknown error';
172
- console.log(` ❌ Failed: ${result.error}`);
173
- }
174
- } catch (err) {
175
- result.status = 'failed';
176
- result.error = err instanceof Error ? err.message : String(err);
177
- console.log(` ❌ Error: ${result.error}`);
178
- }
179
-
180
- results.push(result);
181
-
182
- // Wait for next interval (unless last slice)
183
- if (i < intervals - 1) {
184
- const jitter = randomize > 0 ? (Math.random() - 0.5) * 2 * (randomize / 100) : 0;
185
- const waitTime = Math.max(1000, baseInterval * (1 + jitter));
186
- console.log(` Waiting ${formatDuration(waitTime / 1000)} until next slice...\n`);
187
- await sleep(waitTime);
188
- }
114
+ console.log('\nPlacing native TWAP order...\n');
115
+
116
+ const response = await client.twapOrder(
117
+ coin,
118
+ isBuy,
119
+ totalSize,
120
+ durationMinutes,
121
+ randomize,
122
+ reduceOnly,
123
+ leverage,
124
+ );
125
+
126
+ const status = response.response.data.status;
127
+ if ('running' in status) {
128
+ console.log(`TWAP order placed successfully!`);
129
+ console.log(`TWAP ID: ${status.running.twapId}`);
130
+ console.log(`\nThe exchange is now executing your TWAP order over ${formatDuration(durationMinutes * 60)}.`);
131
+ console.log(`To cancel: openbroker twap-cancel --coin ${coin} --twap-id ${status.running.twapId}`);
132
+ console.log(`To check status: openbroker twap-status`);
133
+ } else if ('error' in status) {
134
+ console.error(`TWAP order failed: ${status.error}`);
135
+ process.exit(1);
189
136
  }
190
-
191
- // Summary
192
- const endTime = Date.now();
193
- const actualDuration = (endTime - startTime) / 1000;
194
- const vwap = totalCost / totalFilled;
195
- const currentPrice = parseFloat((await client.getAllMids())[coin]);
196
- const slippageVsMid = isBuy
197
- ? (vwap - midPrice) / midPrice
198
- : (midPrice - vwap) / midPrice;
199
-
200
- console.log('\n========== TWAP Summary ==========');
201
- console.log(`Total Filled: ${totalFilled.toFixed(6)} / ${totalSize} (${((totalFilled / totalSize) * 100).toFixed(1)}%)`);
202
- console.log(`VWAP: ${formatUsd(vwap)}`);
203
- console.log(`Start Price: ${formatUsd(midPrice)}`);
204
- console.log(`End Price: ${formatUsd(currentPrice)}`);
205
- console.log(`Slippage vs Mid: ${(slippageVsMid * 10000).toFixed(1)} bps`);
206
- console.log(`Total Cost: ${formatUsd(totalCost)}`);
207
- console.log(`Actual Duration: ${formatDuration(actualDuration)}`);
208
- console.log(`Successful: ${results.filter(r => r.status === 'filled').length}/${intervals} slices`);
209
-
210
137
  } catch (error) {
211
- console.error('Error:', error);
138
+ console.error('Error:', error instanceof Error ? error.message : error);
212
139
  process.exit(1);
213
140
  }
214
141
  }
@@ -1231,35 +1231,137 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
1231
1231
 
1232
1232
  {
1233
1233
  name: 'ob_twap',
1234
- description: 'Execute a TWAP (time-weighted average price) order, splitting a large order into smaller slices over time. This is a long-running command.',
1234
+ description: 'Place a native Hyperliquid TWAP order. The exchange handles order slicing and timing server-side. Returns immediately with a TWAP ID.',
1235
1235
  parameters: {
1236
1236
  type: 'object',
1237
1237
  properties: {
1238
- coin: { type: 'string', description: 'Asset symbol' },
1238
+ coin: { type: 'string', description: 'Asset symbol (e.g., ETH, BTC)' },
1239
1239
  side: { type: 'string', enum: ['buy', 'sell'], description: 'Order direction' },
1240
- size: { type: 'number', description: 'Total order size' },
1241
- duration: { type: 'number', description: 'Duration in seconds' },
1242
- intervals: { type: 'number', description: 'Number of slices' },
1243
- randomize: { type: 'number', description: 'Randomize timing by this % (0-50)' },
1240
+ size: { type: 'number', description: 'Total order size in base asset' },
1241
+ duration: { type: 'number', description: 'Duration in minutes (5–1440)' },
1242
+ randomize: { type: 'boolean', description: 'Randomize timing (default: true)' },
1243
+ reduce_only: { type: 'boolean', description: 'Reduce-only order (default: false)' },
1244
1244
  leverage: { type: 'number', description: 'Set leverage (e.g., 10 for 10x)' },
1245
- dry: { type: 'boolean', description: 'Preview without executing' },
1246
1245
  },
1247
1246
  required: ['coin', 'side', 'size', 'duration'],
1248
1247
  },
1249
1248
  async execute(_id, params) {
1250
- const { execFile } = await import('node:child_process');
1251
- const args = ['twap'];
1252
- for (const [key, value] of Object.entries(params)) {
1253
- if (value === undefined || value === null || value === false || value === '') continue;
1254
- if (value === true) args.push(`--${key}`);
1255
- else args.push(`--${key}`, String(value));
1249
+ const { getClient } = await import('../core/client.js');
1250
+ const coin = normalizeCoin(params.coin as string);
1251
+ const isBuy = params.side === 'buy';
1252
+ const size = params.size as number;
1253
+ const durationMinutes = params.duration as number;
1254
+ const randomize = params.randomize !== false;
1255
+ const reduceOnly = params.reduce_only === true;
1256
+ const leverage = params.leverage as number | undefined;
1257
+
1258
+ if (durationMinutes < 5 || durationMinutes > 1440) {
1259
+ return error('Duration must be between 5 and 1440 minutes');
1256
1260
  }
1257
1261
 
1258
- return new Promise((resolve) => {
1259
- execFile('openbroker', args, { timeout: 600_000 }, (_err, stdout, stderr) => {
1260
- resolve({ content: [{ type: 'text' as const, text: (stdout + (stderr || '')).trim() }] });
1261
- });
1262
- });
1262
+ try {
1263
+ const client = getClient();
1264
+ const mids = await client.getAllMids();
1265
+ const midPrice = parseFloat(mids[coin]);
1266
+
1267
+ const response = await client.twapOrder(coin, isBuy, size, durationMinutes, randomize, reduceOnly, leverage);
1268
+ const status = response.response.data.status;
1269
+
1270
+ if ('running' in status) {
1271
+ return json({
1272
+ twapId: status.running.twapId,
1273
+ coin,
1274
+ side: isBuy ? 'buy' : 'sell',
1275
+ size,
1276
+ durationMinutes,
1277
+ randomize,
1278
+ reduceOnly,
1279
+ estimatedNotional: midPrice ? midPrice * size : undefined,
1280
+ midPrice: midPrice || undefined,
1281
+ });
1282
+ } else if ('error' in status) {
1283
+ return error(status.error);
1284
+ }
1285
+ return error('Unexpected response');
1286
+ } catch (err) {
1287
+ return error(err instanceof Error ? err.message : String(err));
1288
+ }
1289
+ },
1290
+ },
1291
+
1292
+ {
1293
+ name: 'ob_twap_cancel',
1294
+ description: 'Cancel a running native Hyperliquid TWAP order by its TWAP ID.',
1295
+ parameters: {
1296
+ type: 'object',
1297
+ properties: {
1298
+ coin: { type: 'string', description: 'Asset symbol (e.g., ETH)' },
1299
+ twap_id: { type: 'number', description: 'TWAP order ID to cancel' },
1300
+ },
1301
+ required: ['coin', 'twap_id'],
1302
+ },
1303
+ async execute(_id, params) {
1304
+ const { getClient } = await import('../core/client.js');
1305
+ const coin = normalizeCoin(params.coin as string);
1306
+ const twapId = params.twap_id as number;
1307
+
1308
+ try {
1309
+ const client = getClient();
1310
+ const response = await client.twapCancel(coin, twapId);
1311
+ const status = response.response.data.status;
1312
+
1313
+ if (typeof status === 'string' && status === 'success') {
1314
+ return json({ cancelled: true, coin, twapId });
1315
+ } else if (typeof status === 'object' && 'error' in status) {
1316
+ return error(status.error);
1317
+ }
1318
+ return error('Unexpected response');
1319
+ } catch (err) {
1320
+ return error(err instanceof Error ? err.message : String(err));
1321
+ }
1322
+ },
1323
+ },
1324
+
1325
+ {
1326
+ name: 'ob_twap_status',
1327
+ description: 'View TWAP order history and status. Shows active and past TWAP orders.',
1328
+ parameters: {
1329
+ type: 'object',
1330
+ properties: {
1331
+ active: { type: 'boolean', description: 'Only show active/running TWAP orders' },
1332
+ },
1333
+ },
1334
+ async execute(_id, params) {
1335
+ const { getClient } = await import('../core/client.js');
1336
+
1337
+ try {
1338
+ const client = getClient();
1339
+ const history = await client.twapHistory();
1340
+
1341
+ const filtered = params.active
1342
+ ? history.filter((h: { status: { status: string } }) => h.status.status === 'activated')
1343
+ : history;
1344
+
1345
+ return json(filtered.map((entry: {
1346
+ twapId?: number;
1347
+ state: { coin: string; side: string; sz: string; executedSz: string; executedNtl: string; minutes: number; randomize: boolean; reduceOnly: boolean; timestamp: number };
1348
+ status: { status: string; description?: string };
1349
+ }) => ({
1350
+ twapId: entry.twapId,
1351
+ coin: entry.state.coin,
1352
+ side: entry.state.side === 'B' ? 'buy' : 'sell',
1353
+ totalSize: entry.state.sz,
1354
+ executedSize: entry.state.executedSz,
1355
+ executedNotional: entry.state.executedNtl,
1356
+ durationMinutes: entry.state.minutes,
1357
+ randomize: entry.state.randomize,
1358
+ reduceOnly: entry.state.reduceOnly,
1359
+ status: entry.status.status,
1360
+ startedAt: new Date(entry.state.timestamp).toISOString(),
1361
+ })));
1362
+ } catch (err) {
1363
+ return error(err instanceof Error ? err.message : String(err));
1364
+ }
1263
1365
  },
1264
1366
  },
1265
1367