openbroker 1.0.69 → 1.0.71
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 +42 -13
- package/SKILL.md +14 -8
- package/bin/cli.ts +6 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -1
- package/scripts/core/client.ts +112 -6
- package/scripts/info/account.ts +7 -0
- package/scripts/info/positions.ts +4 -0
- package/scripts/operations/twap-cancel.ts +65 -0
- package/scripts/operations/twap-status.ts +96 -0
- package/scripts/operations/twap.ts +64 -137
- package/scripts/plugin/tools.ts +125 -18
- package/scripts/setup/onboard.ts +71 -20
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` —
|
|
386
|
+
#### `twap` — Native TWAP Order
|
|
387
387
|
|
|
388
|
-
|
|
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
|
|
392
|
-
openbroker twap --coin ETH --side buy --size 1 --duration
|
|
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
|
|
395
|
-
openbroker twap --coin BTC --side sell --size 0.5 --duration
|
|
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` |
|
|
404
|
-
| `--
|
|
405
|
-
| `--
|
|
406
|
-
| `--
|
|
407
|
-
| `--dry` | Show
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.71", "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
|
|
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 (
|
|
291
|
+
### TWAP (Native Exchange Order)
|
|
292
292
|
```bash
|
|
293
|
-
#
|
|
294
|
-
openbroker twap --coin ETH --side buy --size 1 --duration
|
|
293
|
+
# Buy 1 ETH over 30 minutes (exchange handles slicing)
|
|
294
|
+
openbroker twap --coin ETH --side buy --size 1 --duration 30
|
|
295
295
|
|
|
296
|
-
#
|
|
297
|
-
openbroker twap --coin BTC --side sell --size 0.5 --duration
|
|
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)
|
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
|
|
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
|
|
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
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openbroker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.71",
|
|
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",
|
package/scripts/core/client.ts
CHANGED
|
@@ -1126,7 +1126,18 @@ export class HyperliquidClient {
|
|
|
1126
1126
|
const params: { user: string; dex?: string } = { user: user ?? this.address };
|
|
1127
1127
|
if (dex !== undefined) params.dex = dex;
|
|
1128
1128
|
const response = await this.info.clearinghouseState(params as any);
|
|
1129
|
-
|
|
1129
|
+
|
|
1130
|
+
// The SDK response has `withdrawable` as a top-level field, not inside
|
|
1131
|
+
// marginSummary/crossMarginSummary. Copy it into our MarginSummary shape.
|
|
1132
|
+
const state = response as unknown as ClearinghouseState;
|
|
1133
|
+
const withdrawable = (response as any).withdrawable ?? '0';
|
|
1134
|
+
if (state.marginSummary) {
|
|
1135
|
+
state.marginSummary.withdrawable = withdrawable;
|
|
1136
|
+
}
|
|
1137
|
+
if (state.crossMarginSummary) {
|
|
1138
|
+
state.crossMarginSummary.withdrawable = withdrawable;
|
|
1139
|
+
}
|
|
1140
|
+
return state;
|
|
1130
1141
|
}
|
|
1131
1142
|
|
|
1132
1143
|
/**
|
|
@@ -1142,6 +1153,7 @@ export class HyperliquidClient {
|
|
|
1142
1153
|
const dexs = await this.getPerpDexs();
|
|
1143
1154
|
|
|
1144
1155
|
// Collect positions from all HIP-3 dexes
|
|
1156
|
+
let hip3Errors = 0;
|
|
1145
1157
|
for (let i = 1; i < dexs.length; i++) {
|
|
1146
1158
|
const dex = dexs[i];
|
|
1147
1159
|
if (!dex) continue;
|
|
@@ -1156,22 +1168,32 @@ export class HyperliquidClient {
|
|
|
1156
1168
|
if (!unified) {
|
|
1157
1169
|
const dexMargin = dexState.marginSummary;
|
|
1158
1170
|
if (dexMargin) {
|
|
1171
|
+
const safeAdd = (a: string | undefined, b: string | undefined): string => {
|
|
1172
|
+
const va = parseFloat(a ?? '0') || 0;
|
|
1173
|
+
const vb = parseFloat(b ?? '0') || 0;
|
|
1174
|
+
return String(va + vb);
|
|
1175
|
+
};
|
|
1159
1176
|
const addToSummary = (summary: { accountValue: string; totalNtlPos: string; totalRawUsd: string; totalMarginUsed: string; withdrawable: string }) => {
|
|
1160
|
-
summary.accountValue =
|
|
1161
|
-
summary.totalNtlPos =
|
|
1162
|
-
summary.totalRawUsd =
|
|
1163
|
-
summary.totalMarginUsed =
|
|
1164
|
-
summary.withdrawable =
|
|
1177
|
+
summary.accountValue = safeAdd(summary.accountValue, dexMargin.accountValue);
|
|
1178
|
+
summary.totalNtlPos = safeAdd(summary.totalNtlPos, dexMargin.totalNtlPos);
|
|
1179
|
+
summary.totalRawUsd = safeAdd(summary.totalRawUsd, dexMargin.totalRawUsd);
|
|
1180
|
+
summary.totalMarginUsed = safeAdd(summary.totalMarginUsed, dexMargin.totalMarginUsed);
|
|
1181
|
+
summary.withdrawable = safeAdd(summary.withdrawable, dexMargin.withdrawable);
|
|
1165
1182
|
};
|
|
1166
1183
|
addToSummary(mainState.marginSummary);
|
|
1167
1184
|
addToSummary(mainState.crossMarginSummary);
|
|
1168
1185
|
}
|
|
1169
1186
|
}
|
|
1170
1187
|
} catch (err) {
|
|
1188
|
+
hip3Errors++;
|
|
1171
1189
|
this.log(`Failed to fetch state for dex ${dex.name}:`, err instanceof Error ? err.message : String(err));
|
|
1172
1190
|
}
|
|
1173
1191
|
}
|
|
1174
1192
|
|
|
1193
|
+
if (hip3Errors > 0) {
|
|
1194
|
+
this.log(`Warning: ${hip3Errors} HIP-3 dex queries failed — some positions may be missing. Use --verbose for details.`);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1175
1197
|
// For unified accounts: equity is the USDC balance from spot clearinghouse
|
|
1176
1198
|
if (unified) {
|
|
1177
1199
|
try {
|
|
@@ -1616,6 +1638,90 @@ export class HyperliquidClient {
|
|
|
1616
1638
|
throw error;
|
|
1617
1639
|
}
|
|
1618
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
|
+
}
|
|
1619
1725
|
}
|
|
1620
1726
|
|
|
1621
1727
|
// Singleton instance
|
package/scripts/info/account.ts
CHANGED
|
@@ -109,6 +109,13 @@ async function main() {
|
|
|
109
109
|
console.log(`Builder Approved: ❌ No`);
|
|
110
110
|
console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
// Warn if API wallet setup looks misconfigured
|
|
114
|
+
if (!client.isApiWallet && accountValue === 0 && positions.length === 0) {
|
|
115
|
+
console.log('\n⚠️ No positions and $0 equity.');
|
|
116
|
+
console.log(' If this account is traded via an API wallet, set HYPERLIQUID_ACCOUNT_ADDRESS');
|
|
117
|
+
console.log(' in ~/.openbroker/.env to the master account address (the wallet that holds funds).');
|
|
118
|
+
}
|
|
112
119
|
console.log('');
|
|
113
120
|
|
|
114
121
|
console.log('Margin Summary');
|
|
@@ -71,6 +71,10 @@ async function main() {
|
|
|
71
71
|
|
|
72
72
|
if (positions.length === 0) {
|
|
73
73
|
console.log(filterCoin ? `No position in ${filterCoin}` : 'No open positions');
|
|
74
|
+
if (!filterCoin && !client.isApiWallet) {
|
|
75
|
+
console.log('\n⚠️ If this account is traded via an API wallet, set HYPERLIQUID_ACCOUNT_ADDRESS');
|
|
76
|
+
console.log(' in ~/.openbroker/.env to the master account address.');
|
|
77
|
+
}
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -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
|
|
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
|
-
|
|
13
|
-
|
|
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 <
|
|
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
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
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
|
|
31
|
-
npx tsx scripts/operations/twap.ts --coin ETH --side buy --size 1 --duration
|
|
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
|
|
34
|
-
npx tsx scripts/operations/twap.ts --coin BTC --side sell --size 0.5 --duration
|
|
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
|
|
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
|
|
58
|
-
const
|
|
59
|
-
const
|
|
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(
|
|
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
|
|
75
|
-
console.error('Error: size
|
|
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
|
|
87
|
-
console.log('
|
|
80
|
+
console.log('Open Broker - Native TWAP Order');
|
|
81
|
+
console.log('===============================\n');
|
|
88
82
|
|
|
89
83
|
try {
|
|
90
|
-
// Get current price for
|
|
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('
|
|
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(
|
|
110
|
-
console.log(`
|
|
111
|
-
console.log(`
|
|
112
|
-
|
|
113
|
-
|
|
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('\
|
|
119
|
-
|
|
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('\
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
}
|
package/scripts/plugin/tools.ts
CHANGED
|
@@ -89,6 +89,11 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
|
|
|
89
89
|
}));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Warn if likely misconfigured API wallet (querying the API wallet address instead of master)
|
|
93
|
+
if (!client.isApiWallet && accountValue === 0 && state.assetPositions.filter(ap => parseFloat(ap.position.szi) !== 0).length === 0) {
|
|
94
|
+
result.warning = 'No positions and $0 equity. If using an API wallet, ensure HYPERLIQUID_ACCOUNT_ADDRESS is set to the master account address in ~/.openbroker/.env or plugin config.';
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
return json(result);
|
|
93
98
|
},
|
|
94
99
|
},
|
|
@@ -1226,35 +1231,137 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
|
|
|
1226
1231
|
|
|
1227
1232
|
{
|
|
1228
1233
|
name: 'ob_twap',
|
|
1229
|
-
description: '
|
|
1234
|
+
description: 'Place a native Hyperliquid TWAP order. The exchange handles order slicing and timing server-side. Returns immediately with a TWAP ID.',
|
|
1230
1235
|
parameters: {
|
|
1231
1236
|
type: 'object',
|
|
1232
1237
|
properties: {
|
|
1233
|
-
coin: { type: 'string', description: 'Asset symbol' },
|
|
1238
|
+
coin: { type: 'string', description: 'Asset symbol (e.g., ETH, BTC)' },
|
|
1234
1239
|
side: { type: 'string', enum: ['buy', 'sell'], description: 'Order direction' },
|
|
1235
|
-
size: { type: 'number', description: 'Total order size' },
|
|
1236
|
-
duration: { type: 'number', description: 'Duration in
|
|
1237
|
-
|
|
1238
|
-
|
|
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)' },
|
|
1239
1244
|
leverage: { type: 'number', description: 'Set leverage (e.g., 10 for 10x)' },
|
|
1240
|
-
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
1241
1245
|
},
|
|
1242
1246
|
required: ['coin', 'side', 'size', 'duration'],
|
|
1243
1247
|
},
|
|
1244
1248
|
async execute(_id, params) {
|
|
1245
|
-
const {
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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');
|
|
1251
1260
|
}
|
|
1252
1261
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
+
}
|
|
1258
1365
|
},
|
|
1259
1366
|
},
|
|
1260
1367
|
|
package/scripts/setup/onboard.ts
CHANGED
|
@@ -231,34 +231,85 @@ async function main(): Promise<OnboardResult> {
|
|
|
231
231
|
|
|
232
232
|
// Check if config already exists
|
|
233
233
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
234
|
-
console.log('⚠️ Config already exists!');
|
|
235
|
-
console.log(` Location: ${CONFIG_PATH}\n`);
|
|
236
|
-
|
|
237
|
-
// Read existing config and show wallet address
|
|
238
234
|
const envContent = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
239
235
|
const keyMatch = envContent.match(/HYPERLIQUID_PRIVATE_KEY=0x([a-fA-F0-9]{64})/);
|
|
240
236
|
|
|
241
|
-
if (keyMatch) {
|
|
242
|
-
const existingKey = `0x${keyMatch[1]}` as `0x${string}`;
|
|
243
|
-
const account = privateKeyToAccount(existingKey);
|
|
244
|
-
console.log('Current Configuration');
|
|
245
|
-
console.log('---------------------');
|
|
246
|
-
console.log(`Wallet Address: ${account.address}`);
|
|
247
|
-
console.log(`Config File: ${CONFIG_PATH}`);
|
|
248
|
-
console.log(`\nTo reconfigure, delete the config file first:`);
|
|
249
|
-
console.log(` rm ${CONFIG_PATH}`);
|
|
250
|
-
console.log(`\nTo fund this wallet, send USDC on Arbitrum, then deposit at:`);
|
|
251
|
-
console.log(` https://app.hyperliquid.xyz/`);
|
|
252
|
-
|
|
237
|
+
if (!keyMatch) {
|
|
253
238
|
return {
|
|
254
|
-
success:
|
|
255
|
-
|
|
239
|
+
success: false,
|
|
240
|
+
error: 'Invalid config file - missing or malformed private key',
|
|
256
241
|
};
|
|
257
242
|
}
|
|
258
243
|
|
|
244
|
+
const existingKey = `0x${keyMatch[1]}` as `0x${string}`;
|
|
245
|
+
const account = privateKeyToAccount(existingKey);
|
|
246
|
+
|
|
247
|
+
// Check if this is an incomplete API wallet setup (HYPERLIQUID_ACCOUNT_ADDRESS missing or commented out)
|
|
248
|
+
const hasAccountAddress = /^HYPERLIQUID_ACCOUNT_ADDRESS=0x[a-fA-F0-9]{40}/m.test(envContent);
|
|
249
|
+
const isIncompleteApiWallet = envContent.includes('INCOMPLETE') || envContent.includes('# HYPERLIQUID_ACCOUNT_ADDRESS');
|
|
250
|
+
|
|
251
|
+
if (!hasAccountAddress && isIncompleteApiWallet) {
|
|
252
|
+
console.log('⚠️ Incomplete API wallet setup detected!');
|
|
253
|
+
console.log(` API Wallet: ${account.address}`);
|
|
254
|
+
console.log(` Master account address is missing — re-polling for approval...\n`);
|
|
255
|
+
|
|
256
|
+
const approveUrl = `${OPENBROKER_URL}/approve?agent=${account.address}`;
|
|
257
|
+
console.log(` If not yet approved, visit: ${approveUrl}\n`);
|
|
258
|
+
|
|
259
|
+
const masterAddress = await pollForApproval(account.address);
|
|
260
|
+
|
|
261
|
+
if (masterAddress) {
|
|
262
|
+
console.log(`\n✅ Master wallet detected: ${masterAddress}`);
|
|
263
|
+
|
|
264
|
+
// Verify builder fee on-chain
|
|
265
|
+
console.log(' Verifying builder fee approval...');
|
|
266
|
+
const feeApproved = await verifyBuilderFee(masterAddress);
|
|
267
|
+
if (feeApproved) {
|
|
268
|
+
console.log(' ✅ Builder fee: approved on-chain');
|
|
269
|
+
} else {
|
|
270
|
+
console.log(' ⚠️ Builder fee not yet confirmed on-chain (may take a moment)');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Save complete config
|
|
274
|
+
const completeEnv = buildApiWalletEnvContent(existingKey, masterAddress);
|
|
275
|
+
fs.writeFileSync(CONFIG_PATH, completeEnv, { mode: 0o600 });
|
|
276
|
+
|
|
277
|
+
console.log(`\n✅ Config updated: ${CONFIG_PATH}`);
|
|
278
|
+
console.log(` API Wallet: ${account.address}`);
|
|
279
|
+
console.log(` Master Account: ${masterAddress}`);
|
|
280
|
+
console.log('\n Start trading: openbroker account');
|
|
281
|
+
|
|
282
|
+
return { success: true, walletAddress: account.address };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log('\n⚠️ Approval still not completed.');
|
|
286
|
+
console.log(` Visit: ${approveUrl}`);
|
|
287
|
+
console.log(' Then re-run: openbroker setup\n');
|
|
288
|
+
return { success: false, error: 'Approval not completed' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Config exists and is complete
|
|
292
|
+
console.log('⚠️ Config already exists!');
|
|
293
|
+
console.log(` Location: ${CONFIG_PATH}\n`);
|
|
294
|
+
console.log('Current Configuration');
|
|
295
|
+
console.log('---------------------');
|
|
296
|
+
console.log(`Wallet Address: ${account.address}`);
|
|
297
|
+
if (hasAccountAddress) {
|
|
298
|
+
const addrMatch = envContent.match(/HYPERLIQUID_ACCOUNT_ADDRESS=(0x[a-fA-F0-9]+)/);
|
|
299
|
+
if (addrMatch) {
|
|
300
|
+
console.log(`Master Account: ${addrMatch[1]}`);
|
|
301
|
+
console.log(`Wallet Type: API Wallet`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
console.log(`Config File: ${CONFIG_PATH}`);
|
|
305
|
+
console.log(`\nTo reconfigure, delete the config file first:`);
|
|
306
|
+
console.log(` rm ${CONFIG_PATH}`);
|
|
307
|
+
console.log(`\nTo fund this wallet, send USDC on Arbitrum, then deposit at:`);
|
|
308
|
+
console.log(` https://app.hyperliquid.xyz/`);
|
|
309
|
+
|
|
259
310
|
return {
|
|
260
|
-
success:
|
|
261
|
-
|
|
311
|
+
success: true,
|
|
312
|
+
walletAddress: account.address,
|
|
262
313
|
};
|
|
263
314
|
}
|
|
264
315
|
|