openbroker 1.1.2 → 1.3.0

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,200 @@
1
+ #!/usr/bin/env tsx
2
+ // HIP-4 Outcomes - search and inspect prediction markets
3
+
4
+ import { getClient } from '../core/client.js';
5
+
6
+ interface Args {
7
+ query?: string;
8
+ outcome?: string;
9
+ side?: string;
10
+ balances?: boolean;
11
+ top?: number;
12
+ verbose?: boolean;
13
+ json?: boolean;
14
+ }
15
+
16
+ function parseArgs(): Args {
17
+ const args: Args = {};
18
+
19
+ for (let i = 2; i < process.argv.length; i++) {
20
+ const arg = process.argv[i];
21
+ if ((arg === '--query' || arg === '-q') && process.argv[i + 1]) {
22
+ args.query = process.argv[++i];
23
+ } else if ((arg === '--outcome' || arg === '--id') && process.argv[i + 1]) {
24
+ args.outcome = process.argv[++i];
25
+ } else if (arg === '--side' && process.argv[i + 1]) {
26
+ args.side = process.argv[++i];
27
+ } else if (arg === '--balances') {
28
+ args.balances = true;
29
+ } else if (arg === '--top' && process.argv[i + 1]) {
30
+ args.top = parseInt(process.argv[++i], 10);
31
+ } else if (arg === '--verbose') {
32
+ args.verbose = true;
33
+ } else if (arg === '--json') {
34
+ args.json = true;
35
+ } else if (arg === '--help' || arg === '-h') {
36
+ printUsage();
37
+ process.exit(0);
38
+ } else if (!args.query && !arg.startsWith('-')) {
39
+ args.query = arg;
40
+ }
41
+ }
42
+
43
+ return args;
44
+ }
45
+
46
+ function printUsage() {
47
+ console.log(`
48
+ Open Broker - HIP-4 Outcomes
49
+ ============================
50
+
51
+ Search and inspect Hyperliquid outcome markets.
52
+
53
+ Usage:
54
+ openbroker outcomes [--query <text>] [--outcome <id|#encoding|+encoding>] [options]
55
+
56
+ Options:
57
+ --query, -q <text> Search market name, description, underlying, expiry, target
58
+ --outcome, --id <ref> Show one outcome by id or encoded coin (#1230 / +1230)
59
+ --side <yes|no|0|1> Select a side when using a plain outcome id
60
+ --balances Show outcome token balances for the configured account
61
+ --top <n> Show only top N matches
62
+ --json Output as JSON
63
+ --verbose Include raw descriptions and question metadata
64
+
65
+ Examples:
66
+ openbroker outcomes --query BTC
67
+ openbroker outcomes --outcome 123
68
+ openbroker outcomes --outcome 123 --side yes --json
69
+ openbroker outcomes --balances
70
+ `);
71
+ }
72
+
73
+ function formatPrice(value?: string): string {
74
+ if (!value) return '-';
75
+ const n = parseFloat(value);
76
+ if (!Number.isFinite(n)) return value;
77
+ return n.toFixed(4);
78
+ }
79
+
80
+ function formatVolume(value?: string): string {
81
+ if (!value) return '-';
82
+ const n = parseFloat(value);
83
+ if (!Number.isFinite(n)) return value;
84
+ if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(2)}M`;
85
+ if (n >= 1_000) return `$${(n / 1_000).toFixed(2)}K`;
86
+ return `$${n.toFixed(2)}`;
87
+ }
88
+
89
+ async function main() {
90
+ const args = parseArgs();
91
+ const client = getClient();
92
+ client.verbose = args.verbose ?? false;
93
+
94
+ if (!args.json) {
95
+ console.log('Open Broker - HIP-4 Outcomes');
96
+ console.log('============================\n');
97
+ }
98
+
99
+ try {
100
+ if (args.balances) {
101
+ const balances = await client.getSpotBalances();
102
+ const outcomeBalances = (balances.balances ?? []).filter((b) =>
103
+ b.coin.startsWith('+') || b.coin.startsWith('#')
104
+ );
105
+
106
+ if (args.json) {
107
+ console.log(JSON.stringify(outcomeBalances, null, 2));
108
+ return;
109
+ }
110
+
111
+ if (outcomeBalances.length === 0) {
112
+ console.log('No outcome token balances found.');
113
+ return;
114
+ }
115
+
116
+ console.log('Outcome Balances');
117
+ console.log('----------------');
118
+ console.log('Token Total Hold Entry Value');
119
+ console.log('-'.repeat(70));
120
+ for (const b of outcomeBalances) {
121
+ console.log(
122
+ `${b.coin.padEnd(12)} ${parseFloat(b.total).toFixed(6).padStart(18)} ` +
123
+ `${parseFloat(b.hold).toFixed(6).padStart(18)} ${formatVolume(b.entryNtl).padStart(15)}`
124
+ );
125
+ }
126
+ return;
127
+ }
128
+
129
+ let markets = await client.getOutcomeMarkets();
130
+
131
+ if (args.outcome) {
132
+ const resolved = client.resolveOutcomeRef(args.outcome, args.side);
133
+ markets = markets.filter((market) => market.outcome === resolved.outcome);
134
+ for (const market of markets) {
135
+ market.sides = market.sides.filter((side) => side.side === resolved.side);
136
+ }
137
+ }
138
+
139
+ if (args.query) {
140
+ const query = args.query.toUpperCase();
141
+ markets = markets.filter((market) => {
142
+ const parsed = Object.values(market.parsedDescription).join(' ');
143
+ const searchable = `${market.name} ${market.description} ${parsed}`.toUpperCase();
144
+ return searchable.includes(query);
145
+ });
146
+ }
147
+
148
+ markets.sort((a, b) => {
149
+ const aVol = Math.max(...a.sides.map((s) => parseFloat(s.dayNtlVlm ?? '0')));
150
+ const bVol = Math.max(...b.sides.map((s) => parseFloat(s.dayNtlVlm ?? '0')));
151
+ return bVol - aVol;
152
+ });
153
+
154
+ const displayMarkets = args.top ? markets.slice(0, args.top) : markets;
155
+
156
+ if (args.json) {
157
+ console.log(JSON.stringify(displayMarkets, null, 2));
158
+ return;
159
+ }
160
+
161
+ if (displayMarkets.length === 0) {
162
+ console.log('No outcome markets found.');
163
+ return;
164
+ }
165
+
166
+ console.log(`Found ${displayMarkets.length} outcome market(s)\n`);
167
+ console.log('Outcome Side Coin AssetID Price 24h Volume Market');
168
+ console.log('-'.repeat(98));
169
+
170
+ for (const market of displayMarkets) {
171
+ const spec = market.parsedDescription;
172
+ const labelParts = [
173
+ spec.underlying,
174
+ spec.expiry ? `exp ${spec.expiry}` : undefined,
175
+ spec.targetPrice ? `target ${spec.targetPrice}` : undefined,
176
+ ].filter(Boolean);
177
+ const label = labelParts.length > 0 ? labelParts.join(' | ') : market.description;
178
+
179
+ for (const side of market.sides) {
180
+ console.log(
181
+ `${String(market.outcome).padStart(7)} ${side.name.padEnd(4)} ${side.coin.padEnd(9)} ` +
182
+ `${String(side.assetId).padStart(10)} ${formatPrice(side.midPx ?? side.markPx).padStart(7)} ` +
183
+ `${formatVolume(side.dayNtlVlm).padStart(13)} ${label}`
184
+ );
185
+ }
186
+
187
+ if (args.verbose) {
188
+ console.log(` Description: ${market.description}`);
189
+ if (market.question) console.log(` Question: ${market.question.name}`);
190
+ }
191
+ }
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ console.error(`Error: ${message}`);
195
+ console.error('Note: Hyperliquid currently documents outcomeMeta as testnet-only.');
196
+ process.exit(1);
197
+ }
198
+ }
199
+
200
+ main();
@@ -5,7 +5,7 @@ import { getClient } from '../core/client.js';
5
5
 
6
6
  interface Args {
7
7
  query: string;
8
- type?: 'perp' | 'spot' | 'hip3' | 'all';
8
+ type?: 'perp' | 'spot' | 'hip3' | 'outcome' | 'all';
9
9
  verbose?: boolean;
10
10
  json?: boolean;
11
11
  }
@@ -19,7 +19,7 @@ function parseArgs(): Args {
19
19
  args.query = process.argv[++i];
20
20
  } else if (arg === '--type' && process.argv[i + 1]) {
21
21
  const val = process.argv[++i].toLowerCase();
22
- if (['perp', 'spot', 'hip3', 'all'].includes(val)) {
22
+ if (['perp', 'spot', 'hip3', 'outcome', 'all'].includes(val)) {
23
23
  args.type = val as Args['type'];
24
24
  }
25
25
  } else if (arg === '--verbose') {
@@ -34,7 +34,7 @@ Usage: npx tsx scripts/info/search-markets.ts --query <search> [options]
34
34
 
35
35
  Options:
36
36
  --query <search> Search term (required) - matches coin name
37
- --type <type> Filter by market type: perp, spot, hip3, or all (default: all)
37
+ --type <type> Filter by market type: perp, spot, hip3, outcome, or all (default: all)
38
38
  --json Output as JSON (machine-readable)
39
39
  --verbose Show detailed output
40
40
  --help Show this help
@@ -44,6 +44,7 @@ Examples:
44
44
  npx tsx scripts/info/search-markets.ts --query BTC # Find all BTC markets
45
45
  npx tsx scripts/info/search-markets.ts --query ETH --type perp # ETH perps only
46
46
  npx tsx scripts/info/search-markets.ts --query PURR --type spot # PURR spot only
47
+ npx tsx scripts/info/search-markets.ts --query BTC --type outcome # HIP-4 outcomes only
47
48
  npx tsx scripts/info/search-markets.ts --query HYPE --json # JSON output
48
49
  `);
