openbroker 1.0.44 → 1.0.47
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/CHANGELOG.md +25 -0
- package/README.md +17 -4
- package/SKILL.md +34 -7
- package/bin/cli.ts +3 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/core/client.ts +191 -6
- package/scripts/info/candles.ts +3 -0
- package/scripts/info/funding-history.ts +3 -0
- package/scripts/info/funding-scan.ts +212 -0
- package/scripts/info/funding.ts +35 -0
- package/scripts/info/markets.ts +37 -0
- package/scripts/info/trades.ts +3 -0
- package/scripts/plugin/tools.ts +120 -0
- package/scripts/setup/onboard.ts +10 -8
- package/scripts/strategies/funding-arb.ts +43 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Open Broker will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.0.47] - 2026-03-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **HIP-3 Perp Trading**: All trading commands now work with HIP-3 assets using `dex:COIN` syntax (e.g., `--coin xyz:CL`)
|
|
9
|
+
- `getMetaAndAssetCtxs()` loads HIP-3 dex assets into asset/szDecimals maps (asset index = `10000 * dexIdx + assetIdx`)
|
|
10
|
+
- `getAllMids()` fetches and merges mid prices from all HIP-3 dexes
|
|
11
|
+
- `getCandleSnapshot()`, `getFundingHistory()`, `getRecentTrades()`, `getL2Book()` pass correct `dex` param for HIP-3 coins
|
|
12
|
+
- Market, limit, trigger, bracket, TWAP, scale, chase orders all work with HIP-3 assets
|
|
13
|
+
- **HIP-3 Info Commands**: `candles`, `trades`, `funding-history` now return data for HIP-3 assets (previously returned null/empty)
|
|
14
|
+
- **Better Error Messages**: When a bare coin name (e.g., `CL`) matches HIP-3 assets, the error now suggests the prefixed ticker (e.g., `xyz:CL`)
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Funding Rate Scanner**: New `funding-scan` command for cross-dex funding rate scanning
|
|
18
|
+
- Scans all dexes (main + HIP-3) for high funding opportunities
|
|
19
|
+
- `--pairs` flag identifies opposing funding pairs for delta-neutral strategies
|
|
20
|
+
- `--watch --interval N` for periodic re-scanning
|
|
21
|
+
- `--json` output for piping to alerting systems
|
|
22
|
+
- `--main-only` / `--hip3-only` scope filters
|
|
23
|
+
- **HIP-3 Funding Rates**: `funding` and `markets` commands now support `--include-hip3` flag
|
|
24
|
+
- **HIP-3 Funding Arb**: `funding-arb` strategy now works with HIP-3 assets (monitoring loop correctly resolves HIP-3 funding data)
|
|
25
|
+
- **Plugin**: New `ob_funding_scan` agent tool for cross-dex funding scanning with pairs support
|
|
26
|
+
- **Plugin**: `ob_search` now searches HIP-3 perps in addition to main perps and spot
|
|
27
|
+
- **Client**: Added `getCoinDex()`, `getCoinLocalName()`, `isHip3()`, `getAllAssetNames()`, `getHip3AssetNames()`, `invalidateMetaCache()` methods
|
|
28
|
+
- **Client**: `getPerpDexs()` results are now cached to reduce redundant API calls
|
|
29
|
+
|
|
5
30
|
## [1.0.44] - 2026-02-25
|
|
6
31
|
|
|
7
32
|
### Added
|
package/README.md
CHANGED
|
@@ -31,10 +31,15 @@ openbroker setup # One-command setup (wallet + config + builder a
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
The setup command handles everything automatically:
|
|
34
|
-
- Generates a
|
|
34
|
+
- Generates a fresh trading wallet (recommended for agents) or accepts your existing private key
|
|
35
35
|
- Saves configuration to `~/.openbroker/.env` (permissions `0600`)
|
|
36
36
|
- Approves the builder fee (required for trading)
|
|
37
37
|
|
|
38
|
+
Setup offers three modes:
|
|
39
|
+
1. **Generate fresh wallet** (recommended for agents) — cleanest option, no browser steps
|
|
40
|
+
2. **Import existing key** — use a key you already have
|
|
41
|
+
3. **Generate API wallet** — restricted wallet requiring browser approval from a master wallet
|
|
42
|
+
|
|
38
43
|
---
|
|
39
44
|
|
|
40
45
|
### Info Commands
|
|
@@ -636,16 +641,24 @@ export HYPERLIQUID_NETWORK=mainnet # Optional: mainnet (default) or testne
|
|
|
636
641
|
export HYPERLIQUID_ACCOUNT_ADDRESS=0x... # Optional: for API wallets
|
|
637
642
|
```
|
|
638
643
|
|
|
639
|
-
###
|
|
644
|
+
### Fresh Wallet Setup (Recommended for Agents)
|
|
645
|
+
|
|
646
|
+
The simplest setup for agents — generates a dedicated wallet, auto-approves the builder fee, and is ready to trade after funding:
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
openbroker setup # Choose option 1, fund with USDC, start trading
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### API Wallet Setup (Alternative)
|
|
640
653
|
|
|
641
|
-
For
|
|
654
|
+
For delegated trading without moving funds, use an API wallet:
|
|
642
655
|
|
|
643
656
|
```bash
|
|
644
657
|
export HYPERLIQUID_PRIVATE_KEY="0x..." # API wallet private key
|
|
645
658
|
export HYPERLIQUID_ACCOUNT_ADDRESS="0x..." # Main account address
|
|
646
659
|
```
|
|
647
660
|
|
|
648
|
-
**Note:**
|
|
661
|
+
**Note:** API wallets require browser approval from the master wallet. The master wallet signs `ApproveAgent` and `ApproveBuilderFee` transactions via the approval URL provided during setup.
|
|
649
662
|
|
|
650
663
|
## Builder Fee
|
|
651
664
|
|
package/SKILL.md
CHANGED
|
@@ -4,8 +4,8 @@ description: Hyperliquid trading plugin with background position monitoring. Exe
|
|
|
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_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_twap ob_bracket ob_chase ob_watcher_status Bash(openbroker:*)
|
|
7
|
+
metadata: {"author": "monemetrics", "version": "1.0.47", "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 Bash(openbroker:*)
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Open Broker - Hyperliquid Trading CLI
|
|
@@ -40,16 +40,26 @@ openbroker approve-builder --check # Check builder fee status (for troubleshoot
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
The `setup` command offers three modes:
|
|
43
|
-
1. **
|
|
44
|
-
2. **
|
|
45
|
-
3. **Generate API wallet**
|
|
43
|
+
1. **Generate fresh wallet** (recommended for agents) — creates a dedicated trading wallet with builder fee auto-approved. No browser steps needed — just fund with USDC and start trading.
|
|
44
|
+
2. **Import existing key** — use a private key you already have
|
|
45
|
+
3. **Generate API wallet** — creates a restricted wallet that can trade but cannot withdraw. Requires browser approval from a master wallet.
|
|
46
46
|
|
|
47
47
|
For options 1 and 2, setup saves config and approves the builder fee automatically.
|
|
48
48
|
For option 3 (API wallet), see the API Wallet Setup section below.
|
|
49
49
|
|
|
50
|
-
###
|
|
50
|
+
### Fresh Wallet Setup (Recommended for Agents)
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
The simplest setup for agents. A fresh wallet is generated, the builder fee is auto-approved, and the agent is ready to trade immediately after funding.
|
|
53
|
+
|
|
54
|
+
**Flow:**
|
|
55
|
+
1. Run `openbroker setup` and choose option 1 ("Generate a fresh wallet")
|
|
56
|
+
2. The CLI generates a wallet, saves the config, and approves the builder fee automatically
|
|
57
|
+
3. Fund the wallet with USDC on Arbitrum, then deposit at https://app.hyperliquid.xyz/
|
|
58
|
+
4. Start trading
|
|
59
|
+
|
|
60
|
+
### API Wallet Setup (Alternative)
|
|
61
|
+
|
|
62
|
+
API wallets can place trades on behalf of a master account but **cannot withdraw funds**. Use this if you prefer to keep funds in your existing wallet and only delegate trading access.
|
|
53
63
|
|
|
54
64
|
**Flow:**
|
|
55
65
|
1. Run `openbroker setup` and choose option 3 ("Generate API wallet")
|
|
@@ -160,8 +170,25 @@ openbroker trades --coin BTC --top 50 # Last 50 trades
|
|
|
160
170
|
openbroker rate-limit # API usage and capacity
|
|
161
171
|
```
|
|
162
172
|
|
|
173
|
+
### Funding Rate Scanner (Cross-Dex)
|
|
174
|
+
```bash
|
|
175
|
+
openbroker funding-scan # Scan all dexes, >25% threshold
|
|
176
|
+
openbroker funding-scan --threshold 50 --pairs # Show opposing funding pairs
|
|
177
|
+
openbroker funding-scan --hip3-only --top 20 # HIP-3 only
|
|
178
|
+
openbroker funding-scan --watch --interval 120 # Re-scan every 2 minutes
|
|
179
|
+
```
|
|
180
|
+
|
|
163
181
|
## Trading Commands
|
|
164
182
|
|
|
183
|
+
### HIP-3 Perp Trading
|
|
184
|
+
All trading commands support HIP-3 assets using `dex:COIN` syntax:
|
|
185
|
+
```bash
|
|
186
|
+
openbroker buy --coin xyz:CL --size 1 # Buy crude oil on xyz dex
|
|
187
|
+
openbroker sell --coin xyz:BRENTOIL --size 1 # Sell brent oil
|
|
188
|
+
openbroker limit --coin xyz:GOLD --side buy --size 0.1 --price 2500
|
|
189
|
+
openbroker funding-arb --coin xyz:CL --size 5000 # Funding arb on HIP-3
|
|
190
|
+
```
|
|
191
|
+
|
|
165
192
|
### Market Orders (Quick)
|
|
166
193
|
```bash
|
|
167
194
|
openbroker buy --coin ETH --size 0.1
|
package/bin/cli.ts
CHANGED
|
@@ -31,6 +31,7 @@ const commands: Record<string, { script: string; description: string }> = {
|
|
|
31
31
|
'funding-history': { script: 'info/funding-history.ts', description: 'View historical funding rates' },
|
|
32
32
|
'trades': { script: 'info/trades.ts', description: 'View recent trades for an asset' },
|
|
33
33
|
'rate-limit': { script: 'info/rate-limit.ts', description: 'View API rate limit status' },
|
|
34
|
+
'funding-scan': { script: 'info/funding-scan.ts', description: 'Scan funding rates across all dexes' },
|
|
34
35
|
|
|
35
36
|
// Operations
|
|
36
37
|
'buy': { script: 'operations/market-order.ts', description: 'Market buy order' },
|
|
@@ -78,6 +79,7 @@ Info Commands:
|
|
|
78
79
|
search Search for assets across all providers
|
|
79
80
|
spot View spot markets and balances
|
|
80
81
|
rate-limit View API rate limit status
|
|
82
|
+
funding-scan Scan funding rates across all dexes (main + HIP-3)
|
|
81
83
|
|
|
82
84
|
Trading Commands:
|
|
83
85
|
buy Market buy order
|
|
@@ -148,7 +150,7 @@ function runScript(scriptPath: string, args: string[]) {
|
|
|
148
150
|
function main() {
|
|
149
151
|
const args = process.argv.slice(2);
|
|
150
152
|
|
|
151
|
-
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
153
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
152
154
|
printHelp();
|
|
153
155
|
process.exit(0);
|
|
154
156
|
}
|
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.47",
|
|
4
4
|
"description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"scale": "tsx scripts/operations/scale.ts",
|
|
39
39
|
"bracket": "tsx scripts/operations/bracket.ts",
|
|
40
40
|
"chase": "tsx scripts/operations/chase.ts",
|
|
41
|
+
"funding-scan": "tsx scripts/info/funding-scan.ts",
|
|
41
42
|
"funding-arb": "tsx scripts/strategies/funding-arb.ts",
|
|
42
43
|
"grid": "tsx scripts/strategies/grid.ts",
|
|
43
44
|
"dca": "tsx scripts/strategies/dca.ts",
|
package/scripts/core/client.ts
CHANGED
|
@@ -25,6 +25,12 @@ export class HyperliquidClient {
|
|
|
25
25
|
private meta: MetaAndAssetCtxs | null = null;
|
|
26
26
|
private assetMap: Map<string, number> = new Map();
|
|
27
27
|
private szDecimalsMap: Map<string, number> = new Map();
|
|
28
|
+
/** Maps coin name → dex info for HIP-3 assets. Main dex assets have dexName=null */
|
|
29
|
+
private coinDexMap: Map<string, { dexName: string | null; dexIdx: number; localName: string }> = new Map();
|
|
30
|
+
/** Cache of perpDexs list */
|
|
31
|
+
private perpDexsCache: Array<{ name: string; fullName: string; deployer: string } | null> | null = null;
|
|
32
|
+
/** Whether HIP-3 assets have been loaded into maps */
|
|
33
|
+
private hip3Loaded: boolean = false;
|
|
28
34
|
public verbose: boolean = false;
|
|
29
35
|
|
|
30
36
|
constructor(config?: OpenBrokerConfig) {
|
|
@@ -106,18 +112,103 @@ export class HyperliquidClient {
|
|
|
106
112
|
assetCtxs: response[1],
|
|
107
113
|
};
|
|
108
114
|
|
|
109
|
-
// Build lookup maps
|
|
115
|
+
// Build lookup maps for main dex
|
|
110
116
|
this.meta.meta.universe.forEach((asset, index) => {
|
|
111
117
|
this.assetMap.set(asset.name, index);
|
|
112
118
|
this.szDecimalsMap.set(asset.name, asset.szDecimals);
|
|
119
|
+
this.coinDexMap.set(asset.name, { dexName: null, dexIdx: 0, localName: asset.name });
|
|
113
120
|
});
|
|
114
121
|
|
|
122
|
+
// Load HIP-3 dex assets (only once - maps persist across meta cache invalidation)
|
|
123
|
+
if (!this.hip3Loaded) {
|
|
124
|
+
await this.loadHip3Assets();
|
|
125
|
+
this.hip3Loaded = true;
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
return this.meta;
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Load HIP-3 perp dex assets into the asset/szDecimals maps.
|
|
133
|
+
* Asset index formula: 10000 * dexIdx + assetIdx
|
|
134
|
+
* Coins are keyed as "dexName:COIN" (e.g., "xyz:CL")
|
|
135
|
+
*/
|
|
136
|
+
private async loadHip3Assets(): Promise<void> {
|
|
137
|
+
try {
|
|
138
|
+
const dexs = await this.getPerpDexs();
|
|
139
|
+
const baseUrl = isMainnet()
|
|
140
|
+
? 'https://api.hyperliquid.xyz'
|
|
141
|
+
: 'https://api.hyperliquid-testnet.xyz';
|
|
142
|
+
|
|
143
|
+
for (let dexIdx = 1; dexIdx < dexs.length; dexIdx++) {
|
|
144
|
+
const dex = dexs[dexIdx];
|
|
145
|
+
if (!dex) continue;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const dexResponse = await fetch(baseUrl + '/info', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({ type: 'metaAndAssetCtxs', dex: dex.name }),
|
|
152
|
+
});
|
|
153
|
+
const dexData = await dexResponse.json();
|
|
154
|
+
|
|
155
|
+
if (dexData && dexData[0]?.universe) {
|
|
156
|
+
const universe = dexData[0].universe as Array<{ name: string; szDecimals: number; maxLeverage: number; onlyIsolated?: boolean }>;
|
|
157
|
+
this.log(`Loading HIP-3 dex: ${dex.name} with ${universe.length} markets`);
|
|
158
|
+
|
|
159
|
+
universe.forEach((asset, assetIdx) => {
|
|
160
|
+
const prefixedName = `${dex.name}:${asset.name}`;
|
|
161
|
+
const globalIndex = 10000 * dexIdx + assetIdx;
|
|
162
|
+
|
|
163
|
+
this.assetMap.set(prefixedName, globalIndex);
|
|
164
|
+
this.szDecimalsMap.set(prefixedName, asset.szDecimals);
|
|
165
|
+
this.coinDexMap.set(prefixedName, { dexName: dex.name, dexIdx, localName: asset.name });
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} catch (e) {
|
|
169
|
+
this.log(`Failed to load HIP-3 dex ${dex.name}:`, e);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
this.log('Failed to load HIP-3 assets:', e);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
118
177
|
async getAllMids(): Promise<Record<string, string>> {
|
|
119
178
|
this.log('Fetching allMids...');
|
|
120
|
-
const response = await this.info.allMids()
|
|
179
|
+
const response = await this.info.allMids() as Record<string, string>;
|
|
180
|
+
|
|
181
|
+
// Also fetch HIP-3 dex mids
|
|
182
|
+
try {
|
|
183
|
+
const dexs = await this.getPerpDexs();
|
|
184
|
+
const baseUrl = isMainnet()
|
|
185
|
+
? 'https://api.hyperliquid.xyz'
|
|
186
|
+
: 'https://api.hyperliquid-testnet.xyz';
|
|
187
|
+
|
|
188
|
+
for (let i = 1; i < dexs.length; i++) {
|
|
189
|
+
const dex = dexs[i];
|
|
190
|
+
if (!dex) continue;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const dexResponse = await fetch(baseUrl + '/info', {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ type: 'allMids', dex: dex.name }),
|
|
197
|
+
});
|
|
198
|
+
const dexMids = await dexResponse.json() as Record<string, string>;
|
|
199
|
+
|
|
200
|
+
// Merge with prefixed keys
|
|
201
|
+
for (const [coin, mid] of Object.entries(dexMids)) {
|
|
202
|
+
response[`${dex.name}:${coin}`] = mid;
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
this.log(`Failed to fetch mids for HIP-3 dex ${dex.name}:`, e);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
this.log('Failed to fetch HIP-3 mids:', e);
|
|
210
|
+
}
|
|
211
|
+
|
|
121
212
|
return response;
|
|
122
213
|
}
|
|
123
214
|
|
|
@@ -130,6 +221,8 @@ export class HyperliquidClient {
|
|
|
130
221
|
fullName: string;
|
|
131
222
|
deployer: string;
|
|
132
223
|
} | null>> {
|
|
224
|
+
if (this.perpDexsCache) return this.perpDexsCache;
|
|
225
|
+
|
|
133
226
|
this.log('Fetching perpDexs...');
|
|
134
227
|
const baseUrl = isMainnet()
|
|
135
228
|
? 'https://api.hyperliquid.xyz'
|
|
@@ -142,6 +235,7 @@ export class HyperliquidClient {
|
|
|
142
235
|
});
|
|
143
236
|
const data = await response.json();
|
|
144
237
|
this.log('perpDexs response:', JSON.stringify(data).slice(0, 500));
|
|
238
|
+
this.perpDexsCache = data;
|
|
145
239
|
return data;
|
|
146
240
|
}
|
|
147
241
|
|
|
@@ -412,7 +506,8 @@ export class HyperliquidClient {
|
|
|
412
506
|
spreadBps: number;
|
|
413
507
|
}> {
|
|
414
508
|
this.log('Fetching l2Book for:', coin);
|
|
415
|
-
const
|
|
509
|
+
const localName = this.getCoinLocalName(coin);
|
|
510
|
+
const response = await this.info.l2Book({ coin: localName });
|
|
416
511
|
|
|
417
512
|
const bids = response.levels[0] as Array<{ px: string; sz: string; n: number }>;
|
|
418
513
|
const asks = response.levels[1] as Array<{ px: string; sz: string; n: number }>;
|
|
@@ -437,6 +532,15 @@ export class HyperliquidClient {
|
|
|
437
532
|
getAssetIndex(coin: string): number {
|
|
438
533
|
const index = this.assetMap.get(coin);
|
|
439
534
|
if (index === undefined) {
|
|
535
|
+
// Check if bare name exists in HIP-3 dexes and suggest prefixed version
|
|
536
|
+
const hip3Matches = this.findHip3Matches(coin);
|
|
537
|
+
if (hip3Matches.length > 0) {
|
|
538
|
+
const suggestions = hip3Matches.map(m => `${m}`).join(', ');
|
|
539
|
+
throw new Error(
|
|
540
|
+
`Unknown asset: ${coin}. Did you mean one of these HIP-3 assets? ${suggestions}\n` +
|
|
541
|
+
`Use "openbroker search --query ${coin}" to find the full ticker.`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
440
544
|
throw new Error(`Unknown asset: ${coin}. Available: ${Array.from(this.assetMap.keys()).slice(0, 10).join(', ')}...`);
|
|
441
545
|
}
|
|
442
546
|
return index;
|
|
@@ -445,11 +549,78 @@ export class HyperliquidClient {
|
|
|
445
549
|
getSzDecimals(coin: string): number {
|
|
446
550
|
const decimals = this.szDecimalsMap.get(coin);
|
|
447
551
|
if (decimals === undefined) {
|
|
552
|
+
const hip3Matches = this.findHip3Matches(coin);
|
|
553
|
+
if (hip3Matches.length > 0) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
`Unknown asset: ${coin}. Did you mean: ${hip3Matches.join(', ')}?`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
448
558
|
throw new Error(`Unknown asset: ${coin}`);
|
|
449
559
|
}
|
|
450
560
|
return decimals;
|
|
451
561
|
}
|
|
452
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Find HIP-3 assets matching a bare coin name (without dex prefix)
|
|
565
|
+
*/
|
|
566
|
+
private findHip3Matches(bareName: string): string[] {
|
|
567
|
+
const matches: string[] = [];
|
|
568
|
+
const upperName = bareName.toUpperCase();
|
|
569
|
+
for (const [key, info] of this.coinDexMap.entries()) {
|
|
570
|
+
if (info.dexName && info.localName.toUpperCase() === upperName) {
|
|
571
|
+
matches.push(key);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return matches;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Get the dex name for a coin (null for main dex assets)
|
|
579
|
+
*/
|
|
580
|
+
getCoinDex(coin: string): string | null {
|
|
581
|
+
return this.coinDexMap.get(coin)?.dexName ?? null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get the local (unprefixed) coin name for API calls that need it
|
|
586
|
+
* e.g., "xyz:CL" → "CL", "ETH" → "ETH"
|
|
587
|
+
*/
|
|
588
|
+
getCoinLocalName(coin: string): string {
|
|
589
|
+
return this.coinDexMap.get(coin)?.localName ?? coin;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Check if a coin is a HIP-3 asset
|
|
594
|
+
*/
|
|
595
|
+
isHip3(coin: string): boolean {
|
|
596
|
+
return this.coinDexMap.get(coin)?.dexName != null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Invalidate cached metadata so next call fetches fresh data.
|
|
601
|
+
* Useful for long-running strategies that need updated funding rates.
|
|
602
|
+
*/
|
|
603
|
+
invalidateMetaCache(): void {
|
|
604
|
+
this.meta = null;
|
|
605
|
+
// Keep the asset/szDecimals/coinDex maps - they don't change
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Get all loaded asset names (main + HIP-3)
|
|
610
|
+
*/
|
|
611
|
+
getAllAssetNames(): string[] {
|
|
612
|
+
return Array.from(this.assetMap.keys());
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get all HIP-3 asset names
|
|
617
|
+
*/
|
|
618
|
+
getHip3AssetNames(): string[] {
|
|
619
|
+
return Array.from(this.coinDexMap.entries())
|
|
620
|
+
.filter(([_, info]) => info.dexName !== null)
|
|
621
|
+
.map(([name]) => name);
|
|
622
|
+
}
|
|
623
|
+
|
|
453
624
|
// ============ Account Info ============
|
|
454
625
|
|
|
455
626
|
/**
|
|
@@ -783,8 +954,12 @@ export class HyperliquidClient {
|
|
|
783
954
|
? 'https://api.hyperliquid.xyz'
|
|
784
955
|
: 'https://api.hyperliquid-testnet.xyz';
|
|
785
956
|
|
|
786
|
-
const
|
|
957
|
+
const localName = this.getCoinLocalName(coin);
|
|
958
|
+
const dex = this.getCoinDex(coin);
|
|
959
|
+
|
|
960
|
+
const req: Record<string, unknown> = { coin: localName, interval, startTime };
|
|
787
961
|
if (endTime !== undefined) req.endTime = endTime;
|
|
962
|
+
if (dex) req.dex = dex;
|
|
788
963
|
|
|
789
964
|
const response = await fetch(baseUrl + '/info', {
|
|
790
965
|
method: 'POST',
|
|
@@ -814,8 +989,12 @@ export class HyperliquidClient {
|
|
|
814
989
|
? 'https://api.hyperliquid.xyz'
|
|
815
990
|
: 'https://api.hyperliquid-testnet.xyz';
|
|
816
991
|
|
|
817
|
-
const
|
|
992
|
+
const localName = this.getCoinLocalName(coin);
|
|
993
|
+
const dex = this.getCoinDex(coin);
|
|
994
|
+
|
|
995
|
+
const body: Record<string, unknown> = { type: 'fundingHistory', coin: localName, startTime };
|
|
818
996
|
if (endTime !== undefined) body.endTime = endTime;
|
|
997
|
+
if (dex) body.dex = dex;
|
|
819
998
|
|
|
820
999
|
const response = await fetch(baseUrl + '/info', {
|
|
821
1000
|
method: 'POST',
|
|
@@ -844,10 +1023,16 @@ export class HyperliquidClient {
|
|
|
844
1023
|
? 'https://api.hyperliquid.xyz'
|
|
845
1024
|
: 'https://api.hyperliquid-testnet.xyz';
|
|
846
1025
|
|
|
1026
|
+
const localName = this.getCoinLocalName(coin);
|
|
1027
|
+
const dex = this.getCoinDex(coin);
|
|
1028
|
+
|
|
1029
|
+
const body: Record<string, unknown> = { type: 'recentTrades', coin: localName };
|
|
1030
|
+
if (dex) body.dex = dex;
|
|
1031
|
+
|
|
847
1032
|
const response = await fetch(baseUrl + '/info', {
|
|
848
1033
|
method: 'POST',
|
|
849
1034
|
headers: { 'Content-Type': 'application/json' },
|
|
850
|
-
body: JSON.stringify(
|
|
1035
|
+
body: JSON.stringify(body),
|
|
851
1036
|
});
|
|
852
1037
|
const data = await response.json();
|
|
853
1038
|
this.log('recentTrades response length:', data?.length);
|
package/scripts/info/candles.ts
CHANGED
|
@@ -65,6 +65,9 @@ async function main() {
|
|
|
65
65
|
console.log('='.repeat(40) + '\n');
|
|
66
66
|
|
|
67
67
|
try {
|
|
68
|
+
// Load metadata (needed for HIP-3 coin resolution)
|
|
69
|
+
await client.getMetaAndAssetCtxs();
|
|
70
|
+
|
|
68
71
|
const now = Date.now();
|
|
69
72
|
const startTime = now - (bars * (INTERVAL_MS[interval] || 3_600_000));
|
|
70
73
|
const candles = await client.getCandleSnapshot(coin.toUpperCase(), interval, startTime);
|
|
@@ -41,6 +41,9 @@ async function main() {
|
|
|
41
41
|
console.log('='.repeat(40) + '\n');
|
|
42
42
|
|
|
43
43
|
try {
|
|
44
|
+
// Load metadata (needed for HIP-3 coin resolution)
|
|
45
|
+
await client.getMetaAndAssetCtxs();
|
|
46
|
+
|
|
44
47
|
const now = Date.now();
|
|
45
48
|
const startTime = now - (hours * 3_600_000);
|
|
46
49
|
const history = await client.getFundingHistory(coin.toUpperCase(), startTime);
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Cross-dex funding rate scanner - scan all assets (main + HIP-3) for funding opportunities
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, formatPercent, annualizeFundingRate, parseArgs, sleep } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Usage: openbroker funding-scan [options]
|
|
10
|
+
|
|
11
|
+
Scan funding rates across all dexes (main perps + HIP-3) and find opportunities.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--threshold <n> Min annualized funding rate % to show (default: 25)
|
|
15
|
+
--include-hip3 Include HIP-3 dex assets (default: true)
|
|
16
|
+
--main-only Only scan main perps
|
|
17
|
+
--hip3-only Only scan HIP-3 perps
|
|
18
|
+
--top <n> Show top N results (default: 30)
|
|
19
|
+
--pairs Show correlated pairs with opposing funding
|
|
20
|
+
--json Output as JSON
|
|
21
|
+
--watch Re-scan periodically
|
|
22
|
+
--interval <n> Watch interval in seconds (default: 60)
|
|
23
|
+
--help, -h Show this help
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
openbroker funding-scan
|
|
27
|
+
openbroker funding-scan --threshold 50 --pairs
|
|
28
|
+
openbroker funding-scan --hip3-only --top 20
|
|
29
|
+
openbroker funding-scan --watch --interval 120
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FundingScanResult {
|
|
34
|
+
coin: string;
|
|
35
|
+
dex: string;
|
|
36
|
+
hourlyRate: number;
|
|
37
|
+
annualizedPct: number;
|
|
38
|
+
direction: 'longs pay' | 'shorts pay';
|
|
39
|
+
openInterest: number;
|
|
40
|
+
markPx: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function scanFunding(client: ReturnType<typeof getClient>, options: {
|
|
44
|
+
threshold: number;
|
|
45
|
+
mainOnly: boolean;
|
|
46
|
+
hip3Only: boolean;
|
|
47
|
+
topN: number;
|
|
48
|
+
}): Promise<FundingScanResult[]> {
|
|
49
|
+
const allPerps = await client.getAllPerpMetas();
|
|
50
|
+
const results: FundingScanResult[] = [];
|
|
51
|
+
|
|
52
|
+
for (const dexData of allPerps) {
|
|
53
|
+
const isMain = !dexData.dexName;
|
|
54
|
+
if (options.mainOnly && !isMain) continue;
|
|
55
|
+
if (options.hip3Only && isMain) continue;
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < dexData.meta.universe.length; i++) {
|
|
58
|
+
const asset = dexData.meta.universe[i];
|
|
59
|
+
const ctx = dexData.assetCtxs[i];
|
|
60
|
+
if (!ctx) continue;
|
|
61
|
+
|
|
62
|
+
const hourlyRate = parseFloat(ctx.funding);
|
|
63
|
+
const annualizedPct = annualizeFundingRate(hourlyRate) * 100;
|
|
64
|
+
const openInterest = parseFloat(ctx.openInterest);
|
|
65
|
+
const markPx = parseFloat(ctx.markPx);
|
|
66
|
+
|
|
67
|
+
if (Math.abs(annualizedPct) < options.threshold) continue;
|
|
68
|
+
if (openInterest < 100) continue;
|
|
69
|
+
|
|
70
|
+
const coin = dexData.dexName ? `${dexData.dexName}:${asset.name}` : asset.name;
|
|
71
|
+
|
|
72
|
+
results.push({
|
|
73
|
+
coin,
|
|
74
|
+
dex: dexData.dexName ?? 'main',
|
|
75
|
+
hourlyRate,
|
|
76
|
+
annualizedPct,
|
|
77
|
+
direction: hourlyRate > 0 ? 'longs pay' : 'shorts pay',
|
|
78
|
+
openInterest,
|
|
79
|
+
markPx,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Sort by absolute annualized rate
|
|
85
|
+
results.sort((a, b) => Math.abs(b.annualizedPct) - Math.abs(a.annualizedPct));
|
|
86
|
+
return results.slice(0, options.topN);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function printResults(results: FundingScanResult[], showPairs: boolean) {
|
|
90
|
+
if (results.length === 0) {
|
|
91
|
+
console.log('No assets above threshold');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Table header
|
|
96
|
+
console.log(
|
|
97
|
+
'Coin'.padEnd(16) +
|
|
98
|
+
'Dex'.padEnd(8) +
|
|
99
|
+
'Annualized'.padEnd(14) +
|
|
100
|
+
'Direction'.padEnd(14) +
|
|
101
|
+
'OI'.padEnd(14) +
|
|
102
|
+
'Mark'
|
|
103
|
+
);
|
|
104
|
+
console.log('─'.repeat(75));
|
|
105
|
+
|
|
106
|
+
for (const r of results) {
|
|
107
|
+
const annStr = `${r.annualizedPct >= 0 ? '+' : ''}${r.annualizedPct.toFixed(1)}%`;
|
|
108
|
+
const oiStr = formatOI(r.openInterest);
|
|
109
|
+
|
|
110
|
+
console.log(
|
|
111
|
+
r.coin.padEnd(16) +
|
|
112
|
+
r.dex.padEnd(8) +
|
|
113
|
+
annStr.padStart(12).padEnd(14) +
|
|
114
|
+
r.direction.padEnd(14) +
|
|
115
|
+
oiStr.padStart(12).padEnd(14) +
|
|
116
|
+
formatUsd(r.markPx)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Show opposing pairs
|
|
121
|
+
if (showPairs) {
|
|
122
|
+
console.log('\nOpposing Funding Pairs:');
|
|
123
|
+
console.log('─'.repeat(75));
|
|
124
|
+
|
|
125
|
+
const longs = results.filter(r => r.annualizedPct > 0); // longs pay shorts
|
|
126
|
+
const shorts = results.filter(r => r.annualizedPct < 0); // shorts pay longs
|
|
127
|
+
|
|
128
|
+
const pairs: Array<{ long: FundingScanResult; short: FundingScanResult; spread: number }> = [];
|
|
129
|
+
|
|
130
|
+
for (const l of longs) {
|
|
131
|
+
for (const s of shorts) {
|
|
132
|
+
// Only pair across different dexes or correlated assets
|
|
133
|
+
const spread = l.annualizedPct + Math.abs(s.annualizedPct);
|
|
134
|
+
if (spread > 20) {
|
|
135
|
+
pairs.push({ long: l, short: s, spread });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pairs.sort((a, b) => b.spread - a.spread);
|
|
141
|
+
|
|
142
|
+
for (const p of pairs.slice(0, 10)) {
|
|
143
|
+
console.log(
|
|
144
|
+
` SHORT ${p.long.coin.padEnd(14)} (${p.long.annualizedPct.toFixed(1)}%) ` +
|
|
145
|
+
`+ LONG ${p.short.coin.padEnd(14)} (${p.short.annualizedPct.toFixed(1)}%) ` +
|
|
146
|
+
`= ${p.spread.toFixed(1)}% spread`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (pairs.length === 0) {
|
|
151
|
+
console.log(' No strong opposing pairs found');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatOI(oi: number): string {
|
|
157
|
+
if (oi >= 1_000_000) return `$${(oi / 1_000_000).toFixed(2)}M`;
|
|
158
|
+
if (oi >= 1_000) return `$${(oi / 1_000).toFixed(1)}K`;
|
|
159
|
+
return `$${oi.toFixed(0)}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function main() {
|
|
163
|
+
const args = parseArgs(process.argv.slice(2));
|
|
164
|
+
|
|
165
|
+
if (args.help || args.h) {
|
|
166
|
+
printUsage();
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const threshold = args.threshold ? parseFloat(args.threshold as string) : 25;
|
|
171
|
+
const mainOnly = args['main-only'] as boolean ?? false;
|
|
172
|
+
const hip3Only = args['hip3-only'] as boolean ?? false;
|
|
173
|
+
const topN = parseInt(args.top as string) || 30;
|
|
174
|
+
const showPairs = args.pairs as boolean ?? false;
|
|
175
|
+
const outputJson = args.json as boolean ?? false;
|
|
176
|
+
const watch = args.watch as boolean ?? false;
|
|
177
|
+
const intervalSec = args.interval ? parseInt(args.interval as string) : 60;
|
|
178
|
+
|
|
179
|
+
const client = getClient();
|
|
180
|
+
if (args.verbose) client.verbose = true;
|
|
181
|
+
|
|
182
|
+
console.log('Open Broker - Funding Rate Scanner');
|
|
183
|
+
console.log('==================================\n');
|
|
184
|
+
console.log(`Threshold: ${threshold}% annualized | Scope: ${mainOnly ? 'main only' : hip3Only ? 'HIP-3 only' : 'all dexes'}\n`);
|
|
185
|
+
|
|
186
|
+
const options = { threshold, mainOnly, hip3Only, topN };
|
|
187
|
+
|
|
188
|
+
const runScan = async () => {
|
|
189
|
+
const results = await scanFunding(client, options);
|
|
190
|
+
|
|
191
|
+
if (outputJson) {
|
|
192
|
+
console.log(JSON.stringify(results, null, 2));
|
|
193
|
+
} else {
|
|
194
|
+
printResults(results, showPairs);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return results;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await runScan();
|
|
201
|
+
|
|
202
|
+
if (watch) {
|
|
203
|
+
console.log(`\nWatching every ${intervalSec}s... (Ctrl+C to stop)\n`);
|
|
204
|
+
while (true) {
|
|
205
|
+
await sleep(intervalSec * 1000);
|
|
206
|
+
console.log(`\n[${new Date().toLocaleTimeString()}] Rescanning...\n`);
|
|
207
|
+
await runScan();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
main().catch(err => { console.error(err); process.exit(1); });
|
package/scripts/info/funding.ts
CHANGED
|
@@ -24,12 +24,14 @@ async function main() {
|
|
|
24
24
|
console.log('===========================\n');
|
|
25
25
|
|
|
26
26
|
const client = getClient();
|
|
27
|
+
const includeHip3 = args['include-hip3'] as boolean || args['hip3'] as boolean || (filterCoin?.includes(':') ?? false);
|
|
27
28
|
|
|
28
29
|
try {
|
|
29
30
|
const meta = await client.getMetaAndAssetCtxs();
|
|
30
31
|
|
|
31
32
|
const fundingData: FundingDisplay[] = [];
|
|
32
33
|
|
|
34
|
+
// Main dex assets
|
|
33
35
|
for (let i = 0; i < meta.meta.universe.length; i++) {
|
|
34
36
|
const asset = meta.meta.universe[i];
|
|
35
37
|
const ctx = meta.assetCtxs[i];
|
|
@@ -55,6 +57,39 @@ async function main() {
|
|
|
55
57
|
});
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
// Include HIP-3 dex assets
|
|
61
|
+
if (includeHip3 || showAll || (filterCoin && filterCoin.includes(':'))) {
|
|
62
|
+
const allPerps = await client.getAllPerpMetas();
|
|
63
|
+
for (const dexData of allPerps) {
|
|
64
|
+
if (!dexData.dexName) continue; // Skip main dex (already loaded)
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < dexData.meta.universe.length; i++) {
|
|
67
|
+
const asset = dexData.meta.universe[i];
|
|
68
|
+
const ctx = dexData.assetCtxs[i];
|
|
69
|
+
if (!ctx) continue;
|
|
70
|
+
|
|
71
|
+
const prefixedName = `${dexData.dexName}:${asset.name}`;
|
|
72
|
+
if (filterCoin && prefixedName !== filterCoin) continue;
|
|
73
|
+
|
|
74
|
+
const hourlyRate = parseFloat(ctx.funding);
|
|
75
|
+
const annualizedRate = annualizeFundingRate(hourlyRate);
|
|
76
|
+
const openInterest = parseFloat(ctx.openInterest);
|
|
77
|
+
const markPx = parseFloat(ctx.markPx);
|
|
78
|
+
|
|
79
|
+
if (!showAll && openInterest < 1000) continue;
|
|
80
|
+
|
|
81
|
+
fundingData.push({
|
|
82
|
+
coin: prefixedName,
|
|
83
|
+
hourlyRate,
|
|
84
|
+
annualizedRate,
|
|
85
|
+
premium: 0,
|
|
86
|
+
openInterest,
|
|
87
|
+
markPx,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
// Sort
|
|
59
94
|
if (sortBy === 'hourly' || sortBy === 'annualized') {
|
|
60
95
|
fundingData.sort((a, b) => Math.abs(b.annualizedRate) - Math.abs(a.annualizedRate));
|
package/scripts/info/markets.ts
CHANGED
|
@@ -26,6 +26,7 @@ async function main() {
|
|
|
26
26
|
console.log('=====================\n');
|
|
27
27
|
|
|
28
28
|
const client = getClient();
|
|
29
|
+
const includeHip3 = args['include-hip3'] as boolean || args['hip3'] as boolean || (filterCoin?.includes(':') ?? false);
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
32
|
const meta = await client.getMetaAndAssetCtxs();
|
|
@@ -59,6 +60,42 @@ async function main() {
|
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// Include HIP-3 markets
|
|
64
|
+
if (includeHip3 || (filterCoin && filterCoin.includes(':'))) {
|
|
65
|
+
const allPerps = await client.getAllPerpMetas();
|
|
66
|
+
for (const dexData of allPerps) {
|
|
67
|
+
if (!dexData.dexName) continue;
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < dexData.meta.universe.length; i++) {
|
|
70
|
+
const asset = dexData.meta.universe[i];
|
|
71
|
+
const ctx = dexData.assetCtxs[i];
|
|
72
|
+
if (!ctx) continue;
|
|
73
|
+
|
|
74
|
+
const prefixedName = `${dexData.dexName}:${asset.name}`;
|
|
75
|
+
if (filterCoin && prefixedName !== filterCoin) continue;
|
|
76
|
+
|
|
77
|
+
const markPx = parseFloat(ctx.markPx);
|
|
78
|
+
const oraclePx = parseFloat(ctx.oraclePx);
|
|
79
|
+
const prevDayPx = parseFloat(ctx.prevDayPx);
|
|
80
|
+
const volume24h = parseFloat(ctx.dayNtlVlm);
|
|
81
|
+
const openInterest = parseFloat(ctx.openInterest);
|
|
82
|
+
const change24h = prevDayPx > 0 ? (markPx - prevDayPx) / prevDayPx : 0;
|
|
83
|
+
|
|
84
|
+
markets.push({
|
|
85
|
+
coin: prefixedName,
|
|
86
|
+
markPx,
|
|
87
|
+
oraclePx,
|
|
88
|
+
prevDayPx,
|
|
89
|
+
change24h,
|
|
90
|
+
volume24h,
|
|
91
|
+
openInterest,
|
|
92
|
+
maxLeverage: asset.maxLeverage,
|
|
93
|
+
szDecimals: asset.szDecimals,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
62
99
|
// Sort
|
|
63
100
|
if (sortBy === 'volume') {
|
|
64
101
|
markets.sort((a, b) => b.volume24h - a.volume24h);
|
package/scripts/info/trades.ts
CHANGED
|
@@ -41,6 +41,9 @@ async function main() {
|
|
|
41
41
|
console.log('='.repeat(40) + '\n');
|
|
42
42
|
|
|
43
43
|
try {
|
|
44
|
+
// Load metadata (needed for HIP-3 coin resolution)
|
|
45
|
+
await client.getMetaAndAssetCtxs();
|
|
46
|
+
|
|
44
47
|
let trades = await client.getRecentTrades(coin.toUpperCase());
|
|
45
48
|
|
|
46
49
|
// Most recent first
|
package/scripts/plugin/tools.ts
CHANGED
|
@@ -228,6 +228,29 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// Search HIP-3 perps
|
|
232
|
+
if (!typeFilter || typeFilter === 'hip3' || typeFilter === 'perp') {
|
|
233
|
+
try {
|
|
234
|
+
const allPerps = await client.getAllPerpMetas();
|
|
235
|
+
for (const dexData of allPerps) {
|
|
236
|
+
if (!dexData.dexName) continue; // Skip main dex (already searched)
|
|
237
|
+
for (let i = 0; i < dexData.meta.universe.length; i++) {
|
|
238
|
+
const asset = dexData.meta.universe[i];
|
|
239
|
+
if (asset.name.toUpperCase().includes(query)) {
|
|
240
|
+
results.push({
|
|
241
|
+
coin: `${dexData.dexName}:${asset.name}`,
|
|
242
|
+
type: 'hip3',
|
|
243
|
+
dex: dexData.dexName,
|
|
244
|
+
markPx: dexData.assetCtxs[i]?.markPx,
|
|
245
|
+
dayVolume: dexData.assetCtxs[i]?.dayNtlVlm,
|
|
246
|
+
maxLeverage: asset.maxLeverage,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch { /* HIP-3 may not be available */ }
|
|
252
|
+
}
|
|
253
|
+
|
|
231
254
|
// Search spot
|
|
232
255
|
if (!typeFilter || typeFilter === 'spot') {
|
|
233
256
|
try {
|
|
@@ -472,6 +495,8 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
472
495
|
'1d': 86_400_000, '3d': 259_200_000, '1w': 604_800_000, '1M': 2_592_000_000,
|
|
473
496
|
};
|
|
474
497
|
|
|
498
|
+
// Load metadata for HIP-3 coin resolution
|
|
499
|
+
await client.getMetaAndAssetCtxs();
|
|
475
500
|
const now = Date.now();
|
|
476
501
|
const startTime = now - (bars * (intervalMs[interval] || 3_600_000));
|
|
477
502
|
const candles = await client.getCandleSnapshot(coin, interval, startTime);
|
|
@@ -512,6 +537,8 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
512
537
|
const hours = (params.hours as number) || 24;
|
|
513
538
|
const startTime = Date.now() - (hours * 3_600_000);
|
|
514
539
|
|
|
540
|
+
// Load metadata for HIP-3 coin resolution
|
|
541
|
+
await client.getMetaAndAssetCtxs();
|
|
515
542
|
const history = await client.getFundingHistory(coin, startTime);
|
|
516
543
|
|
|
517
544
|
const rates = history.map(e => parseFloat(e.fundingRate));
|
|
@@ -548,6 +575,8 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
548
575
|
const client = getClient();
|
|
549
576
|
|
|
550
577
|
const coin = (params.coin as string).toUpperCase();
|
|
578
|
+
// Load metadata for HIP-3 coin resolution
|
|
579
|
+
await client.getMetaAndAssetCtxs();
|
|
551
580
|
let trades = await client.getRecentTrades(coin);
|
|
552
581
|
|
|
553
582
|
trades.sort((a, b) => b.time - a.time);
|
|
@@ -600,6 +629,97 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
600
629
|
},
|
|
601
630
|
},
|
|
602
631
|
|
|
632
|
+
{
|
|
633
|
+
name: 'ob_funding_scan',
|
|
634
|
+
description: 'Scan funding rates across all dexes (main + HIP-3) for arbitrage opportunities',
|
|
635
|
+
parameters: {
|
|
636
|
+
type: 'object',
|
|
637
|
+
properties: {
|
|
638
|
+
threshold: { type: 'number', description: 'Min annualized funding rate % to show (default: 25)' },
|
|
639
|
+
mainOnly: { type: 'boolean', description: 'Only scan main perps' },
|
|
640
|
+
hip3Only: { type: 'boolean', description: 'Only scan HIP-3 perps' },
|
|
641
|
+
top: { type: 'number', description: 'Number of results (default: 30)' },
|
|
642
|
+
pairs: { type: 'boolean', description: 'Show opposing funding pairs' },
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
async execute(_id, params) {
|
|
646
|
+
const { getClient } = await import('../core/client.js');
|
|
647
|
+
const { annualizeFundingRate } = await import('../core/utils.js');
|
|
648
|
+
const client = getClient();
|
|
649
|
+
|
|
650
|
+
const threshold = (params.threshold as number) ?? 25;
|
|
651
|
+
const mainOnly = params.mainOnly as boolean ?? false;
|
|
652
|
+
const hip3Only = params.hip3Only as boolean ?? false;
|
|
653
|
+
const topN = (params.top as number) ?? 30;
|
|
654
|
+
|
|
655
|
+
const allPerps = await client.getAllPerpMetas();
|
|
656
|
+
const results: Array<Record<string, unknown>> = [];
|
|
657
|
+
|
|
658
|
+
for (const dexData of allPerps) {
|
|
659
|
+
const isMain = !dexData.dexName;
|
|
660
|
+
if (mainOnly && !isMain) continue;
|
|
661
|
+
if (hip3Only && isMain) continue;
|
|
662
|
+
|
|
663
|
+
for (let i = 0; i < dexData.meta.universe.length; i++) {
|
|
664
|
+
const asset = dexData.meta.universe[i];
|
|
665
|
+
const ctx = dexData.assetCtxs[i];
|
|
666
|
+
if (!ctx) continue;
|
|
667
|
+
|
|
668
|
+
const hourlyRate = parseFloat(ctx.funding);
|
|
669
|
+
const annualizedPct = annualizeFundingRate(hourlyRate) * 100;
|
|
670
|
+
const openInterest = parseFloat(ctx.openInterest);
|
|
671
|
+
|
|
672
|
+
if (Math.abs(annualizedPct) < threshold) continue;
|
|
673
|
+
if (openInterest < 100) continue;
|
|
674
|
+
|
|
675
|
+
results.push({
|
|
676
|
+
coin: dexData.dexName ? `${dexData.dexName}:${asset.name}` : asset.name,
|
|
677
|
+
dex: dexData.dexName ?? 'main',
|
|
678
|
+
annualizedPct: annualizedPct.toFixed(1),
|
|
679
|
+
direction: hourlyRate > 0 ? 'longs pay shorts' : 'shorts pay longs',
|
|
680
|
+
collectBy: hourlyRate > 0 ? 'SHORT' : 'LONG',
|
|
681
|
+
openInterest: ctx.openInterest,
|
|
682
|
+
markPx: ctx.markPx,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
results.sort((a, b) => Math.abs(parseFloat(b.annualizedPct as string)) - Math.abs(parseFloat(a.annualizedPct as string)));
|
|
688
|
+
|
|
689
|
+
const output: Record<string, unknown> = {
|
|
690
|
+
threshold,
|
|
691
|
+
scope: mainOnly ? 'main' : hip3Only ? 'hip3' : 'all',
|
|
692
|
+
results: results.slice(0, topN),
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// Find opposing pairs if requested
|
|
696
|
+
if (params.pairs) {
|
|
697
|
+
const longs = results.filter(r => parseFloat(r.annualizedPct as string) > 0);
|
|
698
|
+
const shorts = results.filter(r => parseFloat(r.annualizedPct as string) < 0);
|
|
699
|
+
const pairs: Array<Record<string, unknown>> = [];
|
|
700
|
+
|
|
701
|
+
for (const l of longs) {
|
|
702
|
+
for (const s of shorts) {
|
|
703
|
+
const spread = parseFloat(l.annualizedPct as string) + Math.abs(parseFloat(s.annualizedPct as string));
|
|
704
|
+
if (spread > 20) {
|
|
705
|
+
pairs.push({
|
|
706
|
+
short: l.coin,
|
|
707
|
+
shortFunding: l.annualizedPct,
|
|
708
|
+
long: s.coin,
|
|
709
|
+
longFunding: s.annualizedPct,
|
|
710
|
+
spreadPct: spread.toFixed(1),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
pairs.sort((a, b) => parseFloat(b.spreadPct as string) - parseFloat(a.spreadPct as string));
|
|
716
|
+
output.opposingPairs = pairs.slice(0, 10);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return json(output);
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
|
|
603
723
|
// ── Trading Tools ───────────────────────────────────────────
|
|
604
724
|
|
|
605
725
|
{
|
package/scripts/setup/onboard.ts
CHANGED
|
@@ -268,11 +268,13 @@ async function main(): Promise<OnboardResult> {
|
|
|
268
268
|
console.log('Step 1/3: Wallet Setup');
|
|
269
269
|
console.log('----------------------');
|
|
270
270
|
console.log('How would you like to set up your wallet?\n');
|
|
271
|
-
console.log(' 1)
|
|
272
|
-
console.log('
|
|
273
|
-
console.log('
|
|
274
|
-
console.log('
|
|
275
|
-
console.log('
|
|
271
|
+
console.log(' 1) Generate a fresh wallet (recommended for agents)');
|
|
272
|
+
console.log(' Creates a dedicated trading wallet. Builder fee is auto-approved.');
|
|
273
|
+
console.log(' Just fund it with USDC and start trading — no browser steps needed.');
|
|
274
|
+
console.log('');
|
|
275
|
+
console.log(' 2) Import existing private key');
|
|
276
|
+
console.log(' 3) Generate API wallet (restricted, requires browser approval)');
|
|
277
|
+
console.log(' Can trade but cannot withdraw. Requires master wallet approval in browser.\n');
|
|
276
278
|
|
|
277
279
|
let choice = '';
|
|
278
280
|
while (choice !== '1' && choice !== '2' && choice !== '3') {
|
|
@@ -292,7 +294,7 @@ async function main(): Promise<OnboardResult> {
|
|
|
292
294
|
// Options 1 & 2: Master wallet flow
|
|
293
295
|
let privateKey: `0x${string}`;
|
|
294
296
|
|
|
295
|
-
if (choice === '
|
|
297
|
+
if (choice === '2') {
|
|
296
298
|
// User has existing key
|
|
297
299
|
const rl2 = createReadline();
|
|
298
300
|
console.log('\nEnter your private key (0x... format):\n');
|
|
@@ -313,7 +315,7 @@ async function main(): Promise<OnboardResult> {
|
|
|
313
315
|
|
|
314
316
|
console.log('\n✅ Private key accepted');
|
|
315
317
|
} else {
|
|
316
|
-
// Generate new wallet
|
|
318
|
+
// Generate new wallet (option 1)
|
|
317
319
|
console.log('\nGenerating new wallet...');
|
|
318
320
|
privateKey = generatePrivateKey();
|
|
319
321
|
console.log('✅ New wallet created');
|
|
@@ -385,7 +387,7 @@ HYPERLIQUID_NETWORK=mainnet
|
|
|
385
387
|
console.log(`Network: Hyperliquid (Mainnet)`);
|
|
386
388
|
console.log(`Config: ${CONFIG_PATH}`);
|
|
387
389
|
|
|
388
|
-
if (choice === '2') {
|
|
390
|
+
if (choice === '1' || choice === '2') {
|
|
389
391
|
console.log('\n⚠️ IMPORTANT: Save your private key!');
|
|
390
392
|
console.log('-----------------------------------');
|
|
391
393
|
console.log(`Private Key: ${privateKey}`);
|
|
@@ -95,11 +95,36 @@ async function main() {
|
|
|
95
95
|
console.log('===============================\n');
|
|
96
96
|
|
|
97
97
|
try {
|
|
98
|
-
// Initial funding check
|
|
98
|
+
// Initial funding check - loads main + HIP-3 assets
|
|
99
99
|
await client.getMetaAndAssetCtxs();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
|
|
101
|
+
// For HIP-3 assets, we need to fetch funding from the dex-specific metadata
|
|
102
|
+
let hourlyFunding: number;
|
|
103
|
+
let openInterestVal: number;
|
|
104
|
+
|
|
105
|
+
if (client.isHip3(coin)) {
|
|
106
|
+
const allPerps = await client.getAllPerpMetas();
|
|
107
|
+
const dexName = client.getCoinDex(coin);
|
|
108
|
+
const localName = client.getCoinLocalName(coin);
|
|
109
|
+
const dexData = allPerps.find(d => d.dexName === dexName);
|
|
110
|
+
if (!dexData) {
|
|
111
|
+
console.error(`Error: No market data for HIP-3 dex ${dexName}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const assetIdx = dexData.meta.universe.findIndex(a => a.name === localName);
|
|
115
|
+
if (assetIdx === -1 || !dexData.assetCtxs[assetIdx]) {
|
|
116
|
+
console.error(`Error: No market data for ${coin}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
hourlyFunding = parseFloat(dexData.assetCtxs[assetIdx].funding);
|
|
120
|
+
openInterestVal = parseFloat(dexData.assetCtxs[assetIdx].openInterest);
|
|
121
|
+
} else {
|
|
122
|
+
const meta = await client.getMetaAndAssetCtxs();
|
|
123
|
+
const assetIndex = client.getAssetIndex(coin);
|
|
124
|
+
const assetCtx = meta.assetCtxs[assetIndex];
|
|
125
|
+
hourlyFunding = parseFloat(assetCtx.funding);
|
|
126
|
+
openInterestVal = parseFloat(assetCtx.openInterest);
|
|
127
|
+
}
|
|
103
128
|
|
|
104
129
|
const mids = await client.getAllMids();
|
|
105
130
|
const midPrice = parseFloat(mids[coin]);
|
|
@@ -108,9 +133,7 @@ async function main() {
|
|
|
108
133
|
process.exit(1);
|
|
109
134
|
}
|
|
110
135
|
|
|
111
|
-
const hourlyFunding = parseFloat(assetCtx.funding);
|
|
112
136
|
const annualizedFunding = annualizeFundingRate(hourlyFunding) * 100; // as percentage
|
|
113
|
-
const openInterest = parseFloat(assetCtx.openInterest);
|
|
114
137
|
const positionSize = notionalSize / midPrice;
|
|
115
138
|
|
|
116
139
|
console.log('Current Market State');
|
|
@@ -119,7 +142,7 @@ async function main() {
|
|
|
119
142
|
console.log(`Mid Price: ${formatUsd(midPrice)}`);
|
|
120
143
|
console.log(`Hourly Funding: ${(hourlyFunding * 100).toFixed(4)}%`);
|
|
121
144
|
console.log(`Annualized: ${annualizedFunding.toFixed(2)}%`);
|
|
122
|
-
console.log(`Open Interest: ${formatUsd(
|
|
145
|
+
console.log(`Open Interest: ${formatUsd(openInterestVal)}`);
|
|
123
146
|
console.log(`\nStrategy Config`);
|
|
124
147
|
console.log('---------------');
|
|
125
148
|
console.log(`Target Notional: ${formatUsd(notionalSize)}`);
|
|
@@ -218,9 +241,19 @@ async function main() {
|
|
|
218
241
|
await sleep(checkInterval);
|
|
219
242
|
|
|
220
243
|
// Get updated funding
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
244
|
+
let newHourlyFunding: number;
|
|
245
|
+
if (client.isHip3(coin)) {
|
|
246
|
+
const freshPerps = await client.getAllPerpMetas();
|
|
247
|
+
const dexName = client.getCoinDex(coin);
|
|
248
|
+
const localName = client.getCoinLocalName(coin);
|
|
249
|
+
const dexData = freshPerps.find(d => d.dexName === dexName);
|
|
250
|
+
const idx = dexData?.meta.universe.findIndex(a => a.name === localName) ?? -1;
|
|
251
|
+
newHourlyFunding = idx >= 0 && dexData?.assetCtxs[idx] ? parseFloat(dexData.assetCtxs[idx].funding) : 0;
|
|
252
|
+
} else {
|
|
253
|
+
const newMeta = await client.getMetaAndAssetCtxs();
|
|
254
|
+
const assetIndex = client.getAssetIndex(coin);
|
|
255
|
+
newHourlyFunding = parseFloat(newMeta.assetCtxs[assetIndex].funding);
|
|
256
|
+
}
|
|
224
257
|
const newAnnualized = annualizeFundingRate(newHourlyFunding) * 100;
|
|
225
258
|
const newMids = await client.getAllMids();
|
|
226
259
|
const newPrice = parseFloat(newMids[coin]);
|