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.
- package/SKILL.md +102 -57
- package/bin/cli.ts +2 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -9
- package/scripts/auto/cli.ts +114 -20
- package/scripts/auto/examples/dca.ts +72 -0
- package/scripts/auto/examples/funding-arb.ts +98 -0
- package/scripts/auto/examples/grid.ts +135 -0
- package/scripts/auto/examples/mm-maker.ts +125 -0
- package/scripts/auto/examples/mm-spread.ts +115 -0
- package/scripts/auto/loader.ts +47 -1
- package/scripts/auto/runtime.ts +13 -0
- package/scripts/auto/types.ts +14 -0
- package/scripts/plugin/tools.ts +20 -7
- package/scripts/strategies/dca.ts +0 -292
- package/scripts/strategies/funding-arb.ts +0 -352
- package/scripts/strategies/grid.ts +0 -397
- package/scripts/strategies/mm-maker.ts +0 -411
- package/scripts/strategies/mm-spread.ts +0 -402
|
@@ -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();
|