openbroker 1.0.51 → 1.0.53

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,16 @@
2
2
 
3
3
  All notable changes to Open Broker will be documented in this file.
4
4
 
5
+ ## [1.0.53] - 2026-03-09
6
+
7
+ ### Fixed
8
+ - **HIP-3 Isolated Margin: Use Max Leverage**: The 3x leverage cap was too conservative for isolated margin — at 3x a $83 CL position needs $28 margin, which gets rejected even on funded accounts. Now uses the asset's `maxLeverage` (e.g., 20x for CL, 25x for GOLD) to minimize margin requirement. At 20x isolated, the same position only needs ~$4 margin.
9
+
10
+ ## [1.0.52] - 2026-03-09
11
+
12
+ ### Fixed
13
+ - **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 on first order for each HIP-3 asset. Affects all trading commands: `buy`, `sell`, `market`, `limit`, `trigger`, `tpsl`, `bracket`, `chase`, `twap`, `scale`.
14
+
5
15
  ## [1.0.51] - 2026-03-09
6
16
 
7
17
  ### Added
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.51", "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.53", "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
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.51",
4
+ "version": "1.0.53",
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.51",
3
+ "version": "1.0.53",
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) {
@@ -1060,12 +1065,41 @@ export class HyperliquidClient {
1060
1065
  return data;
1061
1066
  }
1062
1067
 
1063
- async getUserState(user?: string): Promise<ClearinghouseState> {
1064
- this.log('Fetching clearinghouseState for:', user ?? this.address);
1065
- const response = await this.info.clearinghouseState({ user: user ?? this.address });
1068
+ async getUserState(user?: string, dex?: string): Promise<ClearinghouseState> {
1069
+ this.log('Fetching clearinghouseState for:', user ?? this.address, dex ? `dex: ${dex}` : '');
1070
+ const params: { user: string; dex?: string } = { user: user ?? this.address };
1071
+ if (dex !== undefined) params.dex = dex;
1072
+ const response = await this.info.clearinghouseState(params as any);
1066
1073
  return response as ClearinghouseState;
1067
1074
  }
1068
1075
 
1076
+ /**
1077
+ * Get user state across all dexes (main + HIP-3).
1078
+ * Returns the main state with HIP-3 positions merged into assetPositions.
1079
+ */
1080
+ async getUserStateAll(user?: string): Promise<ClearinghouseState> {
1081
+ await this.getMetaAndAssetCtxs(); // Ensure HIP-3 dex list is loaded
1082
+
1083
+ const mainState = await this.getUserState(user);
1084
+ const dexs = await this.getPerpDexs();
1085
+
1086
+ for (let i = 1; i < dexs.length; i++) {
1087
+ const dex = dexs[i];
1088
+ if (!dex) continue;
1089
+
1090
+ try {
1091
+ const dexState = await this.getUserState(user, dex.name);
1092
+ if (dexState.assetPositions?.length > 0) {
1093
+ mainState.assetPositions.push(...dexState.assetPositions);
1094
+ }
1095
+ } catch (err) {
1096
+ this.log(`Failed to fetch state for dex ${dex.name}:`, err instanceof Error ? err.message : String(err));
1097
+ }
1098
+ }
1099
+
1100
+ return mainState;
1101
+ }
1102
+
1069
1103
  async getOpenOrders(user?: string): Promise<OpenOrder[]> {
1070
1104
  this.log('Fetching openOrders for:', user ?? this.address);
1071
1105
  const response = await this.info.openOrders({ user: user ?? this.address });
@@ -1074,6 +1108,52 @@ export class HyperliquidClient {
1074
1108
 
1075
1109
  // ============ Trading ============
1076
1110
 
1111
+ /**
1112
+ * HIP-3 perps have independent margin per dex. Before ordering:
1113
+ * 1. Set isolated margin mode (required for HIP-3)
1114
+ * 2. Transfer USDC from main perp to the HIP-3 dex (each dex has its own balance)
1115
+ */
1116
+ private async ensureHip3Ready(coin: string, notional: number): Promise<void> {
1117
+ if (!this.isHip3(coin)) return;
1118
+
1119
+ const dexInfo = this.coinDexMap.get(coin);
1120
+ if (!dexInfo?.dexName) return;
1121
+
1122
+ // Set isolated margin on first order per asset
1123
+ if (!this.hip3IsolatedSet.has(coin)) {
1124
+ const maxLev = this.hip3MaxLeverageMap.get(coin) ?? 10;
1125
+ this.log(`HIP-3 asset ${coin} (dex: ${dexInfo.dexName}) — setting isolated margin at ${maxLev}x`);
1126
+ try {
1127
+ await this.updateLeverage(coin, maxLev, false); // false = isolated
1128
+ this.hip3IsolatedSet.add(coin);
1129
+ } catch (err) {
1130
+ this.log(`Failed to set isolated margin for ${coin}:`, err instanceof Error ? err.message : String(err));
1131
+ this.hip3IsolatedSet.add(coin);
1132
+ }
1133
+ }
1134
+
1135
+ // Transfer USDC to the HIP-3 dex to cover margin
1136
+ const maxLev = this.hip3MaxLeverageMap.get(coin) ?? 10;
1137
+ const requiredMargin = notional / maxLev;
1138
+ // Add 20% buffer for fees and slippage
1139
+ const transferAmount = Math.ceil(requiredMargin * 1.2 * 100) / 100;
1140
+
1141
+ this.log(`HIP-3 margin transfer: ${transferAmount} USDC from main → ${dexInfo.dexName} (notional: ${notional}, leverage: ${maxLev}x)`);
1142
+ try {
1143
+ await this.exchange.sendAsset({
1144
+ destination: this.address as `0x${string}`,
1145
+ sourceDex: '', // main perp dex
1146
+ destinationDex: dexInfo.dexName,
1147
+ token: 'USDC:0x6d1e7cde53ba9467b783cb7c530ce054',
1148
+ amount: String(transferAmount),
1149
+ });
1150
+ this.log(`Transferred ${transferAmount} USDC to ${dexInfo.dexName} dex`);
1151
+ } catch (err) {
1152
+ // Log but don't block — dex may already have sufficient balance
1153
+ this.log(`Margin transfer to ${dexInfo.dexName} failed:`, err instanceof Error ? err.message : String(err));
1154
+ }
1155
+ }
1156
+
1077
1157
  async order(
1078
1158
  coin: string,
1079
1159
  isBuy: boolean,
@@ -1086,6 +1166,9 @@ export class HyperliquidClient {
1086
1166
  this.requireTrading();
1087
1167
  await this.getMetaAndAssetCtxs();
1088
1168
 
1169
+ // HIP-3 perps: set isolated margin + transfer USDC to dex
1170
+ await this.ensureHip3Ready(coin, size * price);
1171
+
1089
1172
  const assetIndex = this.getAssetIndex(coin);
1090
1173
  const szDecimals = this.getSzDecimals(coin);
1091
1174
 
@@ -1204,6 +1287,9 @@ export class HyperliquidClient {
1204
1287
  this.requireTrading();
1205
1288
  await this.getMetaAndAssetCtxs();
1206
1289
 
1290
+ // HIP-3 perps: set isolated margin + transfer USDC to dex
1291
+ await this.ensureHip3Ready(coin, size * limitPrice);
1292
+
1207
1293
  const assetIndex = this.getAssetIndex(coin);
1208
1294
  const szDecimals = this.getSzDecimals(coin);
1209
1295
 
@@ -30,7 +30,7 @@ async function main() {
30
30
  console.log('');
31
31
 
32
32
  try {
33
- const state = await client.getUserState();
33
+ const state = await client.getUserStateAll();
34
34
 
35
35
  const margin = state.crossMarginSummary;
36
36
  const accountValue = parseFloat(margin.accountValue);
@@ -14,7 +14,7 @@ async function main() {
14
14
 
15
15
  try {
16
16
  const [state, mids, fundingHistory] = await Promise.all([
17
- client.getUserState(),
17
+ client.getUserStateAll(),
18
18
  client.getAllMids(),
19
19
  client.getUserFunding(),
20
20
  ]);
@@ -34,7 +34,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
34
34
  async execute(_id, params) {
35
35
  const { getClient } = await import('../core/client.js');
36
36
  const client = getClient();
37
- const state = await client.getUserState();
37
+ const state = await client.getUserStateAll();
38
38
 
39
39
  const result: Record<string, unknown> = {
40
40
  address: client.address,
@@ -85,7 +85,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
85
85
  async execute(_id, params) {
86
86
  const { getClient } = await import('../core/client.js');
87
87
  const client = getClient();
88
- const state = await client.getUserState();
88
+ const state = await client.getUserStateAll();
89
89
 
90
90
  let positions = state.assetPositions
91
91
  .filter(ap => parseFloat(ap.position.szi) !== 0)
@@ -969,7 +969,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
969
969
  const coin = normalizeCoin(params.coin as string);
970
970
 
971
971
  // Get current position
972
- const state = await client.getUserState();
972
+ const state = await client.getUserStateAll();
973
973
  const position = state.assetPositions.find(
974
974
  ap => ap.position.coin === coin && parseFloat(ap.position.szi) !== 0,
975
975
  );
@@ -117,7 +117,7 @@ export class PositionWatcher implements PluginService {
117
117
  try {
118
118
  const { getClient } = await import('../core/client.js');
119
119
  const client = getClient();
120
- const state = await client.getUserState(this.accountAddress);
120
+ const state = await client.getUserStateAll(this.accountAddress);
121
121
 
122
122
  const snapshot = this.buildSnapshot(state);
123
123