openbroker 1.0.67 → 1.0.69

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.
@@ -1,411 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- // Maker-Only Market Making - Always provide liquidity, never take
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 - Maker-Only Market Making
10
- ======================================
11
-
12
- Provide liquidity using ALO (Add Liquidity Only) orders that are REJECTED
13
- if they would cross the spread. This guarantees you always earn maker rebates
14
- (~0.3 bps) instead of paying taker fees.
15
-
16
- Usage:
17
- npx tsx scripts/strategies/mm-maker.ts --coin <COIN> --size <SIZE> --offset <BPS>
18
-
19
- Options:
20
- --coin Asset to market make (e.g., ETH, BTC)
21
- --size Order size on each side (in base asset)
22
- --offset Offset from best bid/ask in bps (default: 1)
23
- --max-position Maximum net position (default: 3x size)
24
- --skew-factor How aggressively to skew for inventory (default: 2.0)
25
- --refresh Refresh interval in milliseconds (default: 2000)
26
- --duration How long to run in minutes (default: infinite)
27
- --dry Dry run - show setup without trading
28
-
29
- How ALO Works:
30
- - ALO = Add Liquidity Only (post-only)
31
- - Orders are REJECTED if they would immediately match
32
- - You ALWAYS earn maker rebate (~0.3 bps) instead of paying taker fee
33
- - Guarantees you're providing liquidity, not taking it
34
-
35
- Pricing Strategy:
36
- - Reads actual order book (best bid/ask) not just mid price
37
- - Places bid at: bestBid - offset (or bestBid if joining)
38
- - Places ask at: bestAsk + offset (or bestAsk if joining)
39
- - Skews prices based on inventory to stay neutral
40
-
41
- Examples:
42
- # Market make HYPE with 1 bps offset from best bid/ask
43
- npx tsx scripts/strategies/mm-maker.ts --coin HYPE --size 1 --offset 1
44
-
45
- # Wider offset for volatile assets
46
- npx tsx scripts/strategies/mm-maker.ts --coin ETH --size 0.1 --offset 2 --max-position 0.5
47
-
48
- # Preview setup
49
- npx tsx scripts/strategies/mm-maker.ts --coin HYPE --size 1 --offset 1 --dry
50
-
51
- Fee Structure (Hyperliquid):
52
- - Taker fee: ~2.5 bps (you PAY)
53
- - Maker rebate: ~0.3 bps (you EARN)
54
- - By using ALO only, you always earn the rebate!
55
- `);
56
- }
57
-
58
- interface Quote {
59
- side: 'bid' | 'ask';
60
- price: number;
61
- size: number;
62
- oid?: number;
63
- placedAt: number;
64
- }
65
-
66
- interface MmState {
67
- bid: Quote | null;
68
- ask: Quote | null;
69
- lastBidFill: number;
70
- lastAskFill: number;
71
- totalBought: number;
72
- totalSold: number;
73
- totalBuyCost: number;
74
- totalSellRevenue: number;
75
- roundTrips: number;
76
- rejections: number;
77
- }
78
-
79
- async function main() {
80
- const args = parseArgs(process.argv.slice(2));
81
-
82
- const coin = args.coin as string;
83
- const size = parseFloat(args.size as string);
84
- const offsetBps = args.offset ? parseFloat(args.offset as string) : 1;
85
- const maxPosition = args['max-position'] ? parseFloat(args['max-position'] as string) : size * 3;
86
- const skewFactor = args['skew-factor'] ? parseFloat(args['skew-factor'] as string) : 2.0;
87
- const refreshMs = args.refresh ? parseInt(args.refresh as string) : 2000;
88
- const durationMins = args.duration ? parseFloat(args.duration as string) : Infinity;
89
- const dryRun = args.dry as boolean;
90
-
91
- if (!coin || isNaN(size)) {
92
- printUsage();
93
- process.exit(1);
94
- }
95
-
96
- const client = getClient();
97
-
98
- if (args.verbose) {
99
- client.verbose = true;
100
- }
101
-
102
- console.log('Open Broker - Maker-Only MM');
103
- console.log('===========================\n');
104
-
105
- try {
106
- // Get order book
107
- const book = await client.getL2Book(coin);
108
-
109
- if (book.bestBid === 0 || book.bestAsk === 0) {
110
- console.error(`Error: No order book for ${coin}`);
111
- process.exit(1);
112
- }
113
-
114
- const offsetFraction = offsetBps / 10000;
115
- const bidPrice = book.bestBid * (1 - offsetFraction);
116
- const askPrice = book.bestAsk * (1 + offsetFraction);
117
- const notionalPerSide = size * book.midPrice;
118
-
119
- console.log('Strategy Configuration');
120
- console.log('----------------------');
121
- console.log(`Coin: ${coin}`);
122
- console.log(`Order Size: ${size} ${coin}`);
123
- console.log(`Notional/Side: ${formatUsd(notionalPerSide)}`);
124
- console.log(`Offset: ${offsetBps} bps`);
125
- console.log(`Max Position: ±${maxPosition} ${coin}`);
126
- console.log(`Skew Factor: ${skewFactor}x`);
127
- console.log(`Refresh: ${refreshMs}ms`);
128
- console.log(`\nCurrent Order Book:`);
129
- console.log(` Best Bid: ${formatUsd(book.bestBid)}`);
130
- console.log(` Best Ask: ${formatUsd(book.bestAsk)}`);
131
- console.log(` Mid Price: ${formatUsd(book.midPrice)}`);
132
- console.log(` Spread: ${book.spreadBps.toFixed(2)} bps`);
133
- console.log(`\nQuote Prices (at neutral):`);
134
- console.log(` Our Bid: ${formatUsd(bidPrice)} (${offsetBps} bps below best)`);
135
- console.log(` Our Ask: ${formatUsd(askPrice)} (${offsetBps} bps above best)`);
136
- console.log(`\nOrder Type: ALO (Add Liquidity Only)`);
137
- console.log(` - Orders rejected if they would cross`);
138
- console.log(` - Guarantees maker rebate (~0.3 bps)`);
139
-
140
- if (dryRun) {
141
- console.log('\n--- Dry run complete ---');
142
- return;
143
- }
144
-
145
- console.log('\nStarting maker-only MM...\n');
146
-
147
- const state: MmState = {
148
- bid: null,
149
- ask: null,
150
- lastBidFill: 0,
151
- lastAskFill: 0,
152
- totalBought: 0,
153
- totalSold: 0,
154
- totalBuyCost: 0,
155
- totalSellRevenue: 0,
156
- roundTrips: 0,
157
- rejections: 0,
158
- };
159
-
160
- const endTime = durationMins === Infinity ? Infinity : Date.now() + durationMins * 60 * 1000;
161
-
162
- // Handle graceful shutdown
163
- let shuttingDown = false;
164
- process.on('SIGINT', async () => {
165
- if (shuttingDown) return;
166
- shuttingDown = true;
167
- console.log('\n\nShutting down - cancelling quotes...');
168
- await cancelQuotes(client, coin, state);
169
- await printSummary(client, coin, state);
170
- process.exit(0);
171
- });
172
-
173
- let lastLogTime = 0;
174
-
175
- while (Date.now() < endTime && !shuttingDown) {
176
- const now = Date.now();
177
-
178
- // Get fresh order book
179
- const freshBook = await client.getL2Book(coin);
180
-
181
- // Get actual position from exchange
182
- const userState = await client.getUserState();
183
- const position = userState.assetPositions.find(p => p.position.coin === coin);
184
- const actualPosition = position ? parseFloat(position.position.szi) : 0;
185
-
186
- // Get our open orders
187
- const openOrders = await client.getOpenOrders();
188
- const ourOrders = openOrders.filter(o => o.coin === coin);
189
- const openOids = new Set(ourOrders.map(o => o.oid));
190
-
191
- // Check for bid fill
192
- if (state.bid && state.bid.oid && !openOids.has(state.bid.oid)) {
193
- const fillPrice = state.bid.price;
194
- state.totalBought += state.bid.size;
195
- state.totalBuyCost += fillPrice * state.bid.size;
196
- state.lastBidFill = now;
197
- console.log(`[${new Date().toLocaleTimeString()}] BID FILLED @ ${formatUsd(fillPrice)} | Pos: ${actualPosition.toFixed(4)} | +rebate`);
198
- state.bid = null;
199
- }
200
-
201
- // Check for ask fill
202
- if (state.ask && state.ask.oid && !openOids.has(state.ask.oid)) {
203
- const fillPrice = state.ask.price;
204
- state.totalSold += state.ask.size;
205
- state.totalSellRevenue += fillPrice * state.ask.size;
206
- state.lastAskFill = now;
207
- state.roundTrips += 0.5;
208
- console.log(`[${new Date().toLocaleTimeString()}] ASK FILLED @ ${formatUsd(fillPrice)} | Pos: ${actualPosition.toFixed(4)} | +rebate`);
209
- state.ask = null;
210
- }
211
-
212
- // Calculate inventory-based skew
213
- const inventoryRatio = Math.max(-1, Math.min(1, actualPosition / maxPosition));
214
-
215
- // Determine if we should quote each side
216
- const shouldBid = actualPosition < maxPosition;
217
- const shouldAsk = actualPosition > -maxPosition;
218
-
219
- // Calculate skewed prices relative to order book
220
- // When long: bid further from best (wider), ask closer to best (tighter)
221
- // When short: bid closer to best (tighter), ask further from best (wider)
222
- const bidSkewMult = 1 + inventoryRatio * skewFactor;
223
- const askSkewMult = 1 - inventoryRatio * skewFactor;
224
-
225
- const targetBidPrice = freshBook.bestBid * (1 - offsetFraction * Math.max(0.1, bidSkewMult));
226
- const targetAskPrice = freshBook.bestAsk * (1 + offsetFraction * Math.max(0.1, askSkewMult));
227
-
228
- // CRITICAL: Ensure we don't cross the spread
229
- // Our bid must be BELOW the best ask
230
- // Our ask must be ABOVE the best bid
231
- const safeBidPrice = Math.min(targetBidPrice, freshBook.bestAsk * 0.9999);
232
- const safeAskPrice = Math.max(targetAskPrice, freshBook.bestBid * 1.0001);
233
-
234
- // Cancel stale quotes
235
- if (state.bid && state.bid.oid) {
236
- const bidDrift = Math.abs(state.bid.price - safeBidPrice) / freshBook.midPrice;
237
- if (bidDrift > 0.0005 || !shouldBid) {
238
- try {
239
- await client.cancel(coin, state.bid.oid);
240
- } catch {
241
- // May have been filled
242
- }
243
- state.bid = null;
244
- }
245
- }
246
-
247
- if (state.ask && state.ask.oid) {
248
- const askDrift = Math.abs(state.ask.price - safeAskPrice) / freshBook.midPrice;
249
- if (askDrift > 0.0005 || !shouldAsk) {
250
- try {
251
- await client.cancel(coin, state.ask.oid);
252
- } catch {
253
- // May have been filled
254
- }
255
- state.ask = null;
256
- }
257
- }
258
-
259
- // Place new bid using ALO
260
- if (shouldBid && !state.bid) {
261
- // Double check: our bid must be below the best ask
262
- if (safeBidPrice < freshBook.bestAsk) {
263
- const response = await client.limitOrder(coin, true, size, safeBidPrice, 'Alo', false);
264
-
265
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
266
- const status = response.response.data.statuses[0];
267
- if (status?.resting) {
268
- state.bid = {
269
- side: 'bid',
270
- price: safeBidPrice,
271
- size,
272
- oid: status.resting.oid,
273
- placedAt: now,
274
- };
275
- } else if (status?.error) {
276
- // ALO rejection - order would have crossed
277
- state.rejections++;
278
- if (state.rejections % 10 === 1) {
279
- console.log(`[${new Date().toLocaleTimeString()}] ALO bid rejected (would cross) - this is expected`);
280
- }
281
- }
282
- }
283
- }
284
- }
285
-
286
- // Place new ask using ALO
287
- if (shouldAsk && !state.ask) {
288
- // Double check: our ask must be above the best bid
289
- if (safeAskPrice > freshBook.bestBid) {
290
- const response = await client.limitOrder(coin, false, size, safeAskPrice, 'Alo', false);
291
-
292
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
293
- const status = response.response.data.statuses[0];
294
- if (status?.resting) {
295
- state.ask = {
296
- side: 'ask',
297
- price: safeAskPrice,
298
- size,
299
- oid: status.resting.oid,
300
- placedAt: now,
301
- };
302
- } else if (status?.error) {
303
- // ALO rejection - order would have crossed
304
- state.rejections++;
305
- if (state.rejections % 10 === 1) {
306
- console.log(`[${new Date().toLocaleTimeString()}] ALO ask rejected (would cross) - this is expected`);
307
- }
308
- }
309
- }
310
- }
311
- }
312
-
313
- // Periodic status log
314
- if (now - lastLogTime > 30000) {
315
- const bidStatus = state.bid ? formatUsd(state.bid.price) : (shouldBid ? 'placing...' : 'at max long');
316
- const askStatus = state.ask ? formatUsd(state.ask.price) : (shouldAsk ? 'placing...' : 'at max short');
317
- const realizedPnl = state.totalSellRevenue - state.totalBuyCost;
318
- const skewPct = (inventoryRatio * 100).toFixed(1);
319
-
320
- console.log(`[${new Date().toLocaleTimeString()}] Book: ${formatUsd(freshBook.bestBid)}/${formatUsd(freshBook.bestAsk)} (${freshBook.spreadBps.toFixed(1)}bps) | Bid: ${bidStatus} | Ask: ${askStatus} | Pos: ${actualPosition.toFixed(4)} (${skewPct}%) | PnL: ${formatUsd(realizedPnl)} | Rej: ${state.rejections}`);
321
- lastLogTime = now;
322
- }
323
-
324
- await sleep(refreshMs);
325
- }
326
-
327
- // End of duration
328
- if (!shuttingDown) {
329
- console.log('\nDuration ended. Cancelling quotes...');
330
- await cancelQuotes(client, coin, state);
331
- await printSummary(client, coin, state);
332
- }
333
-
334
- } catch (error) {
335
- console.error('Error:', error);
336
- process.exit(1);
337
- }
338
- }
339
-
340
- async function cancelQuotes(
341
- client: ReturnType<typeof getClient>,
342
- coin: string,
343
- state: MmState
344
- ): Promise<void> {
345
- if (state.bid && state.bid.oid) {
346
- try {
347
- await client.cancel(coin, state.bid.oid);
348
- console.log(` Cancelled bid @ ${formatUsd(state.bid.price)}`);
349
- } catch {
350
- // Ignore
351
- }
352
- }
353
- if (state.ask && state.ask.oid) {
354
- try {
355
- await client.cancel(coin, state.ask.oid);
356
- console.log(` Cancelled ask @ ${formatUsd(state.ask.price)}`);
357
- } catch {
358
- // Ignore
359
- }
360
- }
361
- }
362
-
363
- async function printSummary(
364
- client: ReturnType<typeof getClient>,
365
- coin: string,
366
- state: MmState
367
- ): Promise<void> {
368
- // Get final position and price
369
- const userState = await client.getUserState();
370
- const position = userState.assetPositions.find(p => p.position.coin === coin);
371
- const finalPosition = position ? parseFloat(position.position.szi) : 0;
372
-
373
- const book = await client.getL2Book(coin);
374
- const currentMid = book.midPrice;
375
-
376
- const inventoryValue = finalPosition * currentMid;
377
- const realizedPnl = state.totalSellRevenue - state.totalBuyCost;
378
-
379
- // Estimate rebates earned (0.3 bps per side)
380
- const totalVolume = state.totalBuyCost + state.totalSellRevenue;
381
- const estimatedRebates = totalVolume * 0.00003; // 0.3 bps
382
-
383
- // Calculate inventory PnL
384
- let inventoryPnl = 0;
385
- if (finalPosition > 0 && state.totalBought > 0) {
386
- const avgBuyPrice = state.totalBuyCost / state.totalBought;
387
- inventoryPnl = (currentMid - avgBuyPrice) * finalPosition;
388
- } else if (finalPosition < 0 && state.totalSold > 0) {
389
- const avgSellPrice = state.totalSellRevenue / state.totalSold;
390
- inventoryPnl = (avgSellPrice - currentMid) * Math.abs(finalPosition);
391
- }
392
-
393
- console.log('\n========== Maker MM Summary ==========');
394
- console.log(`Total Bought: ${state.totalBought.toFixed(6)} ${coin}`);
395
- console.log(`Total Sold: ${state.totalSold.toFixed(6)} ${coin}`);
396
- console.log(`Final Position: ${finalPosition.toFixed(6)} ${coin}`);
397
- console.log(`Inventory Value: ${formatUsd(inventoryValue)}`);
398
- console.log(`Round Trips: ${state.roundTrips.toFixed(1)}`);
399
- console.log(`ALO Rejections: ${state.rejections}`);
400
- console.log(`Realized PnL: ${formatUsd(realizedPnl)}`);
401
- console.log(`Est. Rebates: ${formatUsd(estimatedRebates)} (0.3bps on ${formatUsd(totalVolume)})`);
402
- console.log(`Inventory PnL: ${formatUsd(inventoryPnl)}`);
403
- console.log(`Total PnL: ${formatUsd(realizedPnl + inventoryPnl + estimatedRebates)}`);
404
-
405
- if (Math.abs(finalPosition) > 0.0001) {
406
- console.log(`\n You have an open position of ${finalPosition.toFixed(6)} ${coin}`);
407
- console.log(` Close it manually if desired.`);
408
- }
409
- }
410
-
411
- main();