openbroker 1.0.52 → 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 +6 -1
- package/SKILL.md +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/core/client.ts +71 -28
- 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,10 +2,15 @@
|
|
|
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
|
+
|
|
5
10
|
## [1.0.52] - 2026-03-09
|
|
6
11
|
|
|
7
12
|
### 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
|
|
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`.
|
|
9
14
|
|
|
10
15
|
## [1.0.51] - 2026-03-09
|
|
11
16
|
|
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
|
@@ -1065,12 +1065,41 @@ export class HyperliquidClient {
|
|
|
1065
1065
|
return data;
|
|
1066
1066
|
}
|
|
1067
1067
|
|
|
1068
|
-
async getUserState(user?: string): Promise<ClearinghouseState> {
|
|
1069
|
-
this.log('Fetching clearinghouseState for:', user ?? this.address);
|
|
1070
|
-
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);
|
|
1071
1073
|
return response as ClearinghouseState;
|
|
1072
1074
|
}
|
|
1073
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
|
+
|
|
1074
1103
|
async getOpenOrders(user?: string): Promise<OpenOrder[]> {
|
|
1075
1104
|
this.log('Fetching openOrders for:', user ?? this.address);
|
|
1076
1105
|
const response = await this.info.openOrders({ user: user ?? this.address });
|
|
@@ -1080,37 +1109,51 @@ export class HyperliquidClient {
|
|
|
1080
1109
|
// ============ Trading ============
|
|
1081
1110
|
|
|
1082
1111
|
/**
|
|
1083
|
-
* HIP-3 perps
|
|
1084
|
-
*
|
|
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)
|
|
1085
1115
|
*/
|
|
1086
|
-
private async
|
|
1116
|
+
private async ensureHip3Ready(coin: string, notional: number): Promise<void> {
|
|
1087
1117
|
if (!this.isHip3(coin)) return;
|
|
1088
|
-
if (this.hip3IsolatedSet.has(coin)) return;
|
|
1089
1118
|
|
|
1090
1119
|
const dexInfo = this.coinDexMap.get(coin);
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
+
}
|
|
1093
1134
|
|
|
1094
|
-
|
|
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)`);
|
|
1095
1142
|
try {
|
|
1096
|
-
await this.
|
|
1097
|
-
|
|
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`);
|
|
1098
1151
|
} catch (err) {
|
|
1099
|
-
// Log but don't block —
|
|
1100
|
-
this.log(`
|
|
1101
|
-
this.hip3IsolatedSet.add(coin); // Don't retry every order
|
|
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));
|
|
1102
1154
|
}
|
|
1103
1155
|
}
|
|
1104
1156
|
|
|
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
|
-
|
|
1114
1157
|
async order(
|
|
1115
1158
|
coin: string,
|
|
1116
1159
|
isBuy: boolean,
|
|
@@ -1123,8 +1166,8 @@ export class HyperliquidClient {
|
|
|
1123
1166
|
this.requireTrading();
|
|
1124
1167
|
await this.getMetaAndAssetCtxs();
|
|
1125
1168
|
|
|
1126
|
-
// HIP-3 perps
|
|
1127
|
-
await this.
|
|
1169
|
+
// HIP-3 perps: set isolated margin + transfer USDC to dex
|
|
1170
|
+
await this.ensureHip3Ready(coin, size * price);
|
|
1128
1171
|
|
|
1129
1172
|
const assetIndex = this.getAssetIndex(coin);
|
|
1130
1173
|
const szDecimals = this.getSzDecimals(coin);
|
|
@@ -1244,8 +1287,8 @@ export class HyperliquidClient {
|
|
|
1244
1287
|
this.requireTrading();
|
|
1245
1288
|
await this.getMetaAndAssetCtxs();
|
|
1246
1289
|
|
|
1247
|
-
// HIP-3 perps
|
|
1248
|
-
await this.
|
|
1290
|
+
// HIP-3 perps: set isolated margin + transfer USDC to dex
|
|
1291
|
+
await this.ensureHip3Ready(coin, size * limitPrice);
|
|
1249
1292
|
|
|
1250
1293
|
const assetIndex = this.getAssetIndex(coin);
|
|
1251
1294
|
const szDecimals = this.getSzDecimals(coin);
|
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
|
|