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,242 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Open Broker - Automated Onboarding for AI Agents
|
|
3
|
+
// Creates wallet, configures environment, and approves builder fee
|
|
4
|
+
|
|
5
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const OPEN_BROKER_BUILDER_ADDRESS = '0xbb67021fA3e62ab4DA985bb5a55c5c1884381068';
|
|
12
|
+
|
|
13
|
+
interface OnboardResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
walletAddress?: string;
|
|
16
|
+
privateKey?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createReadline(): readline.Interface {
|
|
21
|
+
return readline.createInterface({
|
|
22
|
+
input: process.stdin,
|
|
23
|
+
output: process.stdout,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function prompt(rl: readline.Interface, question: string): Promise<string> {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
rl.question(question, (answer) => {
|
|
30
|
+
resolve(answer.trim());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isValidPrivateKey(key: string): boolean {
|
|
36
|
+
// Check if it's a valid 64-char hex string with 0x prefix
|
|
37
|
+
return /^0x[a-fA-F0-9]{64}$/.test(key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get project root relative to this script (scripts/setup/onboard.ts -> project root)
|
|
41
|
+
function getProjectRoot(): string {
|
|
42
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
43
|
+
const __dirname = path.dirname(__filename);
|
|
44
|
+
return path.resolve(__dirname, '../..');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main(): Promise<OnboardResult> {
|
|
48
|
+
console.log('Open Broker - Automated Onboarding');
|
|
49
|
+
console.log('===================================\n');
|
|
50
|
+
|
|
51
|
+
const projectRoot = getProjectRoot();
|
|
52
|
+
const envPath = path.join(projectRoot, '.env');
|
|
53
|
+
|
|
54
|
+
// Check if .env already exists
|
|
55
|
+
if (fs.existsSync(envPath)) {
|
|
56
|
+
console.log('⚠️ .env file already exists!');
|
|
57
|
+
console.log(' To re-onboard, delete .env first or edit manually.\n');
|
|
58
|
+
|
|
59
|
+
// Read existing config and show wallet address
|
|
60
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
61
|
+
const keyMatch = envContent.match(/HYPERLIQUID_PRIVATE_KEY=0x([a-fA-F0-9]{64})/);
|
|
62
|
+
|
|
63
|
+
if (keyMatch) {
|
|
64
|
+
const existingKey = `0x${keyMatch[1]}` as `0x${string}`;
|
|
65
|
+
const account = privateKeyToAccount(existingKey);
|
|
66
|
+
console.log('Existing Configuration');
|
|
67
|
+
console.log('----------------------');
|
|
68
|
+
console.log(`Wallet Address: ${account.address}`);
|
|
69
|
+
console.log(`\nTo fund this wallet, send USDC to the address above on Arbitrum.`);
|
|
70
|
+
console.log(`Then deposit to Hyperliquid at: https://app.hyperliquid.xyz/`);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
walletAddress: account.address,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
error: 'Invalid .env file - missing or malformed private key',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Ask user if they have an existing private key
|
|
85
|
+
const rl = createReadline();
|
|
86
|
+
|
|
87
|
+
console.log('Do you have an existing Hyperliquid private key?\n');
|
|
88
|
+
console.log(' 1) Yes, I have a private key ready');
|
|
89
|
+
console.log(' 2) No, generate a new wallet for me\n');
|
|
90
|
+
|
|
91
|
+
let choice = '';
|
|
92
|
+
while (choice !== '1' && choice !== '2') {
|
|
93
|
+
choice = await prompt(rl, 'Enter choice (1 or 2): ');
|
|
94
|
+
if (choice !== '1' && choice !== '2') {
|
|
95
|
+
console.log('Please enter 1 or 2');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let privateKey: `0x${string}`;
|
|
100
|
+
|
|
101
|
+
if (choice === '1') {
|
|
102
|
+
// User has existing key
|
|
103
|
+
console.log('\nEnter your private key (0x... format):');
|
|
104
|
+
console.log('(Input is hidden for security)\n');
|
|
105
|
+
|
|
106
|
+
let validKey = false;
|
|
107
|
+
while (!validKey) {
|
|
108
|
+
const inputKey = await prompt(rl, 'Private key: ');
|
|
109
|
+
|
|
110
|
+
if (isValidPrivateKey(inputKey)) {
|
|
111
|
+
privateKey = inputKey as `0x${string}`;
|
|
112
|
+
validKey = true;
|
|
113
|
+
} else {
|
|
114
|
+
console.log('Invalid private key format. Must be 0x followed by 64 hex characters.');
|
|
115
|
+
console.log('Example: 0x1234...abcd (66 characters total)\n');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('\n✅ Private key accepted');
|
|
120
|
+
} else {
|
|
121
|
+
// Generate new wallet
|
|
122
|
+
console.log('\nGenerating new wallet...');
|
|
123
|
+
privateKey = generatePrivateKey();
|
|
124
|
+
console.log('✅ New wallet created');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
rl.close();
|
|
128
|
+
|
|
129
|
+
// Derive account from private key
|
|
130
|
+
const account = privateKeyToAccount(privateKey);
|
|
131
|
+
console.log(`\nWallet Address: ${account.address}\n`);
|
|
132
|
+
|
|
133
|
+
// Create .env file
|
|
134
|
+
console.log('Creating .env file...');
|
|
135
|
+
|
|
136
|
+
const envContent = `# Open Broker - Environment Variables
|
|
137
|
+
# Generated automatically during onboarding
|
|
138
|
+
# WARNING: Keep this file secret! Never commit to git!
|
|
139
|
+
|
|
140
|
+
# Your wallet private key
|
|
141
|
+
HYPERLIQUID_PRIVATE_KEY=${privateKey}
|
|
142
|
+
|
|
143
|
+
# Network: mainnet or testnet
|
|
144
|
+
HYPERLIQUID_NETWORK=mainnet
|
|
145
|
+
|
|
146
|
+
# Builder fee configuration (supports open-broker development)
|
|
147
|
+
# Default: 1 bps (0.01%) on trades
|
|
148
|
+
BUILDER_ADDRESS=${OPEN_BROKER_BUILDER_ADDRESS}
|
|
149
|
+
BUILDER_FEE=10
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
fs.writeFileSync(envPath, envContent, { mode: 0o600 }); // Restricted permissions
|
|
153
|
+
console.log(`✅ .env created at: ${envPath}\n`);
|
|
154
|
+
|
|
155
|
+
// Approve builder fee
|
|
156
|
+
console.log('Approving builder fee...');
|
|
157
|
+
console.log('(This is free and required before trading)\n');
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// Import and run approve-builder inline
|
|
161
|
+
const { getClient } = await import('../core/client.js');
|
|
162
|
+
const client = getClient();
|
|
163
|
+
|
|
164
|
+
console.log(` Account: ${client.address}`);
|
|
165
|
+
console.log(` Builder: ${OPEN_BROKER_BUILDER_ADDRESS}`);
|
|
166
|
+
|
|
167
|
+
// Check if already approved
|
|
168
|
+
const currentApproval = await client.getMaxBuilderFee(client.address, OPEN_BROKER_BUILDER_ADDRESS);
|
|
169
|
+
|
|
170
|
+
if (currentApproval) {
|
|
171
|
+
console.log(`\n✅ Builder fee already approved (${currentApproval})`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log('\n Sending approval transaction...');
|
|
174
|
+
const result = await client.approveBuilderFee('0.1%', OPEN_BROKER_BUILDER_ADDRESS);
|
|
175
|
+
|
|
176
|
+
if (result.status === 'ok') {
|
|
177
|
+
console.log('✅ Builder fee approved successfully!');
|
|
178
|
+
} else {
|
|
179
|
+
console.log(`⚠️ Approval may have failed: ${result.response}`);
|
|
180
|
+
console.log(' You can retry later: npx tsx scripts/setup/approve-builder.ts');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.log(`⚠️ Could not approve builder fee: ${error}`);
|
|
185
|
+
console.log(' You can retry later: npx tsx scripts/setup/approve-builder.ts');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Final summary
|
|
189
|
+
console.log('\n========================================');
|
|
190
|
+
console.log(' ONBOARDING COMPLETE! ');
|
|
191
|
+
console.log('========================================\n');
|
|
192
|
+
|
|
193
|
+
console.log('Your Trading Wallet');
|
|
194
|
+
console.log('-------------------');
|
|
195
|
+
console.log(`Address: ${account.address}`);
|
|
196
|
+
console.log(`Network: Hyperliquid (Mainnet)`);
|
|
197
|
+
|
|
198
|
+
if (choice === '2') {
|
|
199
|
+
console.log('\n⚠️ IMPORTANT: Save your private key!');
|
|
200
|
+
console.log('-----------------------------------');
|
|
201
|
+
console.log(`Private Key: ${privateKey}`);
|
|
202
|
+
console.log('\nThis key is stored in .env but you should back it up securely.');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log('\n📋 Next Step: Fund Your Wallet');
|
|
206
|
+
console.log('-------------------------------');
|
|
207
|
+
console.log('1. Send USDC to your wallet on Arbitrum:');
|
|
208
|
+
console.log(` ${account.address}`);
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log('2. Deposit USDC to Hyperliquid:');
|
|
211
|
+
console.log(' https://app.hyperliquid.xyz/');
|
|
212
|
+
console.log(' (Connect wallet → Deposit → Select amount)');
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log('3. Start trading!');
|
|
215
|
+
console.log(' npx tsx scripts/info/account.ts');
|
|
216
|
+
console.log(' npx tsx scripts/operations/market-order.ts --coin ETH --side buy --size 0.01 --dry');
|
|
217
|
+
|
|
218
|
+
console.log('\n⚠️ SECURITY REMINDER');
|
|
219
|
+
console.log('---------------------');
|
|
220
|
+
console.log('Your private key is stored in .env');
|
|
221
|
+
console.log('NEVER share this file or commit it to git!');
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
walletAddress: account.address,
|
|
226
|
+
privateKey: privateKey,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Export for programmatic use
|
|
231
|
+
export { main as onboard };
|
|
232
|
+
|
|
233
|
+
// Run if executed directly
|
|
234
|
+
main().then(result => {
|
|
235
|
+
if (!result.success) {
|
|
236
|
+
console.error(`\nOnboarding failed: ${result.error}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}).catch(error => {
|
|
240
|
+
console.error('Onboarding error:', error);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
});
|
|
@@ -0,0 +1,292 @@
|
|
|
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();
|