49
50
  process.exit(0);
@@ -95,7 +96,7 @@ async function main() {
95
96
  }
96
97
 
97
98
  interface Result {
98
- type: 'perp' | 'spot' | 'hip3';
99
+ type: 'perp' | 'spot' | 'hip3' | 'outcome';
99
100
  provider: string;
100
101
  coin: string;
101
102
  assetId: number;
@@ -104,6 +105,9 @@ async function main() {
104
105
  funding?: string;
105
106
  maxLeverage?: number;
106
107
  openInterest?: string;
108
+ outcome?: number;
109
+ outcomeSide?: string;
110
+ description?: string;
107
111
  }
108
112
 
109
113
  const results: Result[] = [];
@@ -224,6 +228,34 @@ async function main() {
224
228
  }
225
229
  }
226
230
 
231
+ // Search HIP-4 outcome markets
232
+ if (args.type === 'all' || args.type === 'outcome') {
233
+ try {
234
+ const outcomes = await client.getOutcomeMarkets();
235
+ for (const market of outcomes) {
236
+ const parsed = Object.values(market.parsedDescription).join(' ');
237
+ const searchable = `${market.name} ${market.description} ${parsed}`.toUpperCase();
238
+ if (!searchable.includes(query)) continue;
239
+
240
+ for (const side of market.sides) {
241
+ results.push({
242
+ type: 'outcome',
243
+ provider: 'HIP-4',
244
+ coin: side.coin,
245
+ assetId: side.assetId,
246
+ price: side.midPx ?? side.markPx ?? '0',
247
+ volume24h: parseFloat(side.dayNtlVlm || '0'),
248
+ outcome: market.outcome,
249
+ outcomeSide: side.name,
250
+ description: market.description,
251
+ });
252
+ }
253
+ }
254
+ } catch (e) {
255
+ if (args.verbose) console.error('Failed to fetch HIP-4 outcomes:', e);
256
+ }
257
+ }
258
+
227
259
  // Sort by volume
