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,292 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- // DCA (Dollar Cost Averaging) Strategy - Buy fixed amounts at regular intervals
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 - DCA (Dollar Cost Average)
10
- =======================================
11
-
12
- Automatically buy a fixed USD amount at regular intervals to average into
13
- a position over time, reducing the impact of volatility.
14
-
15
- Usage:
16
- npx tsx scripts/strategies/dca.ts --coin <COIN> --amount <USD> --interval <PERIOD> --count <N>
17
-
18
- Options:
19
- --coin Asset to accumulate (e.g., ETH, BTC)
20
- --amount USD amount per purchase
21
- --interval Time between purchases (e.g., 1h, 4h, 1d, 1w)
22
- --count Number of purchases to make
23
- --total OR total USD to invest (calculates amount per interval)
24
- --slippage Slippage tolerance in bps (default: 50)
25
- --dry Dry run - show DCA plan without executing
26
-
27
- Interval Format:
28
- Xm = X minutes (e.g., 30m)
29
- Xh = X hours (e.g., 4h, 24h)
30
- Xd = X days (e.g., 1d, 7d)
31
- Xw = X weeks (e.g., 1w)
32
-
33
- Examples:
34
- # Buy $100 of ETH every hour for 24 purchases
35
- npx tsx scripts/strategies/dca.ts --coin ETH --amount 100 --interval 1h --count 24
36
-
37
- # Invest $5000 in BTC over 30 days with daily purchases
38
- npx tsx scripts/strategies/dca.ts --coin BTC --total 5000 --interval 1d --count 30
39
-
40
- # Preview DCA plan
41
- npx tsx scripts/strategies/dca.ts --coin SOL --amount 50 --interval 4h --count 42 --dry
42
-
43
- DCA Benefits:
44
- - Removes emotion from buying decisions
45
- - Averages out entry price over time
46
- - Reduces risk of buying at local tops
47
- - Disciplined long-term accumulation strategy
48
- `);
49
- }
50
-
51
- interface DcaPurchase {
52
- number: number;
53
- timestamp: Date;
54
- targetAmount: number;
55
- actualAmount: number;
56
- size: number;
57
- price: number;
58
- status: 'completed' | 'partial' | 'failed' | 'pending';
59
- error?: string;
60
- }
61
-
62
- function parseInterval(interval: string): number {
63
- const match = interval.match(/^(\d+)(m|h|d|w)$/i);
64
- if (!match) {
65
- throw new Error(`Invalid interval format: ${interval}. Use Xm, Xh, Xd, or Xw`);
66
- }
67
-
68
- const value = parseInt(match[1]);
69
- const unit = match[2].toLowerCase();
70
-
71
- switch (unit) {
72
- case 'm':
73
- return value * 60 * 1000;
74
- case 'h':
75
- return value * 60 * 60 * 1000;
76
- case 'd':
77
- return value * 24 * 60 * 60 * 1000;
78
- case 'w':
79
- return value * 7 * 24 * 60 * 60 * 1000;
80
- default:
81
- throw new Error(`Unknown interval unit: ${unit}`);
82
- }
83
- }
84
-
85
- function formatInterval(ms: number): string {
86
- const minutes = ms / 60000;
87
- if (minutes < 60) return `${minutes}m`;
88
- const hours = minutes / 60;
89
- if (hours < 24) return `${hours}h`;
90
- const days = hours / 24;
91
- if (days < 7) return `${days}d`;
92
- return `${days / 7}w`;
93
- }
94
-
95
- function formatDuration(ms: number): string {
96
- const seconds = Math.floor(ms / 1000);
97
- if (seconds < 60) return `${seconds}s`;
98
- const minutes = Math.floor(seconds / 60);
99
- if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
100
- const hours = Math.floor(minutes / 60);
101
- if (hours < 24) return `${hours}h ${minutes % 60}m`;
102
- const days = Math.floor(hours / 24);
103
- return `${days}d ${hours % 24}h`;
104
- }
105
-
106
- async function main() {
107
- const args = parseArgs(process.argv.slice(2));
108
-
109
- const coin = args.coin as string;
110
- const intervalStr = args.interval as string;
111
- const count = args.count ? parseInt(args.count as string) : undefined;
112
- const amountPerPurchase = args.amount ? parseFloat(args.amount as string) : undefined;
113
- const totalAmount = args.total ? parseFloat(args.total as string) : undefined;
114
- const slippage = args.slippage ? parseInt(args.slippage as string) : undefined;
115
- const dryRun = args.dry as boolean;
116
-
117
- if (!coin || !intervalStr || !count) {
118
- printUsage();
119
- process.exit(1);
120
- }
121
-
122
- if (!amountPerPurchase && !totalAmount) {
123
- console.error('Error: Must specify either --amount or --total');
124
- process.exit(1);
125
- }
126
-
127
- const amount = amountPerPurchase || (totalAmount! / count);
128
- const total = totalAmount || (amountPerPurchase! * count);
129
-
130
- let intervalMs: number;
131
- try {
132
- intervalMs = parseInterval(intervalStr);
133
- } catch (err) {
134
- console.error(err instanceof Error ? err.message : String(err));
135
- process.exit(1);
136
- }
137
-
138
- const client = getClient();
139
-
140
- if (args.verbose) {
141
- client.verbose = true;
142
- }
143
-
144
- console.log('Open Broker - DCA Strategy');
145
- console.log('==========================\n');
146
-
147
- try {
148
- const mids = await client.getAllMids();
149
- const currentPrice = parseFloat(mids[coin]);
150
- if (!currentPrice) {
151
- console.error(`Error: No market data for ${coin}`);
152
- process.exit(1);
153
- }
154
-
155
- const totalDuration = intervalMs * (count - 1);
156
- const sizePerPurchase = amount / currentPrice;
157
- const totalSize = sizePerPurchase * count;
158
-
159
- console.log('DCA Plan');
160
- console.log('--------');
161
- console.log(`Coin: ${coin}`);
162
- console.log(`Current Price: ${formatUsd(currentPrice)}`);
163
- console.log(`Amount/Purchase: ${formatUsd(amount)}`);
164
- console.log(`Purchases: ${count}`);
165
- console.log(`Interval: ${formatInterval(intervalMs)}`);
166
- console.log(`Total Investment: ${formatUsd(total)}`);
167
- console.log(`Total Duration: ${formatDuration(totalDuration)}`);
168
- console.log(`\nAt Current Price:`);
169
- console.log(` Size/Purchase: ${sizePerPurchase.toFixed(6)} ${coin}`);
170
- console.log(` Total Size: ${totalSize.toFixed(6)} ${coin}`);
171
-
172
- // Show schedule
173
- console.log('\nSchedule Preview');
174
- console.log('----------------');
175
- const now = new Date();
176
- const previewCount = Math.min(5, count);
177
- for (let i = 0; i < previewCount; i++) {
178
- const time = new Date(now.getTime() + intervalMs * i);
179
- console.log(` #${i + 1}: ${time.toLocaleString()} - ${formatUsd(amount)}`);
180
- }
181
- if (count > 5) {
182
- console.log(` ... ${count - 5} more purchases`);
183
- const lastTime = new Date(now.getTime() + intervalMs * (count - 1));
184
- console.log(` #${count}: ${lastTime.toLocaleString()} - ${formatUsd(amount)}`);
185
- }
186
-
187
- if (dryRun) {
188
- console.log('\n--- Dry run complete ---');
189
- return;
190
- }
191
-
192
- console.log('\nStarting DCA execution...\n');
193
-
194
- const purchases: DcaPurchase[] = [];
195
- let totalSpent = 0;
196
- let totalAcquired = 0;
197
-
198
- for (let i = 0; i < count; i++) {
199
- const purchaseNum = i + 1;
200
- console.log(`[${purchaseNum}/${count}] Executing purchase of ${formatUsd(amount)} ${coin}...`);
201
-
202
- const purchase: DcaPurchase = {
203
- number: purchaseNum,
204
- timestamp: new Date(),
205
- targetAmount: amount,
206
- actualAmount: 0,
207
- size: 0,
208
- price: 0,
209
- status: 'pending',
210
- };
211
-
212
- try {
213
- // Get current price and calculate size
214
- const newMids = await client.getAllMids();
215
- const newPrice = parseFloat(newMids[coin]);
216
- const size = amount / newPrice;
217
-
218
- const response = await client.marketOrder(coin, true, size, slippage);
219
-
220
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
221
- const status = response.response.data.statuses[0];
222
- if (status?.filled) {
223
- purchase.size = parseFloat(status.filled.totalSz);
224
- purchase.price = parseFloat(status.filled.avgPx);
225
- purchase.actualAmount = purchase.size * purchase.price;
226
- purchase.status = purchase.actualAmount >= amount * 0.95 ? 'completed' : 'partial';
227
-
228
- totalSpent += purchase.actualAmount;
229
- totalAcquired += purchase.size;
230
-
231
- const avgPrice = totalSpent / totalAcquired;
232
- console.log(` Filled: ${purchase.size.toFixed(6)} ${coin} @ ${formatUsd(purchase.price)}`);
233
- console.log(` Running: ${totalAcquired.toFixed(6)} ${coin} | Avg: ${formatUsd(avgPrice)} | Spent: ${formatUsd(totalSpent)}`);
234
- } else if (status?.error) {
235
- purchase.status = 'failed';
236
- purchase.error = status.error;
237
- console.log(` Failed: ${status.error}`);
238
- }
239
- } else {
240
- purchase.status = 'failed';
241
- purchase.error = typeof response.response === 'string' ? response.response : 'Unknown error';
242
- console.log(` Failed: ${purchase.error}`);
243
- }
244
- } catch (err) {
245
- purchase.status = 'failed';
246
- purchase.error = err instanceof Error ? err.message : String(err);
247
- console.log(` Error: ${purchase.error}`);
248
- }
249
-
250
- purchases.push(purchase);
251
-
252
- // Wait for next interval (unless last purchase)
253
- if (i < count - 1) {
254
- const nextTime = new Date(Date.now() + intervalMs);
255
- console.log(` Next purchase: ${nextTime.toLocaleString()}\n`);
256
- await sleep(intervalMs);
257
- }
258
- }
259
-
260
- // Summary
261
- const avgPrice = totalSpent / totalAcquired;
262
- const currentMid = parseFloat((await client.getAllMids())[coin]);
263
- const unrealizedPnl = (currentMid - avgPrice) * totalAcquired;
264
- const successful = purchases.filter(p => p.status === 'completed' || p.status === 'partial').length;
265
- const failed = purchases.filter(p => p.status === 'failed').length;
266
-
267
- console.log('\n========== DCA Summary ==========');
268
- console.log(`Purchases: ${successful}/${count} successful${failed > 0 ? ` (${failed} failed)` : ''}`);
269
- console.log(`Total Spent: ${formatUsd(totalSpent)} / ${formatUsd(total)} target`);
270
- console.log(`Total Acquired: ${totalAcquired.toFixed(6)} ${coin}`);
271
- console.log(`Average Price: ${formatUsd(avgPrice)}`);
272
- console.log(`Current Price: ${formatUsd(currentMid)}`);
273
- console.log(`Unrealized PnL: ${formatUsd(unrealizedPnl)} (${((unrealizedPnl / totalSpent) * 100).toFixed(2)}%)`);
274
-
275
- // Show price history
276
- if (purchases.length > 1) {
277
- const prices = purchases.filter(p => p.price > 0).map(p => p.price);
278
- const minPrice = Math.min(...prices);
279
- const maxPrice = Math.max(...prices);
280
- console.log(`\nPrice Range:`);
281
- console.log(` Lowest: ${formatUsd(minPrice)}`);
282
- console.log(` Highest: ${formatUsd(maxPrice)}`);
283
- console.log(` Your Avg: ${formatUsd(avgPrice)} (${((avgPrice - minPrice) / (maxPrice - minPrice) * 100).toFixed(0)}% of range)`);
284
- }
285
-
286
- } catch (error) {
287
- console.error('Error:', error);
288
- process.exit(1);
289
- }
290
- }
291
-
292
- main();
@@ -1,352 +0,0 @@
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 - loads main + HIP-3 assets
99
- await client.getMetaAndAssetCtxs();
100
-
101
- // For HIP-3 assets, we need to fetch funding from the dex-specific metadata
102
- let hourlyFunding: number;
103
- let openInterestVal: number;
104
-
105
- if (client.isHip3(coin)) {
106
- const allPerps = await client.getAllPerpMetas();
107
- const dexName = client.getCoinDex(coin);
108
- const dexData = allPerps.find(d => d.dexName === dexName);
109
- if (!dexData) {
110
- console.error(`Error: No market data for HIP-3 dex ${dexName}`);
111
- process.exit(1);
112
- }
113
- // API returns universe names already prefixed (e.g., "xyz:CL")
114
- const assetIdx = dexData.meta.universe.findIndex(a => a.name === coin);
115
- if (assetIdx === -1 || !dexData.assetCtxs[assetIdx]) {
116
- console.error(`Error: No market data for ${coin}`);
117
- process.exit(1);
118
- }
119
- hourlyFunding = parseFloat(dexData.assetCtxs[assetIdx].funding);
120
- openInterestVal = parseFloat(dexData.assetCtxs[assetIdx].openInterest);
121
- } else {
122
- const meta = await client.getMetaAndAssetCtxs();
123
- const assetIndex = client.getAssetIndex(coin);
124
- const assetCtx = meta.assetCtxs[assetIndex];
125
- hourlyFunding = parseFloat(assetCtx.funding);
126
- openInterestVal = parseFloat(assetCtx.openInterest);
127
- }
128
-
129
- const mids = await client.getAllMids();
130
- const midPrice = parseFloat(mids[coin]);
131
- if (!midPrice) {
132
- console.error(`Error: No market data for ${coin}`);
133
- process.exit(1);
134
- }
135
-
136
- const annualizedFunding = annualizeFundingRate(hourlyFunding) * 100; // as percentage
137
- const positionSize = notionalSize / midPrice;
138
-
139
- console.log('Current Market State');
140
- console.log('--------------------');
141
- console.log(`Coin: ${coin}`);
142
- console.log(`Mid Price: ${formatUsd(midPrice)}`);
143
- console.log(`Hourly Funding: ${(hourlyFunding * 100).toFixed(4)}%`);
144
- console.log(`Annualized: ${annualizedFunding.toFixed(2)}%`);
145
- console.log(`Open Interest: ${formatUsd(openInterestVal)}`);
146
- console.log(`\nStrategy Config`);
147
- console.log('---------------');
148
- console.log(`Target Notional: ${formatUsd(notionalSize)}`);
149
- console.log(`Position Size: ${positionSize.toFixed(6)} ${coin}`);
150
- console.log(`Min Funding: ${minFunding}% annualized`);
151
- console.log(`Max Funding: ${maxFunding}% annualized`);
152
- console.log(`Close At: ${closeAt}% annualized`);
153
- console.log(`Check Interval: ${checkIntervalMins} minutes`);
154
- console.log(`Mode: ${mode.toUpperCase()}`);
155
-
156
- // Determine action based on funding
157
- const absAnnualized = Math.abs(annualizedFunding);
158
- const shouldGoShort = annualizedFunding > 0; // Positive funding = longs pay shorts
159
- const suggestedSide = shouldGoShort ? 'SHORT' : 'LONG';
160
-
161
- console.log(`\nFunding Analysis`);
162
- console.log('----------------');
163
- if (absAnnualized >= minFunding && absAnnualized <= maxFunding) {
164
- console.log(`Status: OPPORTUNITY FOUND`);
165
- console.log(`Suggested: ${suggestedSide} to collect ${absAnnualized.toFixed(2)}% APR`);
166
-
167
- const hourlyPayment = Math.abs(hourlyFunding) * notionalSize;
168
- const dailyPayment = hourlyPayment * 24;
169
- const monthlyPayment = dailyPayment * 30;
170
- console.log(`\nEstimated Funding Income:`);
171
- console.log(` Hourly: ${formatUsd(hourlyPayment)}`);
172
- console.log(` Daily: ${formatUsd(dailyPayment)}`);
173
- console.log(` Monthly: ${formatUsd(monthlyPayment)}`);
174
- } else if (absAnnualized < minFunding) {
175
- console.log(`Status: FUNDING TOO LOW (${absAnnualized.toFixed(2)}% < ${minFunding}%)`);
176
- console.log(`Action: Wait for higher funding rates`);
177
- } else {
178
- console.log(`Status: FUNDING TOO HIGH (${absAnnualized.toFixed(2)}% > ${maxFunding}%)`);
179
- console.log(`Action: Risk of squeeze, skip this opportunity`);
180
- }
181
-
182
- if (dryRun) {
183
- console.log('\n--- Dry run complete ---');
184
- return;
185
- }
186
-
187
- // Check if we should enter
188
- if (absAnnualized < minFunding || absAnnualized > maxFunding) {
189
- console.log('\nNo action taken - funding outside target range.');
190
-
191
- if (durationHours === Infinity) {
192
- console.log('Exiting. Use --duration to run continuous monitoring.');
193
- return;
194
- }
195
- }
196
-
197
- // Execute entry if conditions met
198
- let position: FundingPosition | null = null;
199
-
200
- if (absAnnualized >= minFunding && absAnnualized <= maxFunding) {
201
- console.log(`\nOpening ${suggestedSide} position...`);
202
-
203
- const isBuy = !shouldGoShort;
204
- const response = await client.marketOrder(coin, isBuy, positionSize);
205
-
206
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
207
- const status = response.response.data.statuses[0];
208
- if (status?.filled) {
209
- const avgPrice = parseFloat(status.filled.avgPx);
210
- const filledSize = parseFloat(status.filled.totalSz);
211
- console.log(` Entry filled: ${filledSize} ${coin} @ ${formatUsd(avgPrice)}`);
212
-
213
- position = {
214
- coin,
215
- side: shouldGoShort ? 'short' : 'long',
216
- size: filledSize,
217
- entryPrice: avgPrice,
218
- entryFunding: annualizedFunding,
219
- entryTime: new Date(),
220
- };
221
- } else if (status?.error) {
222
- console.log(` Entry failed: ${status.error}`);
223
- }
224
- } else {
225
- console.log(` Entry failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
226
- }
227
- }
228
-
229
- // Monitoring loop
230
- if (durationHours !== Infinity || position) {
231
- const endTime = durationHours === Infinity ? Infinity : Date.now() + durationHours * 3600 * 1000;
232
- const checkInterval = checkIntervalMins * 60 * 1000;
233
-
234
- console.log(`\nMonitoring funding rates...`);
235
- console.log(`(Press Ctrl+C to exit)\n`);
236
-
237
- let totalFundingCollected = 0;
238
- let lastCheck = Date.now();
239
-
240
- while (Date.now() < endTime) {
241
- await sleep(checkInterval);
242
-
243
- // Get updated funding
244
- let newHourlyFunding: number;
245
- if (client.isHip3(coin)) {
246
- const freshPerps = await client.getAllPerpMetas();
247
- const dexName = client.getCoinDex(coin);
248
- const dexData = freshPerps.find(d => d.dexName === dexName);
249
- // API returns universe names already prefixed (e.g., "xyz:CL")
250
- const idx = dexData?.meta.universe.findIndex(a => a.name === coin) ?? -1;
251
- newHourlyFunding = idx >= 0 && dexData?.assetCtxs[idx] ? parseFloat(dexData.assetCtxs[idx].funding) : 0;
252
- } else {
253
- const newMeta = await client.getMetaAndAssetCtxs();
254
- const assetIndex = client.getAssetIndex(coin);
255
- newHourlyFunding = parseFloat(newMeta.assetCtxs[assetIndex].funding);
256
- }
257
- const newAnnualized = annualizeFundingRate(newHourlyFunding) * 100;
258
- const newMids = await client.getAllMids();
259
- const newPrice = parseFloat(newMids[coin]);
260
-
261
- const timeElapsed = (Date.now() - lastCheck) / 3600000; // hours
262
- lastCheck = Date.now();
263
-
264
- console.log(`[${new Date().toLocaleTimeString()}] ${coin}: ${newAnnualized.toFixed(2)}% APR, Price: ${formatUsd(newPrice)}`);
265
-
266
- if (position) {
267
- // Calculate funding collected
268
- const fundingCollected = Math.abs(newHourlyFunding) * position.size * newPrice * timeElapsed;
269
- totalFundingCollected += fundingCollected;
270
-
271
- // Calculate PnL
272
- const pricePnl = position.side === 'long'
273
- ? (newPrice - position.entryPrice) * position.size
274
- : (position.entryPrice - newPrice) * position.size;
275
- const totalPnl = pricePnl + totalFundingCollected;
276
-
277
- console.log(` Position: ${position.side.toUpperCase()} ${position.size.toFixed(6)} @ ${formatUsd(position.entryPrice)}`);
278
- console.log(` Price PnL: ${formatUsd(pricePnl)}, Funding: ${formatUsd(totalFundingCollected)}, Total: ${formatUsd(totalPnl)}`);
279
-
280
- // Check if we should close
281
- const shouldClose =
282
- (position.side === 'short' && newAnnualized < closeAt) ||
283
- (position.side === 'long' && newAnnualized > -closeAt);
284
-
285
- if (shouldClose) {
286
- console.log(`\n Funding dropped below ${closeAt}%, closing position...`);
287
-
288
- const closeIsBuy = position.side === 'short';
289
- const closeResponse = await client.marketOrder(coin, closeIsBuy, position.size);
290
-
291
- if (closeResponse.status === 'ok' && closeResponse.response && typeof closeResponse.response === 'object') {
292
- const closeStatus = closeResponse.response.data.statuses[0];
293
- if (closeStatus?.filled) {
294
- const exitPrice = parseFloat(closeStatus.filled.avgPx);
295
- console.log(` Closed @ ${formatUsd(exitPrice)}`);
296
- console.log(`\n === Position Summary ===`);
297
- console.log(` Entry: ${formatUsd(position.entryPrice)}`);
298
- console.log(` Exit: ${formatUsd(exitPrice)}`);
299
- console.log(` Price PnL: ${formatUsd(pricePnl)}`);
300
- console.log(` Funding: ${formatUsd(totalFundingCollected)}`);
301
- console.log(` Total PnL: ${formatUsd(totalPnl)}`);
302
- position = null;
303
- }
304
- }
305
- }
306
- } else {
307
- // No position - check if we should enter
308
- const absNewAnnualized = Math.abs(newAnnualized);
309
- if (absNewAnnualized >= minFunding && absNewAnnualized <= maxFunding) {
310
- const enterShort = newAnnualized > 0;
311
- const enterSide = enterShort ? 'SHORT' : 'LONG';
312
- console.log(` Funding opportunity: ${enterSide} at ${absNewAnnualized.toFixed(2)}% APR`);
313
-
314
- const enterIsBuy = !enterShort;
315
- const currentSize = notionalSize / newPrice;
316
- const response = await client.marketOrder(coin, enterIsBuy, currentSize);
317
-
318
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
319
- const status = response.response.data.statuses[0];
320
- if (status?.filled) {
321
- position = {
322
- coin,
323
- side: enterShort ? 'short' : 'long',
324
- size: parseFloat(status.filled.totalSz),
325
- entryPrice: parseFloat(status.filled.avgPx),
326
- entryFunding: newAnnualized,
327
- entryTime: new Date(),
328
- };
329
- console.log(` Entered ${enterSide} ${position.size.toFixed(6)} @ ${formatUsd(position.entryPrice)}`);
330
- }
331
- }
332
- }
333
- }
334
-
335
- console.log('');
336
- }
337
-
338
- // Close any remaining position at end of duration
339
- if (position) {
340
- console.log(`\nDuration ended. Closing position...`);
341
- const closeIsBuy = position.side === 'short';
342
- await client.marketOrder(coin, closeIsBuy, position.size);
343
- }
344
- }
345
-
346
- } catch (error) {
347
- console.error('Error:', error);
348
- process.exit(1);
349
- }
350
- }
351
-
352
- main();