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.
@@ -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();