openbroker 1.0.41 → 1.0.42
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/SKILL.md +3 -3
- package/openclaw.plugin.json +86 -0
- package/package.json +5 -1
- package/scripts/plugin/cli.ts +127 -0
- package/scripts/plugin/config-bridge.ts +30 -0
- package/scripts/plugin/index.ts +57 -0
- package/scripts/plugin/tools.ts +715 -0
- package/scripts/plugin/types.ts +158 -0
- package/scripts/plugin/watcher.ts +312 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
// Agent Tools for the OpenBroker OpenClaw plugin
|
|
2
|
+
// Uses direct imports of core modules instead of shelling out to CLI
|
|
3
|
+
|
|
4
|
+
import type { PluginTool } from './types.js';
|
|
5
|
+
import type { PositionWatcher } from './watcher.js';
|
|
6
|
+
|
|
7
|
+
/** Helper to wrap a result as OpenClaw tool response */
|
|
8
|
+
function json(payload: unknown) {
|
|
9
|
+
return {
|
|
10
|
+
content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }],
|
|
11
|
+
details: payload,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Helper to wrap an error */
|
|
16
|
+
function error(message: string) {
|
|
17
|
+
return json({ error: message });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createTools(watcher: PositionWatcher | null): PluginTool[] {
|
|
21
|
+
return [
|
|
22
|
+
// ── Info Tools ──────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
name: 'ob_account',
|
|
26
|
+
description: 'View Hyperliquid account balance, equity, margin, and optionally open orders',
|
|
27
|
+
parameters: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
orders: { type: 'boolean', description: 'Include open orders in output' },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
async execute(_id, params) {
|
|
34
|
+
const { getClient } = await import('../core/client.js');
|
|
35
|
+
const client = getClient();
|
|
36
|
+
const state = await client.getUserState();
|
|
37
|
+
|
|
38
|
+
const result: Record<string, unknown> = {
|
|
39
|
+
address: client.address,
|
|
40
|
+
isApiWallet: client.isApiWallet,
|
|
41
|
+
equity: state.marginSummary.accountValue,
|
|
42
|
+
totalNtlPos: state.marginSummary.totalNtlPos,
|
|
43
|
+
totalMarginUsed: state.marginSummary.totalMarginUsed,
|
|
44
|
+
withdrawable: state.marginSummary.withdrawable,
|
|
45
|
+
positions: state.assetPositions
|
|
46
|
+
.filter(ap => parseFloat(ap.position.szi) !== 0)
|
|
47
|
+
.map(ap => ({
|
|
48
|
+
coin: ap.position.coin,
|
|
49
|
+
size: ap.position.szi,
|
|
50
|
+
entryPrice: ap.position.entryPx,
|
|
51
|
+
positionValue: ap.position.positionValue,
|
|
52
|
+
unrealizedPnl: ap.position.unrealizedPnl,
|
|
53
|
+
liquidationPx: ap.position.liquidationPx,
|
|
54
|
+
leverage: ap.position.leverage,
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (params.orders) {
|
|
59
|
+
const orders = await client.getOpenOrders();
|
|
60
|
+
result.openOrders = orders.map(o => ({
|
|
61
|
+
coin: o.coin,
|
|
62
|
+
oid: o.oid,
|
|
63
|
+
side: o.side,
|
|
64
|
+
size: o.sz,
|
|
65
|
+
price: o.limitPx,
|
|
66
|
+
orderType: o.orderType,
|
|
67
|
+
timestamp: o.timestamp,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return json(result);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
name: 'ob_positions',
|
|
77
|
+
description: 'View open positions with entry price, mark price, PnL, liquidation price, and leverage',
|
|
78
|
+
parameters: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
coin: { type: 'string', description: 'Filter by coin symbol (e.g. ETH, BTC)' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
async execute(_id, params) {
|
|
85
|
+
const { getClient } = await import('../core/client.js');
|
|
86
|
+
const client = getClient();
|
|
87
|
+
const state = await client.getUserState();
|
|
88
|
+
|
|
89
|
+
let positions = state.assetPositions
|
|
90
|
+
.filter(ap => parseFloat(ap.position.szi) !== 0)
|
|
91
|
+
.map(ap => ({
|
|
92
|
+
coin: ap.position.coin,
|
|
93
|
+
side: parseFloat(ap.position.szi) > 0 ? 'long' : 'short',
|
|
94
|
+
size: ap.position.szi,
|
|
95
|
+
entryPrice: ap.position.entryPx,
|
|
96
|
+
positionValue: ap.position.positionValue,
|
|
97
|
+
unrealizedPnl: ap.position.unrealizedPnl,
|
|
98
|
+
returnOnEquity: ap.position.returnOnEquity,
|
|
99
|
+
liquidationPx: ap.position.liquidationPx,
|
|
100
|
+
leverage: ap.position.leverage,
|
|
101
|
+
marginUsed: ap.position.marginUsed,
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
if (params.coin) {
|
|
105
|
+
const coin = (params.coin as string).toUpperCase();
|
|
106
|
+
positions = positions.filter(p => p.coin === coin);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return json({ address: client.address, positions });
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
name: 'ob_funding',
|
|
115
|
+
description: 'View funding rates for Hyperliquid perpetuals, sorted by annualized rate',
|
|
116
|
+
parameters: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
coin: { type: 'string', description: 'Filter by coin symbol' },
|
|
120
|
+
top: { type: 'number', description: 'Show top N results (default: 20)' },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
async execute(_id, params) {
|
|
124
|
+
const { getClient } = await import('../core/client.js');
|
|
125
|
+
const { annualizeFundingRate } = await import('../core/utils.js');
|
|
126
|
+
const client = getClient();
|
|
127
|
+
const raw = await client.getPredictedFundings();
|
|
128
|
+
|
|
129
|
+
// raw is Array<[coin, Array<[venue, { fundingRate, nextFundingTime }]>]>
|
|
130
|
+
let results = raw.map(([coin, venues]) => {
|
|
131
|
+
// Use the first venue's funding rate
|
|
132
|
+
const rate = venues.length > 0 ? parseFloat(venues[0][1].fundingRate) : 0;
|
|
133
|
+
return {
|
|
134
|
+
coin,
|
|
135
|
+
fundingRate: rate,
|
|
136
|
+
annualizedRate: annualizeFundingRate(rate),
|
|
137
|
+
venues: venues.map(([venue, data]) => ({ venue, fundingRate: data.fundingRate })),
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (params.coin) {
|
|
142
|
+
const coin = (params.coin as string).toUpperCase();
|
|
143
|
+
results = results.filter(r => r.coin === coin);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
results.sort((a, b) => Math.abs(b.annualizedRate) - Math.abs(a.annualizedRate));
|
|
147
|
+
|
|
148
|
+
const top = (params.top as number) || 20;
|
|
149
|
+
results = results.slice(0, top);
|
|
150
|
+
|
|
151
|
+
return json({ fundings: results });
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
{
|
|
156
|
+
name: 'ob_markets',
|
|
157
|
+
description: 'View market data for Hyperliquid perpetuals (price, volume, open interest)',
|
|
158
|
+
parameters: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
coin: { type: 'string', description: 'Filter by coin symbol' },
|
|
162
|
+
top: { type: 'number', description: 'Show top N results (default: 30)' },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
async execute(_id, params) {
|
|
166
|
+
const { getClient } = await import('../core/client.js');
|
|
167
|
+
const client = getClient();
|
|
168
|
+
const { meta, assetCtxs } = await client.getMetaAndAssetCtxs();
|
|
169
|
+
|
|
170
|
+
let markets = meta.universe.map((asset, i) => ({
|
|
171
|
+
coin: asset.name,
|
|
172
|
+
szDecimals: asset.szDecimals,
|
|
173
|
+
maxLeverage: asset.maxLeverage,
|
|
174
|
+
markPx: assetCtxs[i]?.markPx,
|
|
175
|
+
midPx: assetCtxs[i]?.midPx,
|
|
176
|
+
oraclePx: assetCtxs[i]?.oraclePx,
|
|
177
|
+
funding: assetCtxs[i]?.funding,
|
|
178
|
+
openInterest: assetCtxs[i]?.openInterest,
|
|
179
|
+
dayVolume: assetCtxs[i]?.dayNtlVlm,
|
|
180
|
+
prevDayPx: assetCtxs[i]?.prevDayPx,
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
if (params.coin) {
|
|
184
|
+
const coin = (params.coin as string).toUpperCase();
|
|
185
|
+
markets = markets.filter(m => m.coin === coin);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const top = (params.top as number) || 30;
|
|
189
|
+
markets = markets.slice(0, top);
|
|
190
|
+
|
|
191
|
+
return json({ markets });
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
{
|
|
196
|
+
name: 'ob_search',
|
|
197
|
+
description: 'Search for assets across all Hyperliquid market providers (perps, HIP-3, spot)',
|
|
198
|
+
parameters: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
properties: {
|
|
201
|
+
query: { type: 'string', description: 'Search query (e.g. GOLD, BTC, ETH)' },
|
|
202
|
+
type: { type: 'string', description: 'Filter by market type: perp, hip3, spot' },
|
|
203
|
+
},
|
|
204
|
+
required: ['query'],
|
|
205
|
+
},
|
|
206
|
+
async execute(_id, params) {
|
|
207
|
+
const { getClient } = await import('../core/client.js');
|
|
208
|
+
const client = getClient();
|
|
209
|
+
const query = (params.query as string).toUpperCase();
|
|
210
|
+
const typeFilter = params.type as string | undefined;
|
|
211
|
+
|
|
212
|
+
const results: Array<Record<string, unknown>> = [];
|
|
213
|
+
|
|
214
|
+
// Search main perps
|
|
215
|
+
if (!typeFilter || typeFilter === 'perp') {
|
|
216
|
+
const { meta, assetCtxs } = await client.getMetaAndAssetCtxs();
|
|
217
|
+
for (let i = 0; i < meta.universe.length; i++) {
|
|
218
|
+
const asset = meta.universe[i];
|
|
219
|
+
if (asset.name.toUpperCase().includes(query)) {
|
|
220
|
+
results.push({
|
|
221
|
+
coin: asset.name,
|
|
222
|
+
type: 'perp',
|
|
223
|
+
markPx: assetCtxs[i]?.markPx,
|
|
224
|
+
dayVolume: assetCtxs[i]?.dayNtlVlm,
|
|
225
|
+
maxLeverage: asset.maxLeverage,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Search spot
|
|
232
|
+
if (!typeFilter || typeFilter === 'spot') {
|
|
233
|
+
try {
|
|
234
|
+
const spotData = await client.getSpotMetaAndAssetCtxs();
|
|
235
|
+
for (let i = 0; i < spotData.meta.universe.length; i++) {
|
|
236
|
+
const pair = spotData.meta.universe[i];
|
|
237
|
+
if (pair.name.toUpperCase().includes(query)) {
|
|
238
|
+
results.push({
|
|
239
|
+
coin: pair.name,
|
|
240
|
+
type: 'spot',
|
|
241
|
+
markPx: spotData.assetCtxs[i]?.markPx,
|
|
242
|
+
dayVolume: spotData.assetCtxs[i]?.dayNtlVlm,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch { /* spot may not be available */ }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return json({ query, results });
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
{
|
|
254
|
+
name: 'ob_spot',
|
|
255
|
+
description: 'View spot markets, balances, and token info on Hyperliquid',
|
|
256
|
+
parameters: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
properties: {
|
|
259
|
+
coin: { type: 'string', description: 'Filter by coin symbol' },
|
|
260
|
+
balances: { type: 'boolean', description: 'Show your spot token balances' },
|
|
261
|
+
top: { type: 'number', description: 'Show top N results' },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
async execute(_id, params) {
|
|
265
|
+
const { getClient } = await import('../core/client.js');
|
|
266
|
+
const client = getClient();
|
|
267
|
+
|
|
268
|
+
if (params.balances) {
|
|
269
|
+
const balances = await client.getSpotBalances();
|
|
270
|
+
return json({ address: client.address, balances });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const spotData = await client.getSpotMetaAndAssetCtxs();
|
|
274
|
+
return json({ spotData });
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
// ── Trading Tools ───────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
{
|
|
281
|
+
name: 'ob_buy',
|
|
282
|
+
description: 'Quick market buy on Hyperliquid. Always use dry=true first to preview.',
|
|
283
|
+
parameters: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
coin: { type: 'string', description: 'Asset symbol (ETH, BTC, SOL, etc.)' },
|
|
287
|
+
size: { type: 'number', description: 'Order size in base asset' },
|
|
288
|
+
slippage: { type: 'number', description: 'Slippage tolerance in bps (default: 50)' },
|
|
289
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
290
|
+
},
|
|
291
|
+
required: ['coin', 'size'],
|
|
292
|
+
},
|
|
293
|
+
async execute(_id, params) {
|
|
294
|
+
const { getClient } = await import('../core/client.js');
|
|
295
|
+
const { roundSize, getSlippagePrice } = await import('../core/utils.js');
|
|
296
|
+
const client = getClient();
|
|
297
|
+
|
|
298
|
+
if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
|
|
299
|
+
|
|
300
|
+
const coin = (params.coin as string).toUpperCase();
|
|
301
|
+
const size = params.size as number;
|
|
302
|
+
const slippageBps = (params.slippage as number) ?? client.builderInfo.f;
|
|
303
|
+
|
|
304
|
+
const mids = await client.getAllMids();
|
|
305
|
+
const midPrice = parseFloat(mids[coin]);
|
|
306
|
+
if (!midPrice) return error(`Unknown coin: ${coin}`);
|
|
307
|
+
|
|
308
|
+
const szDecimals = await client.getSzDecimals(coin);
|
|
309
|
+
const roundedSize = roundSize(size, szDecimals);
|
|
310
|
+
const slippagePrice = getSlippagePrice(midPrice, true, slippageBps);
|
|
311
|
+
|
|
312
|
+
if (params.dry) {
|
|
313
|
+
return json({
|
|
314
|
+
dryRun: true,
|
|
315
|
+
action: 'buy',
|
|
316
|
+
coin,
|
|
317
|
+
size: roundedSize,
|
|
318
|
+
midPrice,
|
|
319
|
+
slippagePrice,
|
|
320
|
+
slippageBps,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const result = await client.marketOrder(coin, true, parseFloat(roundedSize), slippageBps);
|
|
325
|
+
return json({ action: 'buy', coin, size: roundedSize, result });
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
{
|
|
330
|
+
name: 'ob_sell',
|
|
331
|
+
description: 'Quick market sell on Hyperliquid. Always use dry=true first to preview.',
|
|
332
|
+
parameters: {
|
|
333
|
+
type: 'object',
|
|
334
|
+
properties: {
|
|
335
|
+
coin: { type: 'string', description: 'Asset symbol (ETH, BTC, SOL, etc.)' },
|
|
336
|
+
size: { type: 'number', description: 'Order size in base asset' },
|
|
337
|
+
slippage: { type: 'number', description: 'Slippage tolerance in bps (default: 50)' },
|
|
338
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
339
|
+
},
|
|
340
|
+
required: ['coin', 'size'],
|
|
341
|
+
},
|
|
342
|
+
async execute(_id, params) {
|
|
343
|
+
const { getClient } = await import('../core/client.js');
|
|
344
|
+
const { roundSize, getSlippagePrice } = await import('../core/utils.js');
|
|
345
|
+
const client = getClient();
|
|
346
|
+
|
|
347
|
+
if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
|
|
348
|
+
|
|
349
|
+
const coin = (params.coin as string).toUpperCase();
|
|
350
|
+
const size = params.size as number;
|
|
351
|
+
const slippageBps = (params.slippage as number) ?? client.builderInfo.f;
|
|
352
|
+
|
|
353
|
+
const mids = await client.getAllMids();
|
|
354
|
+
const midPrice = parseFloat(mids[coin]);
|
|
355
|
+
if (!midPrice) return error(`Unknown coin: ${coin}`);
|
|
356
|
+
|
|
357
|
+
const szDecimals = await client.getSzDecimals(coin);
|
|
358
|
+
const roundedSize = roundSize(size, szDecimals);
|
|
359
|
+
const slippagePrice = getSlippagePrice(midPrice, false, slippageBps);
|
|
360
|
+
|
|
361
|
+
if (params.dry) {
|
|
362
|
+
return json({
|
|
363
|
+
dryRun: true,
|
|
364
|
+
action: 'sell',
|
|
365
|
+
coin,
|
|
366
|
+
size: roundedSize,
|
|
367
|
+
midPrice,
|
|
368
|
+
slippagePrice,
|
|
369
|
+
slippageBps,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const result = await client.marketOrder(coin, false, parseFloat(roundedSize), slippageBps);
|
|
374
|
+
return json({ action: 'sell', coin, size: roundedSize, result });
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
{
|
|
379
|
+
name: 'ob_limit',
|
|
380
|
+
description: 'Place a limit order on Hyperliquid. Always use dry=true first to preview.',
|
|
381
|
+
parameters: {
|
|
382
|
+
type: 'object',
|
|
383
|
+
properties: {
|
|
384
|
+
coin: { type: 'string', description: 'Asset symbol' },
|
|
385
|
+
side: { type: 'string', enum: ['buy', 'sell'], description: 'Order direction' },
|
|
386
|
+
size: { type: 'number', description: 'Order size in base asset' },
|
|
387
|
+
price: { type: 'number', description: 'Limit price' },
|
|
388
|
+
tif: { type: 'string', enum: ['GTC', 'IOC', 'ALO'], description: 'Time in force (default: GTC)' },
|
|
389
|
+
reduce: { type: 'boolean', description: 'Reduce-only order' },
|
|
390
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
391
|
+
},
|
|
392
|
+
required: ['coin', 'side', 'size', 'price'],
|
|
393
|
+
},
|
|
394
|
+
async execute(_id, params) {
|
|
395
|
+
const { getClient } = await import('../core/client.js');
|
|
396
|
+
const { roundSize, roundPrice } = await import('../core/utils.js');
|
|
397
|
+
const client = getClient();
|
|
398
|
+
|
|
399
|
+
if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
|
|
400
|
+
|
|
401
|
+
const coin = (params.coin as string).toUpperCase();
|
|
402
|
+
const isBuy = params.side === 'buy';
|
|
403
|
+
const size = params.size as number;
|
|
404
|
+
const price = params.price as number;
|
|
405
|
+
const tif = ((params.tif as string) || 'GTC').toLowerCase();
|
|
406
|
+
const reduceOnly = (params.reduce as boolean) || false;
|
|
407
|
+
|
|
408
|
+
const szDecimals = await client.getSzDecimals(coin);
|
|
409
|
+
const roundedSize = roundSize(size, szDecimals);
|
|
410
|
+
const roundedPrice = roundPrice(price, szDecimals);
|
|
411
|
+
|
|
412
|
+
// Map tif string to SDK format
|
|
413
|
+
const tifMap: Record<string, 'Gtc' | 'Ioc' | 'Alo'> = { gtc: 'Gtc', ioc: 'Ioc', alo: 'Alo' };
|
|
414
|
+
const sdkTif = tifMap[tif] || 'Gtc';
|
|
415
|
+
|
|
416
|
+
if (params.dry) {
|
|
417
|
+
return json({
|
|
418
|
+
dryRun: true,
|
|
419
|
+
action: 'limit',
|
|
420
|
+
coin,
|
|
421
|
+
side: params.side,
|
|
422
|
+
size: roundedSize,
|
|
423
|
+
price: roundedPrice,
|
|
424
|
+
tif: sdkTif,
|
|
425
|
+
reduceOnly,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const result = await client.limitOrder(
|
|
430
|
+
coin, isBuy, parseFloat(roundedSize), parseFloat(roundedPrice), sdkTif, reduceOnly,
|
|
431
|
+
);
|
|
432
|
+
return json({ action: 'limit', coin, side: params.side, size: roundedSize, price: roundedPrice, result });
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
{
|
|
437
|
+
name: 'ob_trigger',
|
|
438
|
+
description: 'Place a trigger order (take profit or stop loss) on Hyperliquid. Always use dry=true first.',
|
|
439
|
+
parameters: {
|
|
440
|
+
type: 'object',
|
|
441
|
+
properties: {
|
|
442
|
+
coin: { type: 'string', description: 'Asset symbol' },
|
|
443
|
+
side: { type: 'string', enum: ['buy', 'sell'], description: 'Order direction' },
|
|
444
|
+
size: { type: 'number', description: 'Order size in base asset' },
|
|
445
|
+
trigger: { type: 'number', description: 'Trigger price' },
|
|
446
|
+
type: { type: 'string', enum: ['tp', 'sl'], description: 'Trigger type: tp (take profit) or sl (stop loss)' },
|
|
447
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
448
|
+
},
|
|
449
|
+
required: ['coin', 'side', 'size', 'trigger', 'type'],
|
|
450
|
+
},
|
|
451
|
+
async execute(_id, params) {
|
|
452
|
+
const { getClient } = await import('../core/client.js');
|
|
453
|
+
const { roundSize, roundPrice } = await import('../core/utils.js');
|
|
454
|
+
const client = getClient();
|
|
455
|
+
|
|
456
|
+
if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
|
|
457
|
+
|
|
458
|
+
const coin = (params.coin as string).toUpperCase();
|
|
459
|
+
const isBuy = params.side === 'buy';
|
|
460
|
+
const size = params.size as number;
|
|
461
|
+
const triggerPrice = params.trigger as number;
|
|
462
|
+
const tpsl = params.type as 'tp' | 'sl';
|
|
463
|
+
|
|
464
|
+
const szDecimals = await client.getSzDecimals(coin);
|
|
465
|
+
const roundedSize = roundSize(size, szDecimals);
|
|
466
|
+
const roundedTrigger = roundPrice(triggerPrice, szDecimals);
|
|
467
|
+
|
|
468
|
+
if (params.dry) {
|
|
469
|
+
return json({
|
|
470
|
+
dryRun: true,
|
|
471
|
+
action: 'trigger',
|
|
472
|
+
coin,
|
|
473
|
+
side: params.side,
|
|
474
|
+
size: roundedSize,
|
|
475
|
+
triggerPrice: roundedTrigger,
|
|
476
|
+
type: tpsl,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let result;
|
|
481
|
+
if (tpsl === 'sl') {
|
|
482
|
+
result = await client.stopLoss(coin, isBuy, parseFloat(roundedSize), parseFloat(roundedTrigger));
|
|
483
|
+
} else {
|
|
484
|
+
result = await client.takeProfit(coin, isBuy, parseFloat(roundedSize), parseFloat(roundedTrigger));
|
|
485
|
+
}
|
|
486
|
+
return json({ action: 'trigger', coin, type: tpsl, size: roundedSize, triggerPrice: roundedTrigger, result });
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
{
|
|
491
|
+
name: 'ob_tpsl',
|
|
492
|
+
description: 'Set take profit and/or stop loss on an existing position. Supports absolute price, percentage (+10%, -5%), and "entry" keyword.',
|
|
493
|
+
parameters: {
|
|
494
|
+
type: 'object',
|
|
495
|
+
properties: {
|
|
496
|
+
coin: { type: 'string', description: 'Asset symbol' },
|
|
497
|
+
tp: { type: 'string', description: 'Take profit price (absolute, +10%, or "entry")' },
|
|
498
|
+
sl: { type: 'string', description: 'Stop loss price (absolute, -5%, or "entry")' },
|
|
499
|
+
size: { type: 'number', description: 'Position size (defaults to full position)' },
|
|
500
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
501
|
+
},
|
|
502
|
+
required: ['coin'],
|
|
503
|
+
},
|
|
504
|
+
async execute(_id, params) {
|
|
505
|
+
const { getClient } = await import('../core/client.js');
|
|
506
|
+
const { roundSize, roundPrice } = await import('../core/utils.js');
|
|
507
|
+
const client = getClient();
|
|
508
|
+
|
|
509
|
+
if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
|
|
510
|
+
if (!params.tp && !params.sl) return error('At least one of tp or sl is required.');
|
|
511
|
+
|
|
512
|
+
const coin = (params.coin as string).toUpperCase();
|
|
513
|
+
|
|
514
|
+
// Get current position
|
|
515
|
+
const state = await client.getUserState();
|
|
516
|
+
const position = state.assetPositions.find(
|
|
517
|
+
ap => ap.position.coin === coin && parseFloat(ap.position.szi) !== 0,
|
|
518
|
+
);
|
|
519
|
+
if (!position) return error(`No open position for ${coin}`);
|
|
520
|
+
|
|
521
|
+
const posSize = parseFloat(position.position.szi);
|
|
522
|
+
const entryPx = parseFloat(position.position.entryPx);
|
|
523
|
+
const isLong = posSize > 0;
|
|
524
|
+
const szDecimals = await client.getSzDecimals(coin);
|
|
525
|
+
const orderSize = params.size ? parseFloat(roundSize(params.size as number, szDecimals)) : Math.abs(posSize);
|
|
526
|
+
|
|
527
|
+
// Parse price helper
|
|
528
|
+
const parsePrice = (input: string): number => {
|
|
529
|
+
if (input.toLowerCase() === 'entry') return entryPx;
|
|
530
|
+
if (input.endsWith('%')) {
|
|
531
|
+
const pct = parseFloat(input.replace('%', ''));
|
|
532
|
+
return entryPx * (1 + pct / 100);
|
|
533
|
+
}
|
|
534
|
+
return parseFloat(input);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const results: Record<string, unknown> = { coin, positionSize: posSize, entryPrice: entryPx };
|
|
538
|
+
|
|
539
|
+
if (params.tp) {
|
|
540
|
+
const tpPrice = parsePrice(params.tp as string);
|
|
541
|
+
const roundedTp = parseFloat(roundPrice(tpPrice, szDecimals));
|
|
542
|
+
results.tpPrice = roundedTp;
|
|
543
|
+
|
|
544
|
+
if (!params.dry) {
|
|
545
|
+
// TP closes position: long → sell, short → buy
|
|
546
|
+
results.tpResult = await client.takeProfit(coin, !isLong, orderSize, roundedTp);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (params.sl) {
|
|
551
|
+
const slPrice = parsePrice(params.sl as string);
|
|
552
|
+
const roundedSl = parseFloat(roundPrice(slPrice, szDecimals));
|
|
553
|
+
results.slPrice = roundedSl;
|
|
554
|
+
|
|
555
|
+
if (!params.dry) {
|
|
556
|
+
results.slResult = await client.stopLoss(coin, !isLong, orderSize, roundedSl);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (params.dry) results.dryRun = true;
|
|
561
|
+
|
|
562
|
+
return json(results);
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
{
|
|
567
|
+
name: 'ob_cancel',
|
|
568
|
+
description: 'Cancel open orders on Hyperliquid',
|
|
569
|
+
parameters: {
|
|
570
|
+
type: 'object',
|
|
571
|
+
properties: {
|
|
572
|
+
coin: { type: 'string', description: 'Cancel orders for this coin only' },
|
|
573
|
+
oid: { type: 'number', description: 'Cancel specific order by ID' },
|
|
574
|
+
all: { type: 'boolean', description: 'Cancel all open orders' },
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
async execute(_id, params) {
|
|
578
|
+
const { getClient } = await import('../core/client.js');
|
|
579
|
+
const client = getClient();
|
|
580
|
+
|
|
581
|
+
if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
|
|
582
|
+
|
|
583
|
+
if (params.oid) {
|
|
584
|
+
const coin = params.coin as string | undefined;
|
|
585
|
+
if (!coin) return error('--coin is required when cancelling by order ID');
|
|
586
|
+
const result = await client.cancel(coin.toUpperCase(), params.oid as number);
|
|
587
|
+
return json({ action: 'cancel', coin: coin.toUpperCase(), oid: params.oid, result });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (params.all || params.coin) {
|
|
591
|
+
const coin = params.coin ? (params.coin as string).toUpperCase() : undefined;
|
|
592
|
+
const results = await client.cancelAll(coin);
|
|
593
|
+
return json({ action: 'cancelAll', coin: coin ?? 'all', results });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return error('Specify --all, --coin, or --oid to cancel orders.');
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
// ── Advanced Execution (shell out — these are long-running scripts) ──
|
|
601
|
+
|
|
602
|
+
{
|
|
603
|
+
name: 'ob_twap',
|
|
604
|
+
description: 'Execute a TWAP (time-weighted average price) order, splitting a large order into smaller slices over time. This is a long-running command.',
|
|
605
|
+
parameters: {
|
|
606
|
+
type: 'object',
|
|
607
|
+
properties: {
|
|
608
|
+
coin: { type: 'string', description: 'Asset symbol' },
|
|
609
|
+
side: { type: 'string', enum: ['buy', 'sell'], description: 'Order direction' },
|
|
610
|
+
size: { type: 'number', description: 'Total order size' },
|
|
611
|
+
duration: { type: 'number', description: 'Duration in seconds' },
|
|
612
|
+
intervals: { type: 'number', description: 'Number of slices' },
|
|
613
|
+
randomize: { type: 'number', description: 'Randomize timing by this % (0-50)' },
|
|
614
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
615
|
+
},
|
|
616
|
+
required: ['coin', 'side', 'size', 'duration'],
|
|
617
|
+
},
|
|
618
|
+
async execute(_id, params) {
|
|
619
|
+
const { execFile } = await import('node:child_process');
|
|
620
|
+
const args = ['twap'];
|
|
621
|
+
for (const [key, value] of Object.entries(params)) {
|
|
622
|
+
if (value === undefined || value === null || value === false || value === '') continue;
|
|
623
|
+
if (value === true) args.push(`--${key}`);
|
|
624
|
+
else args.push(`--${key}`, String(value));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return new Promise((resolve) => {
|
|
628
|
+
execFile('openbroker', args, { timeout: 600_000 }, (_err, stdout, stderr) => {
|
|
629
|
+
resolve({ content: [{ type: 'text' as const, text: (stdout + (stderr || '')).trim() }] });
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
|
|
635
|
+
{
|
|
636
|
+
name: 'ob_bracket',
|
|
637
|
+
description: 'Place a bracket order: entry + take profit + stop loss in one command',
|
|
638
|
+
parameters: {
|
|
639
|
+
type: 'object',
|
|
640
|
+
properties: {
|
|
641
|
+
coin: { type: 'string', description: 'Asset symbol' },
|
|
642
|
+
side: { type: 'string', enum: ['buy', 'sell'], description: 'Order direction' },
|
|
643
|
+
size: { type: 'number', description: 'Order size' },
|
|
644
|
+
tp: { type: 'number', description: 'Take profit percentage from entry' },
|
|
645
|
+
sl: { type: 'number', description: 'Stop loss percentage from entry' },
|
|
646
|
+
entry: { type: 'string', enum: ['market', 'limit'], description: 'Entry type (default: market)' },
|
|
647
|
+
price: { type: 'number', description: 'Entry price (required if entry=limit)' },
|
|
648
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
649
|
+
},
|
|
650
|
+
required: ['coin', 'side', 'size', 'tp', 'sl'],
|
|
651
|
+
},
|
|
652
|
+
async execute(_id, params) {
|
|
653
|
+
const { execFile } = await import('node:child_process');
|
|
654
|
+
const args = ['bracket'];
|
|
655
|
+
for (const [key, value] of Object.entries(params)) {
|
|
656
|
+
if (value === undefined || value === null || value === false || value === '') continue;
|
|
657
|
+
if (value === true) args.push(`--${key}`);
|
|
658
|
+
else args.push(`--${key}`, String(value));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return new Promise((resolve) => {
|
|
662
|
+
execFile('openbroker', args, { timeout: 60_000 }, (_err, stdout, stderr) => {
|
|
663
|
+
resolve({ content: [{ type: 'text' as const, text: (stdout + (stderr || '')).trim() }] });
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
{
|
|
670
|
+
name: 'ob_chase',
|
|
671
|
+
description: 'Chase price with ALO (post-only) orders, continuously replacing until filled. This is a long-running command.',
|
|
672
|
+
parameters: {
|
|
673
|
+
type: 'object',
|
|
674
|
+
properties: {
|
|
675
|
+
coin: { type: 'string', description: 'Asset symbol' },
|
|
676
|
+
side: { type: 'string', enum: ['buy', 'sell'], description: 'Order direction' },
|
|
677
|
+
size: { type: 'number', description: 'Order size' },
|
|
678
|
+
offset: { type: 'number', description: 'Tick offset from best price (default: 1)' },
|
|
679
|
+
timeout: { type: 'number', description: 'Timeout in seconds' },
|
|
680
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
681
|
+
},
|
|
682
|
+
required: ['coin', 'side', 'size'],
|
|
683
|
+
},
|
|
684
|
+
async execute(_id, params) {
|
|
685
|
+
const { execFile } = await import('node:child_process');
|
|
686
|
+
const args = ['chase'];
|
|
687
|
+
for (const [key, value] of Object.entries(params)) {
|
|
688
|
+
if (value === undefined || value === null || value === false || value === '') continue;
|
|
689
|
+
if (value === true) args.push(`--${key}`);
|
|
690
|
+
else args.push(`--${key}`, String(value));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return new Promise((resolve) => {
|
|
694
|
+
execFile('openbroker', args, { timeout: 600_000 }, (_err, stdout, stderr) => {
|
|
695
|
+
resolve({ content: [{ type: 'text' as const, text: (stdout + (stderr || '')).trim() }] });
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
|
|
701
|
+
// ── Watcher Tool ────────────────────────────────────────────
|
|
702
|
+
|
|
703
|
+
{
|
|
704
|
+
name: 'ob_watcher_status',
|
|
705
|
+
description: 'Get the status of the background position watcher: tracked positions, margin usage, events detected',
|
|
706
|
+
parameters: { type: 'object', properties: {} },
|
|
707
|
+
async execute() {
|
|
708
|
+
if (!watcher) {
|
|
709
|
+
return json({ running: false, error: 'Watcher is not enabled' });
|
|
710
|
+
}
|
|
711
|
+
return json(watcher.getStatus());
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
}
|