228
260
  results.sort((a, b) => b.volume24h - a.volume24h);
229
261
 
@@ -242,11 +274,14 @@ async function main() {
242
274
  console.log('-'.repeat(112));
243
275
 
244
276
  for (const m of results) {
245
- const typeStr = m.type === 'hip3' ? 'HIP-3' : m.type.charAt(0).toUpperCase() + m.type.slice(1);
277
+ const typeStr = m.type === 'hip3' ? 'HIP-3' : m.type === 'outcome' ? 'HIP-4' : m.type.charAt(0).toUpperCase() + m.type.slice(1);
246
278
  const oi = m.openInterest ? formatVolume(parseFloat(m.openInterest)) : '-';
247
279
  console.log(
248
280
  `${typeStr.padEnd(8)} ${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)} ${oi.padStart(10)}`
249
281
  );
282
+ if (m.type === 'outcome' && args.verbose) {
283
+ console.log(` Outcome ${m.outcome} ${m.outcomeSide}: ${m.description}`);
284
+ }
250
285
  }
251
286
 
252
287
  // Show comparison if same asset on multiple providers
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env npx tsx
2
+ // Execute a HIP-4 outcome order on Hyperliquid
3
+
4
+ import { getClient } from '../core/client.js';
5
+ import { checkBuilderFeeApproval, formatUsd, parseArgs } from '../core/utils.js';
6
+
7
+ function printUsage() {
8
+ console.log(`
9
+ Open Broker - HIP-4 Outcome Order
10
+ =================================
11
+
12
+ Buy or sell a YES/NO outcome token.
13
+
14
+ Usage:
15
+ openbroker outcome-order --outcome <id|#encoding|+encoding> --outcome-side <yes|no> --side <buy|sell> --size <SIZE>
16
+
17
+ Options:
18
+ --outcome Outcome id, outcome spot coin (#1230), or token name (+1230)
19
+ --outcome-side Outcome side when --outcome is a plain id: yes/no or 0/1 (default: yes)
20
+ --side Trade side: buy or sell
21
+ --size Order size in outcome token units
22
+ --price Limit price between 0.001 and 0.999 (omit for market IOC)
23
+ --tif Time-in-force for limit orders: Gtc, Ioc, Alo (default: Gtc)
24
+ --slippage Slippage tolerance in bps for market orders (default: config, usually 50)
25
+ --sz-decimals Override size decimals if outcome metadata omits token decimals
26
+ --dry Dry run - show order details without executing
27
+ --verbose Show full API request/response for debugging
28
+
29
+ Examples:
30
+ openbroker outcomes --query BTC
31
+ openbroker outcome-order --outcome 123 --outcome-side yes --side buy --size 10 --dry
32
+ openbroker outcome-buy --outcome 123 --outcome-side no --size 5 --price 0.42
33
+ openbroker outcome-sell --outcome #1230 --size 10
34
+ `);
35
+ }
36
+
37
+ function formatOutcomePrice(price: number): string {
38
+ return price.toFixed(4);
39
+ }
40
+
41
+ async function main() {
42
+ const args = parseArgs(process.argv.slice(2));
43
+
44
+ if (args.help || args.h) {
45
+ printUsage();
46
+ process.exit(0);
47
+ }
48
+
49
+ const outcomeRef = args.outcome as string;
50
+ const outcomeSide = args['outcome-side'] as string | undefined;
51
+ const side = args.side as string;
52
+ const size = parseFloat(args.size as string);
53
+ const price = args.price ? parseFloat(args.price as string) : undefined;
54
+ const tif = (args.tif as 'Gtc' | 'Ioc' | 'Alo') ?? 'Gtc';
55
+ const slippage = args.slippage ? parseInt(args.slippage as string) : undefined;
56
+ const szDecimals = args['sz-decimals'] ? parseInt(args['sz-decimals'] as string, 10) : undefined;
57
+ const dryRun = args.dry as boolean;
58
+
59
+ if (!outcomeRef || !side || isNaN(size)) {
60
+ printUsage();
61
+ process.exit(1);
62
+ }
63
+
64
+ if (side !== 'buy' && side !== 'sell') {
65
+ console.error('Error: --side must be "buy" or "sell"');
66
+ process.exit(1);
67
+ }
68
+
69
+ if (size <= 0) {
70
+ console.error('Error: --size must be positive');
71
+ process.exit(1);
72
+ }
73
+
74
+ if (price !== undefined && (price <= 0 || price >= 1)) {
75
+ console.error('Error: --price must be between 0 and 1 for outcome tokens');
76
+ process.exit(1);
77
+ }
78
+
79
+ if (szDecimals !== undefined && (szDecimals < 0 || szDecimals > 8)) {
80
+ console.error('Error: --sz-decimals must be between 0 and 8');
81
+ process.exit(1);
82
+ }
83
+
84
+ const client = getClient();
85
+ if (args.verbose) client.verbose = true;
86
+
87
+ const isBuy = side === 'buy';
88
+ const isMarket = price === undefined;
89
+
90
+ console.log('Open Broker - HIP-4 Outcome Order');
91
+ console.log('=================================\n');
92
+
93
+ await checkBuilderFeeApproval(client);
94
+
95
+ try {
96
+ const resolved = client.resolveOutcomeRef(outcomeRef, outcomeSide);
97
+ const market = await client.getOutcomeMarket(resolved.outcome);
98
+ const marketSide = market?.sides.find((s) => s.side === resolved.side);
99
+ const sideName = marketSide?.name ?? (resolved.side === 0 ? 'Yes' : 'No');
100
+ const midPrice = await client.getOutcomeMidPrice(resolved.outcome, resolved.side);
101
+ const slippageBps = slippage ?? 50;
102
+ const limitPrice = isMarket
103
+ ? (isBuy ? midPrice * (1 + slippageBps / 10000) : midPrice * (1 - slippageBps / 10000))
104
+ : price;
105
+ const notional = midPrice * size;
106
+
107
+ console.log('Order Details');
108
+ console.log('-------------');
109
+ console.log(`Outcome: ${resolved.outcome}`);
110
+ console.log(`Market: ${market?.name ?? 'Unknown'}${market?.parsedDescription.underlying ? ` (${market.parsedDescription.underlying})` : ''}`);
111
+ if (market?.parsedDescription.expiry) console.log(`Expiry: ${market.parsedDescription.expiry}`);
112
+ if (market?.parsedDescription.targetPrice) console.log(`Target: ${market.parsedDescription.targetPrice}`);
113
+ console.log(`Outcome Side: ${sideName.toUpperCase()} (${resolved.side})`);
114
+ console.log(`Coin: ${resolved.coin}`);
115
+ console.log(`Asset ID: ${resolved.assetId}`);
116
+ console.log(`Trade Side: ${isBuy ? 'BUY' : 'SELL'}`);
117
+ console.log(`Size: ${size}`);
118
+ console.log(`Mid Price: ${formatOutcomePrice(midPrice)}`);
119
+ if (isMarket) {
120
+ console.log(`Type: Market (IOC)`);
121
+ console.log(`Limit Price: ${formatOutcomePrice(limitPrice)} (${slippageBps} bps slippage)`);
122
+ } else {
123
+ console.log(`Type: Limit (${tif})`);
124
+ console.log(`Limit Price: ${formatOutcomePrice(price)}`);
125
+ }
126
+ console.log(`Notional: ~${formatUsd(notional)}`);
127
+ if (marketSide?.szDecimals !== undefined || szDecimals !== undefined) {
128
+ console.log(`Sz Decimals: ${szDecimals ?? marketSide?.szDecimals}`);
129
+ }
130
+ console.log(`Builder Fee: ${client.builderInfo.f / 10} bps`);
131
+
132
+ if (dryRun) {
133
+ console.log('\nDry run - order not submitted');
134
+ return;
135
+ }
136
+
137
+ console.log('\nExecuting...');
138
+
139
+ const response = isMarket
140
+ ? await client.outcomeMarketOrder(outcomeRef, outcomeSide, isBuy, size, slippage, szDecimals)
141
+ : await client.outcomeLimitOrder(outcomeRef, outcomeSide, isBuy, size, price!, tif, szDecimals);
142
+
143
+ console.log('\nResult');
144
+ console.log('------');
145
+
146
+ if (args.verbose || process.env.VERBOSE) {
147
+ console.log('\nFull Response:');
148
+ console.log(JSON.stringify(response, null, 2));
149
+ }
150
+
151
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
152
+ const statuses = response.response.data.statuses;
153
+ for (const status of statuses) {
154
+ if (status.filled) {
155
+ const fillSz = parseFloat(status.filled.totalSz);
156
+ const avgPx = parseFloat(status.filled.avgPx);
157
+ console.log('Filled');
158
+ console.log(` Order ID: ${status.filled.oid}`);
159
+ console.log(` Size: ${fillSz}`);
160
+ console.log(` Avg Price: ${formatOutcomePrice(avgPx)}`);
161
+ console.log(` Notional: ${formatUsd(fillSz * avgPx)}`);
162
+ } else if (status.resting) {
163
+ console.log('Resting');
164
+ console.log(` Order ID: ${status.resting.oid}`);
165
+ } else if (status.error) {
166
+ console.log(`Error: ${status.error}`);
167
+ } else {
168
+ console.log('Unknown status:');
169
+ console.log(JSON.stringify(status, null, 2));
170
+ }
171
+ }
172
+ } else if (response.status === 'err') {
173
+ console.log(`API Error: ${response.response || JSON.stringify(response)}`);
174
+ } else {
175
+ console.log('Unexpected response:');
176
+ console.log(JSON.stringify(response, null, 2));
177
+ }
178
+ } catch (error) {
179
+ console.error('Error executing outcome order:', error);
180
+ console.error('Note: Hyperliquid currently documents outcomeMeta as testnet-only.');
181
+ process.exit(1);
182
+ }
183
+ }
184
+
185
+ main();