openbroker 1.0.33
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 +94 -0
- package/README.md +160 -0
- package/SKILL.md +296 -0
- package/bin/cli.ts +170 -0
- package/bin/openbroker.js +24 -0
- package/config/example.env +48 -0
- package/package.json +79 -0
- package/scripts/core/client.ts +844 -0
- package/scripts/core/config.ts +92 -0
- package/scripts/core/types.ts +192 -0
- package/scripts/core/utils.ts +156 -0
- package/scripts/info/account.ts +117 -0
- package/scripts/info/all-markets.ts +223 -0
- package/scripts/info/funding.ts +133 -0
- package/scripts/info/markets.ts +151 -0
- package/scripts/info/positions.ts +88 -0
- package/scripts/info/search-markets.ts +230 -0
- package/scripts/info/spot.ts +192 -0
- package/scripts/operations/bracket.ts +285 -0
- package/scripts/operations/cancel.ts +124 -0
- package/scripts/operations/chase.ts +236 -0
- package/scripts/operations/limit-order.ts +160 -0
- package/scripts/operations/market-order.ts +167 -0
- package/scripts/operations/scale.ts +263 -0
- package/scripts/operations/set-tpsl.ts +302 -0
- package/scripts/operations/trigger-order.ts +201 -0
- package/scripts/operations/twap.ts +222 -0
- package/scripts/setup/approve-builder.ts +178 -0
- package/scripts/setup/onboard.ts +242 -0
- package/scripts/strategies/dca.ts +292 -0
- package/scripts/strategies/funding-arb.ts +319 -0
- package/scripts/strategies/grid.ts +397 -0
- package/scripts/strategies/mm-maker.ts +411 -0
- package/scripts/strategies/mm-spread.ts +402 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Cancel orders on Hyperliquid
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, parseArgs } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Open Broker - Cancel Orders
|
|
10
|
+
===========================
|
|
11
|
+
|
|
12
|
+
Cancel open orders.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
npx tsx scripts/operations/cancel.ts [--coin <COIN>] [--oid <ORDER_ID>] [--all]
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--coin Cancel orders for specific coin only
|
|
19
|
+
--oid Cancel specific order by ID
|
|
20
|
+
--all Cancel all open orders
|
|
21
|
+
--dry Dry run - show what would be cancelled
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
npx tsx scripts/operations/cancel.ts --all # Cancel all orders
|
|
25
|
+
npx tsx scripts/operations/cancel.ts --coin ETH # Cancel all ETH orders
|
|
26
|
+
npx tsx scripts/operations/cancel.ts --coin ETH --oid 123456 # Cancel specific order
|
|
27
|
+
npx tsx scripts/operations/cancel.ts --all --dry # Show all orders (dry run)
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
const args = parseArgs(process.argv.slice(2));
|
|
33
|
+
|
|
34
|
+
const coin = args.coin as string | undefined;
|
|
35
|
+
const oid = args.oid ? parseInt(args.oid as string) : undefined;
|
|
36
|
+
const all = args.all as boolean;
|
|
37
|
+
const dryRun = args.dry as boolean;
|
|
38
|
+
|
|
39
|
+
// Must specify something to cancel
|
|
40
|
+
if (!coin && !oid && !all) {
|
|
41
|
+
printUsage();
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const client = getClient();
|
|
46
|
+
|
|
47
|
+
console.log('Open Broker - Cancel Orders');
|
|
48
|
+
console.log('===========================\n');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Get open orders
|
|
52
|
+
const orders = await client.getOpenOrders();
|
|
53
|
+
|
|
54
|
+
// Filter orders based on arguments
|
|
55
|
+
let targetOrders = orders;
|
|
56
|
+
if (coin) {
|
|
57
|
+
targetOrders = orders.filter(o => o.coin === coin);
|
|
58
|
+
}
|
|
59
|
+
if (oid) {
|
|
60
|
+
targetOrders = orders.filter(o => o.oid === oid);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (targetOrders.length === 0) {
|
|
64
|
+
if (oid) {
|
|
65
|
+
console.log(`No order found with ID ${oid}`);
|
|
66
|
+
} else if (coin) {
|
|
67
|
+
console.log(`No open orders for ${coin}`);
|
|
68
|
+
} else {
|
|
69
|
+
console.log('No open orders to cancel');
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Display orders to be cancelled
|
|
75
|
+
console.log('Orders to Cancel');
|
|
76
|
+
console.log('----------------');
|
|
77
|
+
console.log('Coin | Side | Size | Price | Order ID');
|
|
78
|
+
console.log('---------|------|------------|------------|----------');
|
|
79
|
+
|
|
80
|
+
for (const order of targetOrders) {
|
|
81
|
+
const side = order.side === 'B' ? 'BUY ' : 'SELL';
|
|
82
|
+
console.log(
|
|
83
|
+
`${order.coin.padEnd(8)} | ${side} | ${parseFloat(order.sz).toFixed(4).padStart(10)} | ` +
|
|
84
|
+
`${formatUsd(parseFloat(order.limitPx)).padStart(10)} | ${order.oid}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`\nTotal: ${targetOrders.length} order(s)`);
|
|
89
|
+
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
console.log('\nš Dry run - orders not cancelled');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('\nCancelling...');
|
|
96
|
+
|
|
97
|
+
let successCount = 0;
|
|
98
|
+
let failCount = 0;
|
|
99
|
+
|
|
100
|
+
for (const order of targetOrders) {
|
|
101
|
+
try {
|
|
102
|
+
const response = await client.cancel(order.coin, order.oid);
|
|
103
|
+
if (response.status === 'ok') {
|
|
104
|
+
console.log(`ā
Cancelled ${order.coin} order ${order.oid}`);
|
|
105
|
+
successCount++;
|
|
106
|
+
} else {
|
|
107
|
+
console.log(`ā Failed to cancel ${order.coin} order ${order.oid}`);
|
|
108
|
+
failCount++;
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.log(`ā Error cancelling ${order.coin} order ${order.oid}: ${err}`);
|
|
112
|
+
failCount++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`\nResult: ${successCount} cancelled, ${failCount} failed`);
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('Error:', error);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main();
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Chase Order - Follow price with limit orders until filled
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, parseArgs, sleep } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Open Broker - Chase Order
|
|
10
|
+
=========================
|
|
11
|
+
|
|
12
|
+
Place a limit order that chases the price until filled.
|
|
13
|
+
Keeps adjusting the order to stay near the best price while avoiding taker fees.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
npx tsx scripts/operations/chase.ts --coin <COIN> --side <buy|sell> --size <SIZE>
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--coin Asset to trade (e.g., ETH, BTC)
|
|
20
|
+
--side Order side: buy or sell
|
|
21
|
+
--size Order size in base asset
|
|
22
|
+
--offset Offset from mid price in bps (default: 5 = 0.05%)
|
|
23
|
+
--timeout Max time to chase in seconds (default: 300 = 5 min)
|
|
24
|
+
--interval Update interval in ms (default: 2000)
|
|
25
|
+
--max-chase Max price to chase to in bps from start (default: 100 = 1%)
|
|
26
|
+
--reduce Reduce-only order
|
|
27
|
+
--dry Dry run - show chase parameters without executing
|
|
28
|
+
|
|
29
|
+
Strategy:
|
|
30
|
+
- Places ALO (post-only) order at mid ± offset
|
|
31
|
+
- If not filled, cancels and replaces closer to mid
|
|
32
|
+
- Stops when filled or timeout/max-chase reached
|
|
33
|
+
- Uses ALO to ensure maker rebates
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
# Chase buy 0.5 ETH with 5 bps offset, 5 min timeout
|
|
37
|
+
npx tsx scripts/operations/chase.ts --coin ETH --side buy --size 0.5
|
|
38
|
+
|
|
39
|
+
# Chase sell with tighter offset and longer timeout
|
|
40
|
+
npx tsx scripts/operations/chase.ts --coin BTC --side sell --size 0.1 --offset 2 --timeout 600
|
|
41
|
+
|
|
42
|
+
# Quick aggressive chase (1 bps offset, 1 min timeout, 50 bps max chase)
|
|
43
|
+
npx tsx scripts/operations/chase.ts --coin SOL --side buy --size 10 --offset 1 --timeout 60 --max-chase 50
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const args = parseArgs(process.argv.slice(2));
|
|
49
|
+
|
|
50
|
+
const coin = args.coin as string;
|
|
51
|
+
const side = args.side as string;
|
|
52
|
+
const size = parseFloat(args.size as string);
|
|
53
|
+
const offsetBps = args.offset ? parseInt(args.offset as string) : 5;
|
|
54
|
+
const timeoutSec = args.timeout ? parseInt(args.timeout as string) : 300;
|
|
55
|
+
const intervalMs = args.interval ? parseInt(args.interval as string) : 2000;
|
|
56
|
+
const maxChaseBps = args['max-chase'] ? parseInt(args['max-chase'] as string) : 100;
|
|
57
|
+
const reduceOnly = args.reduce as boolean;
|
|
58
|
+
const dryRun = args.dry as boolean;
|
|
59
|
+
|
|
60
|
+
if (!coin || !side || isNaN(size)) {
|
|
61
|
+
printUsage();
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (side !== 'buy' && side !== 'sell') {
|
|
66
|
+
console.error('Error: --side must be "buy" or "sell"');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isBuy = side === 'buy';
|
|
71
|
+
const client = getClient();
|
|
72
|
+
|
|
73
|
+
if (args.verbose) {
|
|
74
|
+
client.verbose = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log('Open Broker - Chase Order');
|
|
78
|
+
console.log('=========================\n');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const mids = await client.getAllMids();
|
|
82
|
+
const startMid = parseFloat(mids[coin]);
|
|
83
|
+
if (!startMid) {
|
|
84
|
+
console.error(`Error: No market data for ${coin}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const maxChasePrice = isBuy
|
|
89
|
+
? startMid * (1 + maxChaseBps / 10000)
|
|
90
|
+
: startMid * (1 - maxChaseBps / 10000);
|
|
91
|
+
|
|
92
|
+
console.log('Chase Parameters');
|
|
93
|
+
console.log('----------------');
|
|
94
|
+
console.log(`Coin: ${coin}`);
|
|
95
|
+
console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
|
|
96
|
+
console.log(`Size: ${size}`);
|
|
97
|
+
console.log(`Start Mid: ${formatUsd(startMid)}`);
|
|
98
|
+
console.log(`Offset: ${offsetBps} bps (${(offsetBps / 100).toFixed(2)}%)`);
|
|
99
|
+
console.log(`Max Chase: ${maxChaseBps} bps to ${formatUsd(maxChasePrice)}`);
|
|
100
|
+
console.log(`Timeout: ${timeoutSec}s`);
|
|
101
|
+
console.log(`Update Rate: ${intervalMs}ms`);
|
|
102
|
+
console.log(`Order Type: ALO (post-only)`);
|
|
103
|
+
|
|
104
|
+
if (dryRun) {
|
|
105
|
+
console.log('\nš Dry run - chase not started');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log('\nChasing...\n');
|
|
110
|
+
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
let currentOid: number | null = null;
|
|
113
|
+
let lastPrice: number | null = null;
|
|
114
|
+
let iteration = 0;
|
|
115
|
+
let filled = false;
|
|
116
|
+
|
|
117
|
+
while (Date.now() - startTime < timeoutSec * 1000) {
|
|
118
|
+
iteration++;
|
|
119
|
+
|
|
120
|
+
// Get current mid
|
|
121
|
+
const currentMids = await client.getAllMids();
|
|
122
|
+
const currentMid = parseFloat(currentMids[coin]);
|
|
123
|
+
|
|
124
|
+
// Check max chase limit
|
|
125
|
+
if (isBuy && currentMid > maxChasePrice) {
|
|
126
|
+
console.log(`\nā ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (!isBuy && currentMid < maxChasePrice) {
|
|
130
|
+
console.log(`\nā ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Calculate order price with offset
|
|
135
|
+
const orderPrice = isBuy
|
|
136
|
+
? currentMid * (1 - offsetBps / 10000)
|
|
137
|
+
: currentMid * (1 + offsetBps / 10000);
|
|
138
|
+
|
|
139
|
+
// Check if price moved enough to update
|
|
140
|
+
const priceChanged = !lastPrice || Math.abs(orderPrice - lastPrice) / lastPrice > 0.0001;
|
|
141
|
+
|
|
142
|
+
if (priceChanged) {
|
|
143
|
+
// Cancel existing order if any
|
|
144
|
+
if (currentOid !== null) {
|
|
145
|
+
try {
|
|
146
|
+
await client.cancel(coin, currentOid);
|
|
147
|
+
} catch {
|
|
148
|
+
// Order might have filled
|
|
149
|
+
}
|
|
150
|
+
currentOid = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if we got filled while cancelling
|
|
154
|
+
const orders = await client.getOpenOrders();
|
|
155
|
+
const ourOrder = orders.find(o => o.coin === coin && o.oid === currentOid);
|
|
156
|
+
if (!ourOrder && currentOid !== null) {
|
|
157
|
+
// Order gone - might have filled
|
|
158
|
+
// Check position to confirm
|
|
159
|
+
const state = await client.getUserState();
|
|
160
|
+
const pos = state.assetPositions.find(p => p.position.coin === coin);
|
|
161
|
+
if (pos && Math.abs(parseFloat(pos.position.szi)) >= size * 0.99) {
|
|
162
|
+
filled = true;
|
|
163
|
+
console.log(`\nā
Order filled!`);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Place new order
|
|
169
|
+
process.stdout.write(`[${iteration}] Mid: ${formatUsd(currentMid)} ā Order: ${formatUsd(orderPrice)}... `);
|
|
170
|
+
|
|
171
|
+
const response = await client.limitOrder(coin, isBuy, size, orderPrice, 'Alo', reduceOnly);
|
|
172
|
+
|
|
173
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
174
|
+
const status = response.response.data.statuses[0];
|
|
175
|
+
if (status?.resting) {
|
|
176
|
+
currentOid = status.resting.oid;
|
|
177
|
+
lastPrice = orderPrice;
|
|
178
|
+
console.log(`OID: ${currentOid}`);
|
|
179
|
+
} else if (status?.filled) {
|
|
180
|
+
filled = true;
|
|
181
|
+
console.log(`ā
Filled @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
|
|
182
|
+
break;
|
|
183
|
+
} else if (status?.error) {
|
|
184
|
+
console.log(`ā ${status.error}`);
|
|
185
|
+
// If ALO rejected (would be taker), try again next iteration
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
console.log(`ā Failed`);
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// Price stable, check if filled
|
|
192
|
+
if (currentOid !== null) {
|
|
193
|
+
const orders = await client.getOpenOrders();
|
|
194
|
+
const ourOrder = orders.find(o => o.oid === currentOid);
|
|
195
|
+
if (!ourOrder) {
|
|
196
|
+
filled = true;
|
|
197
|
+
console.log(`\nā
Order filled!`);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
process.stdout.write('.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await sleep(intervalMs);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Cleanup
|
|
208
|
+
if (currentOid !== null && !filled) {
|
|
209
|
+
console.log(`\nCancelling unfilled order...`);
|
|
210
|
+
try {
|
|
211
|
+
await client.cancel(coin, currentOid);
|
|
212
|
+
console.log(`ā
Cancelled`);
|
|
213
|
+
} catch {
|
|
214
|
+
console.log(`ā ļø Could not cancel (may have filled)`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Summary
|
|
219
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
220
|
+
const endMid = parseFloat((await client.getAllMids())[coin]);
|
|
221
|
+
const priceMove = ((endMid - startMid) / startMid) * 10000;
|
|
222
|
+
|
|
223
|
+
console.log('\n========== Chase Summary ==========');
|
|
224
|
+
console.log(`Duration: ${elapsed.toFixed(1)}s`);
|
|
225
|
+
console.log(`Iterations: ${iteration}`);
|
|
226
|
+
console.log(`Start Mid: ${formatUsd(startMid)}`);
|
|
227
|
+
console.log(`End Mid: ${formatUsd(endMid)} (${priceMove >= 0 ? '+' : ''}${priceMove.toFixed(1)} bps)`);
|
|
228
|
+
console.log(`Result: ${filled ? 'ā
Filled' : 'ā Not filled'}`);
|
|
229
|
+
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('Error:', error);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
main();
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Execute a limit order on Hyperliquid
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, parseArgs } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Open Broker - Limit Order
|
|
10
|
+
=========================
|
|
11
|
+
|
|
12
|
+
Place a limit order with specified price.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
npx tsx scripts/operations/limit-order.ts --coin <COIN> --side <buy|sell> --size <SIZE> --price <PRICE>
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--coin Asset to trade (e.g., ETH, BTC)
|
|
19
|
+
--side Order side: buy or sell
|
|
20
|
+
--size Order size in base asset
|
|
21
|
+
--price Limit price
|
|
22
|
+
--tif Time in force: GTC, IOC, or ALO (default: GTC)
|
|
23
|
+
GTC = Good Till Cancel (rests on book)
|
|
24
|
+
IOC = Immediate Or Cancel (fills or cancels)
|
|
25
|
+
ALO = Add Liquidity Only (post-only, maker only)
|
|
26
|
+
--reduce Reduce-only order (default: false)
|
|
27
|
+
--dry Dry run - show order details without executing
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
npx tsx scripts/operations/limit-order.ts --coin ETH --side buy --size 1 --price 3000
|
|
31
|
+
npx tsx scripts/operations/limit-order.ts --coin BTC --side sell --size 0.1 --price 100000 --tif ALO
|
|
32
|
+
npx tsx scripts/operations/limit-order.ts --coin SOL --side buy --size 10 --price 150 --reduce
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
const args = parseArgs(process.argv.slice(2));
|
|
38
|
+
|
|
39
|
+
// Parse and validate arguments
|
|
40
|
+
const coin = args.coin as string;
|
|
41
|
+
const side = args.side as string;
|
|
42
|
+
const size = parseFloat(args.size as string);
|
|
43
|
+
const price = parseFloat(args.price as string);
|
|
44
|
+
const tifArg = (args.tif as string)?.toUpperCase() || 'GTC';
|
|
45
|
+
const reduceOnly = args.reduce as boolean;
|
|
46
|
+
const dryRun = args.dry as boolean;
|
|
47
|
+
|
|
48
|
+
if (!coin || !side || isNaN(size) || isNaN(price)) {
|
|
49
|
+
printUsage();
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (side !== 'buy' && side !== 'sell') {
|
|
54
|
+
console.error('Error: --side must be "buy" or "sell"');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (size <= 0) {
|
|
59
|
+
console.error('Error: --size must be positive');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (price <= 0) {
|
|
64
|
+
console.error('Error: --price must be positive');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Map uppercase CLI input to Pascal case for SDK
|
|
69
|
+
const tifMap: Record<string, 'Gtc' | 'Ioc' | 'Alo'> = {
|
|
70
|
+
'GTC': 'Gtc',
|
|
71
|
+
'IOC': 'Ioc',
|
|
72
|
+
'ALO': 'Alo'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const tif = tifMap[tifArg];
|
|
76
|
+
if (!tif) {
|
|
77
|
+
console.error(`Error: --tif must be one of: GTC, IOC, ALO`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
const isBuy = side === 'buy';
|
|
81
|
+
const client = getClient();
|
|
82
|
+
|
|
83
|
+
console.log('Open Broker - Limit Order');
|
|
84
|
+
console.log('=========================\n');
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Get current price for reference
|
|
88
|
+
const mids = await client.getAllMids();
|
|
89
|
+
const midPrice = parseFloat(mids[coin]);
|
|
90
|
+
|
|
91
|
+
if (!midPrice) {
|
|
92
|
+
console.error(`Error: No market data for ${coin}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const notional = price * size;
|
|
97
|
+
const distanceFromMid = ((price - midPrice) / midPrice) * 100;
|
|
98
|
+
|
|
99
|
+
console.log('Order Details');
|
|
100
|
+
console.log('-------------');
|
|
101
|
+
console.log(`Coin: ${coin}`);
|
|
102
|
+
console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
|
|
103
|
+
console.log(`Size: ${size}`);
|
|
104
|
+
console.log(`Limit Price: ${formatUsd(price)}`);
|
|
105
|
+
console.log(`Current Mid: ${formatUsd(midPrice)}`);
|
|
106
|
+
console.log(`Distance: ${distanceFromMid >= 0 ? '+' : ''}${distanceFromMid.toFixed(2)}% from mid`);
|
|
107
|
+
console.log(`Notional: ${formatUsd(notional)}`);
|
|
108
|
+
console.log(`Time in Force: ${tif}`);
|
|
109
|
+
console.log(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
|
|
110
|
+
console.log(`Builder Fee: ${client.builderInfo.f / 10} bps`);
|
|
111
|
+
|
|
112
|
+
// Warning if order would be aggressively priced
|
|
113
|
+
if ((isBuy && price > midPrice) || (!isBuy && price < midPrice)) {
|
|
114
|
+
console.log(`\nā ļø Order is priced aggressively - may fill immediately as taker`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
console.log('\nš Dry run - order not submitted');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log('\nSubmitting...');
|
|
123
|
+
|
|
124
|
+
const response = await client.limitOrder(coin, isBuy, size, price, tif, reduceOnly);
|
|
125
|
+
|
|
126
|
+
console.log('\nResult');
|
|
127
|
+
console.log('------');
|
|
128
|
+
|
|
129
|
+
if (response.status === 'ok' && response.response) {
|
|
130
|
+
const statuses = response.response.data.statuses;
|
|
131
|
+
for (const status of statuses) {
|
|
132
|
+
if (status.filled) {
|
|
133
|
+
const fillSz = parseFloat(status.filled.totalSz);
|
|
134
|
+
const avgPx = parseFloat(status.filled.avgPx);
|
|
135
|
+
const fillNotional = fillSz * avgPx;
|
|
136
|
+
|
|
137
|
+
console.log(`ā
Filled`);
|
|
138
|
+
console.log(` Order ID: ${status.filled.oid}`);
|
|
139
|
+
console.log(` Size: ${fillSz}`);
|
|
140
|
+
console.log(` Avg Price: ${formatUsd(avgPx)}`);
|
|
141
|
+
console.log(` Notional: ${formatUsd(fillNotional)}`);
|
|
142
|
+
} else if (status.resting) {
|
|
143
|
+
console.log(`ā
Order placed`);
|
|
144
|
+
console.log(` Order ID: ${status.resting.oid}`);
|
|
145
|
+
console.log(` Status: Resting on book`);
|
|
146
|
+
} else if (status.error) {
|
|
147
|
+
console.log(`ā Error: ${status.error}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
console.log(`ā Error: ${response.error || 'Unknown error'}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Error submitting order:', error);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
main();
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Execute a market order on Hyperliquid
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, parseArgs, checkBuilderFeeApproval } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Open Broker - Market Order
|
|
10
|
+
==========================
|
|
11
|
+
|
|
12
|
+
Execute a market order with slippage protection.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
npx tsx scripts/operations/market-order.ts --coin <COIN> --side <buy|sell> --size <SIZE>
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--coin Asset to trade (e.g., ETH, BTC)
|
|
19
|
+
--side Order side: buy or sell
|
|
20
|
+
--size Order size in base asset
|
|
21
|
+
--slippage Slippage tolerance in bps (default: from config, usually 50 = 0.5%)
|
|
22
|
+
--reduce Reduce-only order (default: false)
|
|
23
|
+
--dry Dry run - show order details without executing
|
|
24
|
+
--verbose Show full API request/response for debugging
|
|
25
|
+
|
|
26
|
+
Environment:
|
|
27
|
+
HYPERLIQUID_PRIVATE_KEY Your wallet private key (0x...)
|
|
28
|
+
HYPERLIQUID_NETWORK "mainnet" or "testnet" (default: mainnet)
|
|
29
|
+
VERBOSE=1 Enable verbose logging
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
npx tsx scripts/operations/market-order.ts --coin ETH --side buy --size 0.1
|
|
33
|
+
npx tsx scripts/operations/market-order.ts --coin BTC --side sell --size 0.01 --slippage 100
|
|
34
|
+
npx tsx scripts/operations/market-order.ts --coin SOL --side buy --size 10 --dry
|
|
35
|
+
npx tsx scripts/operations/market-order.ts --coin ETH --side buy --size 0.1 --verbose
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
const args = parseArgs(process.argv.slice(2));
|
|
41
|
+
|
|
42
|
+
// Parse and validate arguments
|
|
43
|
+
const coin = args.coin as string;
|
|
44
|
+
const side = args.side as string;
|
|
45
|
+
const size = parseFloat(args.size as string);
|
|
46
|
+
const slippage = args.slippage ? parseInt(args.slippage as string) : undefined;
|
|
47
|
+
const reduceOnly = args.reduce as boolean;
|
|
48
|
+
const dryRun = args.dry as boolean;
|
|
49
|
+
|
|
50
|
+
if (!coin || !side || isNaN(size)) {
|
|
51
|
+
printUsage();
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (side !== 'buy' && side !== 'sell') {
|
|
56
|
+
console.error('Error: --side must be "buy" or "sell"');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (size <= 0) {
|
|
61
|
+
console.error('Error: --size must be positive');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const isBuy = side === 'buy';
|
|
66
|
+
const client = getClient();
|
|
67
|
+
|
|
68
|
+
// Enable verbose mode if requested
|
|
69
|
+
if (args.verbose) {
|
|
70
|
+
client.verbose = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('Open Broker - Market Order');
|
|
74
|
+
console.log('==========================\n');
|
|
75
|
+
|
|
76
|
+
// Check builder fee approval (warning only, don't block)
|
|
77
|
+
await checkBuilderFeeApproval(client);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Get current price
|
|
81
|
+
const mids = await client.getAllMids();
|
|
82
|
+
const midPrice = parseFloat(mids[coin]);
|
|
83
|
+
|
|
84
|
+
if (!midPrice) {
|
|
85
|
+
console.error(`Error: No market data for ${coin}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Calculate order details
|
|
90
|
+
const slippageBps = slippage ?? 50;
|
|
91
|
+
const slippageMultiplier = slippageBps / 10000;
|
|
92
|
+
const limitPrice = isBuy
|
|
93
|
+
? midPrice * (1 + slippageMultiplier)
|
|
94
|
+
: midPrice * (1 - slippageMultiplier);
|
|
95
|
+
const notional = midPrice * size;
|
|
96
|
+
|
|
97
|
+
console.log('Order Details');
|
|
98
|
+
console.log('-------------');
|
|
99
|
+
console.log(`Coin: ${coin}`);
|
|
100
|
+
console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
|
|
101
|
+
console.log(`Size: ${size}`);
|
|
102
|
+
console.log(`Mid Price: ${formatUsd(midPrice)}`);
|
|
103
|
+
console.log(`Limit Price: ${formatUsd(limitPrice)} (${slippageBps} bps slippage)`);
|
|
104
|
+
console.log(`Notional: ~${formatUsd(notional)}`);
|
|
105
|
+
console.log(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
|
|
106
|
+
console.log(`Builder Fee: ${client.builderInfo.f / 10} bps`);
|
|
107
|
+
|
|
108
|
+
if (dryRun) {
|
|
109
|
+
console.log('\nš Dry run - order not submitted');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log('\nExecuting...');
|
|
114
|
+
|
|
115
|
+
const response = await client.marketOrder(coin, isBuy, size, slippage);
|
|
116
|
+
|
|
117
|
+
console.log('\nResult');
|
|
118
|
+
console.log('------');
|
|
119
|
+
|
|
120
|
+
// Log full response for debugging
|
|
121
|
+
if (args.verbose || process.env.VERBOSE) {
|
|
122
|
+
console.log('\nFull Response:');
|
|
123
|
+
console.log(JSON.stringify(response, null, 2));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
127
|
+
const statuses = response.response.data.statuses;
|
|
128
|
+
for (const status of statuses) {
|
|
129
|
+
if (status.filled) {
|
|
130
|
+
const fillSz = parseFloat(status.filled.totalSz);
|
|
131
|
+
const avgPx = parseFloat(status.filled.avgPx);
|
|
132
|
+
const fillNotional = fillSz * avgPx;
|
|
133
|
+
const slippageActual = isBuy
|
|
134
|
+
? (avgPx - midPrice) / midPrice
|
|
135
|
+
: (midPrice - avgPx) / midPrice;
|
|
136
|
+
|
|
137
|
+
console.log(`ā
Filled`);
|
|
138
|
+
console.log(` Order ID: ${status.filled.oid}`);
|
|
139
|
+
console.log(` Size: ${fillSz}`);
|
|
140
|
+
console.log(` Avg Price: ${formatUsd(avgPx)}`);
|
|
141
|
+
console.log(` Notional: ${formatUsd(fillNotional)}`);
|
|
142
|
+
console.log(` Slippage: ${(slippageActual * 10000).toFixed(1)} bps`);
|
|
143
|
+
} else if (status.resting) {
|
|
144
|
+
console.log(`ā³ Resting (partial fill)`);
|
|
145
|
+
console.log(` Order ID: ${status.resting.oid}`);
|
|
146
|
+
} else if (status.error) {
|
|
147
|
+
console.log(`ā Error: ${status.error}`);
|
|
148
|
+
} else {
|
|
149
|
+
// Unknown status - show the whole thing
|
|
150
|
+
console.log(`ā ļø Unknown status:`);
|
|
151
|
+
console.log(JSON.stringify(status, null, 2));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else if (response.status === 'err') {
|
|
155
|
+
console.log(`ā API Error: ${response.response || JSON.stringify(response)}`);
|
|
156
|
+
} else {
|
|
157
|
+
console.log(`ā Unexpected response:`);
|
|
158
|
+
console.log(JSON.stringify(response, null, 2));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error executing order:', error);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
main();
|