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 +10 -0
- package/SKILL.md +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/core/client.ts +89 -3
- package/scripts/info/account.ts +1 -1
- package/scripts/info/positions.ts +1 -1
- package/scripts/plugin/tools.ts +3 -3
- package/scripts/plugin/watcher.ts +1 -1
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.
|
|
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
|
|
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) {
|
|
@@ -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
|
|
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
|
|
package/scripts/info/account.ts
CHANGED
package/scripts/plugin/tools.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
120
|
+
const state = await client.getUserStateAll(this.accountAddress);
|
|
121
121
|
|
|
122
122
|
const snapshot = this.buildSnapshot(state);
|
|
123
123
|
|