openbroker 1.0.49 → 1.0.52
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 +16 -0
- package/SKILL.md +16 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/core/client.ts +43 -0
- package/scripts/plugin/tools.ts +28 -19
- package/scripts/plugin/watcher.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Open Broker will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.0.52] - 2026-03-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **HIP-3 Trading: Isolated Margin**: HIP-3 perps require isolated margin mode (per Hyperliquid docs), but orders were sent without setting it — causing "Insufficient margin to place order" rejections. Now automatically sets isolated margin (3x or asset max, whichever is lower) on first order for each HIP-3 asset. Affects all trading commands: `buy`, `sell`, `market`, `limit`, `trigger`, `tpsl`, `bracket`, `chase`, `twap`, `scale`.
|
|
9
|
+
|
|
10
|
+
## [1.0.51] - 2026-03-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Watcher Poll Logging**: Position watcher now logs each poll cycle at debug level — shows position count, equity, and margin usage so you can confirm the watcher is running.
|
|
14
|
+
|
|
15
|
+
## [1.0.50] - 2026-03-09
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Plugin `ob_search` HIP-3 Results**: Fixed empty results for HIP-3 assets when using the `ob_search` plugin tool. Added type filter normalization (handles `HIP3`, `HIP-3`, `hip3`, `all`), added `enum` constraint to type parameter schema, surfaced errors instead of silently swallowing them, and aligned HIP-3 iteration with CLI search (index-based with null guards).
|
|
19
|
+
- **SKILL.md**: Added "Finding Assets Before Trading" section instructing agents to always search for unfamiliar assets before trading, with examples of `ob_search` and `openbroker search`.
|
|
20
|
+
|
|
5
21
|
## [1.0.49] - 2026-03-09
|
|
6
22
|
|
|
7
23
|
### Fixed
|
package/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ 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.
|
|
7
|
+
metadata: {"author": "monemetrics", "version": "1.0.52", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
|
|
8
8
|
allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_twap ob_bracket ob_chase ob_watcher_status Bash(openbroker:*)
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -31,6 +31,21 @@ openbroker account
|
|
|
31
31
|
openbroker buy --coin ETH --size 0.1
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Important: Finding Assets Before Trading
|
|
35
|
+
|
|
36
|
+
**Always search before trading an unfamiliar asset.** Hyperliquid has main perps (ETH, BTC, SOL...), HIP-3 perps (xyz:CL, xyz:GOLD, km:USOIL...), and spot markets. Use search to discover the correct ticker:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
openbroker search --query GOLD # Find all GOLD markets across all providers
|
|
40
|
+
openbroker search --query oil # Find oil-related assets (CL, BRENTOIL, USOIL...)
|
|
41
|
+
openbroker search --query BTC --type perp # BTC perps only
|
|
42
|
+
openbroker search --query NATGAS --type hip3 # HIP-3 only
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or with the `ob_search` plugin tool: `{ "query": "gold" }` or `{ "query": "oil", "type": "hip3" }`
|
|
46
|
+
|
|
47
|
+
**HIP-3 assets use `dex:COIN` format** — e.g., `xyz:CL` not just `CL`. If you get an error like "No market data found", search for the asset to find the correct prefixed ticker. Common HIP-3 dexes: `xyz`, `flx`, `km`, `hyna`, `vntl`, `cash`.
|
|
48
|
+
|
|
34
49
|
## Command Reference
|
|
35
50
|
|
|
36
51
|
### Setup
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/scripts/core/client.ts
CHANGED
|
@@ -31,6 +31,10 @@ export class HyperliquidClient {
|
|
|
31
31
|
private perpDexsCache: Array<{ name: string; fullName: string; deployer: string } | null> | null = null;
|
|
32
32
|
/** Whether HIP-3 assets have been loaded into maps */
|
|
33
33
|
private hip3Loaded: boolean = false;
|
|
34
|
+
/** HIP-3 assets that have had isolated margin set this session */
|
|
35
|
+
private hip3IsolatedSet: Set<string> = new Set();
|
|
36
|
+
/** Cached maxLeverage for HIP-3 assets */
|
|
37
|
+
private hip3MaxLeverageMap: Map<string, number> = new Map();
|
|
34
38
|
public verbose: boolean = false;
|
|
35
39
|
|
|
36
40
|
constructor(config?: OpenBrokerConfig) {
|
|
@@ -166,6 +170,7 @@ export class HyperliquidClient {
|
|
|
166
170
|
this.assetMap.set(coinName, globalIndex);
|
|
167
171
|
this.szDecimalsMap.set(coinName, asset.szDecimals);
|
|
168
172
|
this.coinDexMap.set(coinName, { dexName: dex.name, dexIdx, localName });
|
|
173
|
+
if (asset.maxLeverage) this.hip3MaxLeverageMap.set(coinName, asset.maxLeverage);
|
|
169
174
|
});
|
|
170
175
|
}
|
|
171
176
|
} catch (e) {
|
|
@@ -1074,6 +1079,38 @@ export class HyperliquidClient {
|
|
|
1074
1079
|
|
|
1075
1080
|
// ============ Trading ============
|
|
1076
1081
|
|
|
1082
|
+
/**
|
|
1083
|
+
* HIP-3 perps require isolated margin mode. Automatically sets isolated margin
|
|
1084
|
+
* with default leverage (3x) on first order for each HIP-3 asset this session.
|
|
1085
|
+
*/
|
|
1086
|
+
private async ensureHip3Isolated(coin: string): Promise<void> {
|
|
1087
|
+
if (!this.isHip3(coin)) return;
|
|
1088
|
+
if (this.hip3IsolatedSet.has(coin)) return;
|
|
1089
|
+
|
|
1090
|
+
const dexInfo = this.coinDexMap.get(coin);
|
|
1091
|
+
const maxLev = this.getHip3MaxLeverage(coin);
|
|
1092
|
+
const leverage = Math.min(3, maxLev || 3);
|
|
1093
|
+
|
|
1094
|
+
this.log(`HIP-3 asset ${coin} (dex: ${dexInfo?.dexName}) — setting isolated margin at ${leverage}x`);
|
|
1095
|
+
try {
|
|
1096
|
+
await this.updateLeverage(coin, leverage, false); // false = isolated
|
|
1097
|
+
this.hip3IsolatedSet.add(coin);
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
// Log but don't block — might already be set
|
|
1100
|
+
this.log(`Failed to set isolated margin for ${coin}:`, err instanceof Error ? err.message : String(err));
|
|
1101
|
+
this.hip3IsolatedSet.add(coin); // Don't retry every order
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Get maxLeverage for a HIP-3 asset from cached metadata.
|
|
1107
|
+
*/
|
|
1108
|
+
private getHip3MaxLeverage(coin: string): number | null {
|
|
1109
|
+
// Already loaded via getMetaAndAssetCtxs → loadHip3Assets
|
|
1110
|
+
// Check if we cached it during loadHip3Assets
|
|
1111
|
+
return this.hip3MaxLeverageMap.get(coin) ?? null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1077
1114
|
async order(
|
|
1078
1115
|
coin: string,
|
|
1079
1116
|
isBuy: boolean,
|
|
@@ -1086,6 +1123,9 @@ export class HyperliquidClient {
|
|
|
1086
1123
|
this.requireTrading();
|
|
1087
1124
|
await this.getMetaAndAssetCtxs();
|
|
1088
1125
|
|
|
1126
|
+
// HIP-3 perps require isolated margin mode
|
|
1127
|
+
await this.ensureHip3Isolated(coin);
|
|
1128
|
+
|
|
1089
1129
|
const assetIndex = this.getAssetIndex(coin);
|
|
1090
1130
|
const szDecimals = this.getSzDecimals(coin);
|
|
1091
1131
|
|
|
@@ -1204,6 +1244,9 @@ export class HyperliquidClient {
|
|
|
1204
1244
|
this.requireTrading();
|
|
1205
1245
|
await this.getMetaAndAssetCtxs();
|
|
1206
1246
|
|
|
1247
|
+
// HIP-3 perps require isolated margin mode
|
|
1248
|
+
await this.ensureHip3Isolated(coin);
|
|
1249
|
+
|
|
1207
1250
|
const assetIndex = this.getAssetIndex(coin);
|
|
1208
1251
|
const szDecimals = this.getSzDecimals(coin);
|
|
1209
1252
|
|
package/scripts/plugin/tools.ts
CHANGED
|
@@ -200,7 +200,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
200
200
|
type: 'object',
|
|
201
201
|
properties: {
|
|
202
202
|
query: { type: 'string', description: 'Search query (e.g. GOLD, BTC, ETH)' },
|
|
203
|
-
type: { type: 'string', description: 'Filter by market type: perp, hip3, spot' },
|
|
203
|
+
type: { type: 'string', enum: ['perp', 'hip3', 'spot', 'all'], description: 'Filter by market type: perp, hip3, spot, or all (default: all)' },
|
|
204
204
|
},
|
|
205
205
|
required: ['query'],
|
|
206
206
|
},
|
|
@@ -208,35 +208,42 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
208
208
|
const { getClient } = await import('../core/client.js');
|
|
209
209
|
const client = getClient();
|
|
210
210
|
const query = (params.query as string).toUpperCase();
|
|
211
|
-
|
|
211
|
+
// Normalize type filter: lowercase, strip hyphens, treat "all" as no filter
|
|
212
|
+
const rawType = params.type ? String(params.type).toLowerCase().replace(/-/g, '') : undefined;
|
|
213
|
+
const typeFilter = rawType === 'all' ? undefined : rawType;
|
|
212
214
|
|
|
213
215
|
const results: Array<Record<string, unknown>> = [];
|
|
216
|
+
const errors: string[] = [];
|
|
214
217
|
|
|
215
218
|
// Search main perps
|
|
216
219
|
if (!typeFilter || typeFilter === 'perp') {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
220
|
+
try {
|
|
221
|
+
const { meta, assetCtxs } = await client.getMetaAndAssetCtxs();
|
|
222
|
+
for (let i = 0; i < meta.universe.length; i++) {
|
|
223
|
+
const asset = meta.universe[i];
|
|
224
|
+
if (asset.name.toUpperCase().includes(query)) {
|
|
225
|
+
results.push({
|
|
226
|
+
coin: asset.name,
|
|
227
|
+
type: 'perp',
|
|
228
|
+
markPx: assetCtxs[i]?.markPx,
|
|
229
|
+
dayVolume: assetCtxs[i]?.dayNtlVlm,
|
|
230
|
+
maxLeverage: asset.maxLeverage,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
228
233
|
}
|
|
229
|
-
}
|
|
234
|
+
} catch (e) { errors.push(`perp: ${e instanceof Error ? e.message : String(e)}`); }
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
// Search HIP-3 perps
|
|
233
238
|
if (!typeFilter || typeFilter === 'hip3' || typeFilter === 'perp') {
|
|
234
239
|
try {
|
|
235
240
|
const allPerps = await client.getAllPerpMetas();
|
|
236
|
-
for (
|
|
237
|
-
|
|
241
|
+
for (let dexIdx = 1; dexIdx < allPerps.length; dexIdx++) {
|
|
242
|
+
const dexData = allPerps[dexIdx];
|
|
243
|
+
if (!dexData || !dexData.meta?.universe) continue;
|
|
238
244
|
for (let i = 0; i < dexData.meta.universe.length; i++) {
|
|
239
245
|
const asset = dexData.meta.universe[i];
|
|
246
|
+
if (!asset) continue;
|
|
240
247
|
if (asset.name.toUpperCase().includes(query)) {
|
|
241
248
|
results.push({
|
|
242
249
|
// API returns names already prefixed (e.g., "xyz:CL")
|
|
@@ -250,7 +257,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
250
257
|
}
|
|
251
258
|
}
|
|
252
259
|
}
|
|
253
|
-
} catch {
|
|
260
|
+
} catch (e) { errors.push(`hip3: ${e instanceof Error ? e.message : String(e)}`); }
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
// Search spot
|
|
@@ -268,10 +275,12 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
|
268
275
|
});
|
|
269
276
|
}
|
|
270
277
|
}
|
|
271
|
-
} catch {
|
|
278
|
+
} catch (e) { errors.push(`spot: ${e instanceof Error ? e.message : String(e)}`); }
|
|
272
279
|
}
|
|
273
280
|
|
|
274
|
-
|
|
281
|
+
const response: Record<string, unknown> = { query, results };
|
|
282
|
+
if (errors.length > 0) response.errors = errors;
|
|
283
|
+
return json(response);
|
|
275
284
|
},
|
|
276
285
|
},
|
|
277
286
|
|
|
@@ -120,6 +120,12 @@ export class PositionWatcher implements PluginService {
|
|
|
120
120
|
const state = await client.getUserState(this.accountAddress);
|
|
121
121
|
|
|
122
122
|
const snapshot = this.buildSnapshot(state);
|
|
123
|
+
|
|
124
|
+
const posCount = snapshot.positions.size;
|
|
125
|
+
const equity = parseFloat(snapshot.equity).toFixed(2);
|
|
126
|
+
const marginPct = snapshot.marginUsedPct.toFixed(1);
|
|
127
|
+
this.logger.debug(`Poll: ${posCount} position(s), equity $${equity}, margin ${marginPct}%`);
|
|
128
|
+
|
|
123
129
|
const events = this.seeded ? this.detectEvents(snapshot) : [];
|
|
124
130
|
|
|
125
131
|
if (events.length > 0) {
|
|
@@ -128,6 +134,8 @@ export class PositionWatcher implements PluginService {
|
|
|
128
134
|
this.logger.info(`[${event.type}] ${event.message}`);
|
|
129
135
|
await this.sendHook(event);
|
|
130
136
|
}
|
|
137
|
+
} else if (this.seeded) {
|
|
138
|
+
this.logger.debug('No position changes detected');
|
|
131
139
|
}
|
|
132
140
|
|
|
133
141
|
this.previousSnapshot = snapshot;
|