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 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.49", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
7
+ metadata: {"author": "monemetrics", "version": "1.0.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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.49",
4
+ "version": "1.0.52",
5
5
  "description": "Trade on Hyperliquid DEX with background position monitoring",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.0.49",
3
+ "version": "1.0.52",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
 
@@ -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
- const typeFilter = params.type as string | undefined;
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
- const { meta, assetCtxs } = await client.getMetaAndAssetCtxs();
218
- for (let i = 0; i < meta.universe.length; i++) {
219
- const asset = meta.universe[i];
220
- if (asset.name.toUpperCase().includes(query)) {
221
- results.push({
222
- coin: asset.name,
223
- type: 'perp',
224
- markPx: assetCtxs[i]?.markPx,
225
- dayVolume: assetCtxs[i]?.dayNtlVlm,
226
- maxLeverage: asset.maxLeverage,
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 (const dexData of allPerps) {
237
- if (!dexData.dexName) continue; // Skip main dex (already searched)
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 { /* HIP-3 may not be available */ }
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 { /* spot may not be available */ }
278
+ } catch (e) { errors.push(`spot: ${e instanceof Error ? e.message : String(e)}`); }
272
279
  }
273
280
 
274
- return json({ query, results });
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;