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,319 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Funding Arbitrage Strategy - Collect funding by being on the paying side
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, formatPercent, parseArgs, sleep, annualizeFundingRate } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Open Broker - Funding Arbitrage
|
|
10
|
+
===============================
|
|
11
|
+
|
|
12
|
+
Collect funding payments by taking positions opposite to the majority.
|
|
13
|
+
When funding is positive (longs pay shorts), go short to collect.
|
|
14
|
+
When funding is negative (shorts pay longs), go long to collect.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
npx tsx scripts/strategies/funding-arb.ts --coin <COIN> --size <SIZE> [options]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--coin Asset to trade (e.g., ETH, BTC)
|
|
21
|
+
--size Position size in USD notional
|
|
22
|
+
--min-funding Minimum annualized funding rate to enter (default: 20%)
|
|
23
|
+
--max-funding Maximum funding rate - avoid extreme rates (default: 200%)
|
|
24
|
+
--duration How long to run in hours (default: runs until stopped)
|
|
25
|
+
--check Interval to check funding in minutes (default: 60)
|
|
26
|
+
--close-at Close position when funding drops below X% (default: 5%)
|
|
27
|
+
--dry Dry run - show opportunities without trading
|
|
28
|
+
|
|
29
|
+
Modes:
|
|
30
|
+
--mode perp Perp only - directional exposure to funding (default)
|
|
31
|
+
--mode hedge Open opposite spot position for delta neutral (requires spot)
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
# Monitor ETH funding, enter $5000 short if funding > 25% annualized
|
|
35
|
+
npx tsx scripts/strategies/funding-arb.ts --coin ETH --size 5000 --min-funding 25
|
|
36
|
+
|
|
37
|
+
# Run for 24 hours, check every 30 minutes
|
|
38
|
+
npx tsx scripts/strategies/funding-arb.ts --coin BTC --size 10000 --duration 24 --check 30
|
|
39
|
+
|
|
40
|
+
# Preview current opportunities
|
|
41
|
+
npx tsx scripts/strategies/funding-arb.ts --coin ETH --size 5000 --dry
|
|
42
|
+
|
|
43
|
+
Funding Info:
|
|
44
|
+
- Hyperliquid funding is paid HOURLY (not 8h like CEXs)
|
|
45
|
+
- Positive rate: longs pay shorts (go short to collect)
|
|
46
|
+
- Negative rate: shorts pay longs (go long to collect)
|
|
47
|
+
- Annualized = hourly rate × 8760
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface FundingPosition {
|
|
52
|
+
coin: string;
|
|
53
|
+
side: 'long' | 'short';
|
|
54
|
+
size: number;
|
|
55
|
+
entryPrice: number;
|
|
56
|
+
entryFunding: number;
|
|
57
|
+
entryTime: Date;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const args = parseArgs(process.argv.slice(2));
|
|
62
|
+
|
|
63
|
+
const coin = args.coin as string;
|
|
64
|
+
const notionalSize = parseFloat(args.size as string);
|
|
65
|
+
const minFunding = args['min-funding'] ? parseFloat(args['min-funding'] as string) : 20;
|
|
66
|
+
const maxFunding = args['max-funding'] ? parseFloat(args['max-funding'] as string) : 200;
|
|
67
|
+
const durationHours = args.duration ? parseFloat(args.duration as string) : Infinity;
|
|
68
|
+
const checkIntervalMins = args.check ? parseFloat(args.check as string) : 60;
|
|
69
|
+
const closeAt = args['close-at'] ? parseFloat(args['close-at'] as string) : 5;
|
|
70
|
+
const mode = (args.mode as string) || 'perp';
|
|
71
|
+
const dryRun = args.dry as boolean;
|
|
72
|
+
|
|
73
|
+
if (!coin || isNaN(notionalSize)) {
|
|
74
|
+
printUsage();
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (mode !== 'perp' && mode !== 'hedge') {
|
|
79
|
+
console.error('Error: --mode must be "perp" or "hedge"');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (mode === 'hedge') {
|
|
84
|
+
console.error('Error: Hedge mode (perp + spot) not yet implemented');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const client = getClient();
|
|
89
|
+
|
|
90
|
+
if (args.verbose) {
|
|
91
|
+
client.verbose = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('Open Broker - Funding Arbitrage');
|
|
95
|
+
console.log('===============================\n');
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Initial funding check
|
|
99
|
+
await client.getMetaAndAssetCtxs();
|
|
100
|
+
const meta = await client.getMetaAndAssetCtxs();
|
|
101
|
+
const assetIndex = client.getAssetIndex(coin);
|
|
102
|
+
const assetCtx = meta.assetCtxs[assetIndex];
|
|
103
|
+
|
|
104
|
+
const mids = await client.getAllMids();
|
|
105
|
+
const midPrice = parseFloat(mids[coin]);
|
|
106
|
+
if (!midPrice) {
|
|
107
|
+
console.error(`Error: No market data for ${coin}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const hourlyFunding = parseFloat(assetCtx.funding);
|
|
112
|
+
const annualizedFunding = annualizeFundingRate(hourlyFunding) * 100; // as percentage
|
|
113
|
+
const openInterest = parseFloat(assetCtx.openInterest);
|
|
114
|
+
const positionSize = notionalSize / midPrice;
|
|
115
|
+
|
|
116
|
+
console.log('Current Market State');
|
|
117
|
+
console.log('--------------------');
|
|
118
|
+
console.log(`Coin: ${coin}`);
|
|
119
|
+
console.log(`Mid Price: ${formatUsd(midPrice)}`);
|
|
120
|
+
console.log(`Hourly Funding: ${(hourlyFunding * 100).toFixed(4)}%`);
|
|
121
|
+
console.log(`Annualized: ${annualizedFunding.toFixed(2)}%`);
|
|
122
|
+
console.log(`Open Interest: ${formatUsd(openInterest)}`);
|
|
123
|
+
console.log(`\nStrategy Config`);
|
|
124
|
+
console.log('---------------');
|
|
125
|
+
console.log(`Target Notional: ${formatUsd(notionalSize)}`);
|
|
126
|
+
console.log(`Position Size: ${positionSize.toFixed(6)} ${coin}`);
|
|
127
|
+
console.log(`Min Funding: ${minFunding}% annualized`);
|
|
128
|
+
console.log(`Max Funding: ${maxFunding}% annualized`);
|
|
129
|
+
console.log(`Close At: ${closeAt}% annualized`);
|
|
130
|
+
console.log(`Check Interval: ${checkIntervalMins} minutes`);
|
|
131
|
+
console.log(`Mode: ${mode.toUpperCase()}`);
|
|
132
|
+
|
|
133
|
+
// Determine action based on funding
|
|
134
|
+
const absAnnualized = Math.abs(annualizedFunding);
|
|
135
|
+
const shouldGoShort = annualizedFunding > 0; // Positive funding = longs pay shorts
|
|
136
|
+
const suggestedSide = shouldGoShort ? 'SHORT' : 'LONG';
|
|
137
|
+
|
|
138
|
+
console.log(`\nFunding Analysis`);
|
|
139
|
+
console.log('----------------');
|
|
140
|
+
if (absAnnualized >= minFunding && absAnnualized <= maxFunding) {
|
|
141
|
+
console.log(`Status: OPPORTUNITY FOUND`);
|
|
142
|
+
console.log(`Suggested: ${suggestedSide} to collect ${absAnnualized.toFixed(2)}% APR`);
|
|
143
|
+
|
|
144
|
+
const hourlyPayment = Math.abs(hourlyFunding) * notionalSize;
|
|
145
|
+
const dailyPayment = hourlyPayment * 24;
|
|
146
|
+
const monthlyPayment = dailyPayment * 30;
|
|
147
|
+
console.log(`\nEstimated Funding Income:`);
|
|
148
|
+
console.log(` Hourly: ${formatUsd(hourlyPayment)}`);
|
|
149
|
+
console.log(` Daily: ${formatUsd(dailyPayment)}`);
|
|
150
|
+
console.log(` Monthly: ${formatUsd(monthlyPayment)}`);
|
|
151
|
+
} else if (absAnnualized < minFunding) {
|
|
152
|
+
console.log(`Status: FUNDING TOO LOW (${absAnnualized.toFixed(2)}% < ${minFunding}%)`);
|
|
153
|
+
console.log(`Action: Wait for higher funding rates`);
|
|
154
|
+
} else {
|
|
155
|
+
console.log(`Status: FUNDING TOO HIGH (${absAnnualized.toFixed(2)}% > ${maxFunding}%)`);
|
|
156
|
+
console.log(`Action: Risk of squeeze, skip this opportunity`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (dryRun) {
|
|
160
|
+
console.log('\n--- Dry run complete ---');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check if we should enter
|
|
165
|
+
if (absAnnualized < minFunding || absAnnualized > maxFunding) {
|
|
166
|
+
console.log('\nNo action taken - funding outside target range.');
|
|
167
|
+
|
|
168
|
+
if (durationHours === Infinity) {
|
|
169
|
+
console.log('Exiting. Use --duration to run continuous monitoring.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Execute entry if conditions met
|
|
175
|
+
let position: FundingPosition | null = null;
|
|
176
|
+
|
|
177
|
+
if (absAnnualized >= minFunding && absAnnualized <= maxFunding) {
|
|
178
|
+
console.log(`\nOpening ${suggestedSide} position...`);
|
|
179
|
+
|
|
180
|
+
const isBuy = !shouldGoShort;
|
|
181
|
+
const response = await client.marketOrder(coin, isBuy, positionSize);
|
|
182
|
+
|
|
183
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
184
|
+
const status = response.response.data.statuses[0];
|
|
185
|
+
if (status?.filled) {
|
|
186
|
+
const avgPrice = parseFloat(status.filled.avgPx);
|
|
187
|
+
const filledSize = parseFloat(status.filled.totalSz);
|
|
188
|
+
console.log(` Entry filled: ${filledSize} ${coin} @ ${formatUsd(avgPrice)}`);
|
|
189
|
+
|
|
190
|
+
position = {
|
|
191
|
+
coin,
|
|
192
|
+
side: shouldGoShort ? 'short' : 'long',
|
|
193
|
+
size: filledSize,
|
|
194
|
+
entryPrice: avgPrice,
|
|
195
|
+
entryFunding: annualizedFunding,
|
|
196
|
+
entryTime: new Date(),
|
|
197
|
+
};
|
|
198
|
+
} else if (status?.error) {
|
|
199
|
+
console.log(` Entry failed: ${status.error}`);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
console.log(` Entry failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Monitoring loop
|
|
207
|
+
if (durationHours !== Infinity || position) {
|
|
208
|
+
const endTime = durationHours === Infinity ? Infinity : Date.now() + durationHours * 3600 * 1000;
|
|
209
|
+
const checkInterval = checkIntervalMins * 60 * 1000;
|
|
210
|
+
|
|
211
|
+
console.log(`\nMonitoring funding rates...`);
|
|
212
|
+
console.log(`(Press Ctrl+C to exit)\n`);
|
|
213
|
+
|
|
214
|
+
let totalFundingCollected = 0;
|
|
215
|
+
let lastCheck = Date.now();
|
|
216
|
+
|
|
217
|
+
while (Date.now() < endTime) {
|
|
218
|
+
await sleep(checkInterval);
|
|
219
|
+
|
|
220
|
+
// Get updated funding
|
|
221
|
+
const newMeta = await client.getMetaAndAssetCtxs();
|
|
222
|
+
const newAssetCtx = newMeta.assetCtxs[assetIndex];
|
|
223
|
+
const newHourlyFunding = parseFloat(newAssetCtx.funding);
|
|
224
|
+
const newAnnualized = annualizeFundingRate(newHourlyFunding) * 100;
|
|
225
|
+
const newMids = await client.getAllMids();
|
|
226
|
+
const newPrice = parseFloat(newMids[coin]);
|
|
227
|
+
|
|
228
|
+
const timeElapsed = (Date.now() - lastCheck) / 3600000; // hours
|
|
229
|
+
lastCheck = Date.now();
|
|
230
|
+
|
|
231
|
+
console.log(`[${new Date().toLocaleTimeString()}] ${coin}: ${newAnnualized.toFixed(2)}% APR, Price: ${formatUsd(newPrice)}`);
|
|
232
|
+
|
|
233
|
+
if (position) {
|
|
234
|
+
// Calculate funding collected
|
|
235
|
+
const fundingCollected = Math.abs(newHourlyFunding) * position.size * newPrice * timeElapsed;
|
|
236
|
+
totalFundingCollected += fundingCollected;
|
|
237
|
+
|
|
238
|
+
// Calculate PnL
|
|
239
|
+
const pricePnl = position.side === 'long'
|
|
240
|
+
? (newPrice - position.entryPrice) * position.size
|
|
241
|
+
: (position.entryPrice - newPrice) * position.size;
|
|
242
|
+
const totalPnl = pricePnl + totalFundingCollected;
|
|
243
|
+
|
|
244
|
+
console.log(` Position: ${position.side.toUpperCase()} ${position.size.toFixed(6)} @ ${formatUsd(position.entryPrice)}`);
|
|
245
|
+
console.log(` Price PnL: ${formatUsd(pricePnl)}, Funding: ${formatUsd(totalFundingCollected)}, Total: ${formatUsd(totalPnl)}`);
|
|
246
|
+
|
|
247
|
+
// Check if we should close
|
|
248
|
+
const shouldClose =
|
|
249
|
+
(position.side === 'short' && newAnnualized < closeAt) ||
|
|
250
|
+
(position.side === 'long' && newAnnualized > -closeAt);
|
|
251
|
+
|
|
252
|
+
if (shouldClose) {
|
|
253
|
+
console.log(`\n Funding dropped below ${closeAt}%, closing position...`);
|
|
254
|
+
|
|
255
|
+
const closeIsBuy = position.side === 'short';
|
|
256
|
+
const closeResponse = await client.marketOrder(coin, closeIsBuy, position.size);
|
|
257
|
+
|
|
258
|
+
if (closeResponse.status === 'ok' && closeResponse.response && typeof closeResponse.response === 'object') {
|
|
259
|
+
const closeStatus = closeResponse.response.data.statuses[0];
|
|
260
|
+
if (closeStatus?.filled) {
|
|
261
|
+
const exitPrice = parseFloat(closeStatus.filled.avgPx);
|
|
262
|
+
console.log(` Closed @ ${formatUsd(exitPrice)}`);
|
|
263
|
+
console.log(`\n === Position Summary ===`);
|
|
264
|
+
console.log(` Entry: ${formatUsd(position.entryPrice)}`);
|
|
265
|
+
console.log(` Exit: ${formatUsd(exitPrice)}`);
|
|
266
|
+
console.log(` Price PnL: ${formatUsd(pricePnl)}`);
|
|
267
|
+
console.log(` Funding: ${formatUsd(totalFundingCollected)}`);
|
|
268
|
+
console.log(` Total PnL: ${formatUsd(totalPnl)}`);
|
|
269
|
+
position = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
// No position - check if we should enter
|
|
275
|
+
const absNewAnnualized = Math.abs(newAnnualized);
|
|
276
|
+
if (absNewAnnualized >= minFunding && absNewAnnualized <= maxFunding) {
|
|
277
|
+
const enterShort = newAnnualized > 0;
|
|
278
|
+
const enterSide = enterShort ? 'SHORT' : 'LONG';
|
|
279
|
+
console.log(` Funding opportunity: ${enterSide} at ${absNewAnnualized.toFixed(2)}% APR`);
|
|
280
|
+
|
|
281
|
+
const enterIsBuy = !enterShort;
|
|
282
|
+
const currentSize = notionalSize / newPrice;
|
|
283
|
+
const response = await client.marketOrder(coin, enterIsBuy, currentSize);
|
|
284
|
+
|
|
285
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
286
|
+
const status = response.response.data.statuses[0];
|
|
287
|
+
if (status?.filled) {
|
|
288
|
+
position = {
|
|
289
|
+
coin,
|
|
290
|
+
side: enterShort ? 'short' : 'long',
|
|
291
|
+
size: parseFloat(status.filled.totalSz),
|
|
292
|
+
entryPrice: parseFloat(status.filled.avgPx),
|
|
293
|
+
entryFunding: newAnnualized,
|
|
294
|
+
entryTime: new Date(),
|
|
295
|
+
};
|
|
296
|
+
console.log(` Entered ${enterSide} ${position.size.toFixed(6)} @ ${formatUsd(position.entryPrice)}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log('');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Close any remaining position at end of duration
|
|
306
|
+
if (position) {
|
|
307
|
+
console.log(`\nDuration ended. Closing position...`);
|
|
308
|
+
const closeIsBuy = position.side === 'short';
|
|
309
|
+
await client.marketOrder(coin, closeIsBuy, position.size);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error('Error:', error);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
main();
|