openbroker 1.0.75 → 1.0.79
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 +2 -2
- package/bin/cli.ts +16 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/core/client.ts +245 -0
- package/scripts/core/ws.ts +25 -0
- package/scripts/info/funding-history.ts +5 -5
- package/scripts/info/search-markets.ts +30 -6
- package/scripts/info/spot.ts +23 -8
- package/scripts/operations/spot-order.ts +189 -0
- package/scripts/plugin/tools.ts +126 -6
package/SKILL.md
CHANGED
|
@@ -4,8 +4,8 @@ description: Hyperliquid trading plugin with background position monitoring and
|
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Requires Node.js 22+, network access to api.hyperliquid.xyz
|
|
6
6
|
homepage: https://www.npmjs.com/package/openbroker
|
|
7
|
-
metadata: {"author": "monemetrics", "version": "1.0.
|
|
8
|
-
allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_twap ob_twap_cancel ob_twap_status ob_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
|
|
7
|
+
metadata: {"author": "monemetrics", "version": "1.0.79", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
|
|
8
|
+
allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_spot_buy ob_spot_sell ob_twap ob_twap_cancel ob_twap_status ob_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Open Broker - Hyperliquid Trading CLI
|
package/bin/cli.ts
CHANGED
|
@@ -47,6 +47,9 @@ const commands: Record<string, { script: string; description: string }> = {
|
|
|
47
47
|
'scale': { script: 'operations/scale.ts', description: 'Scale in/out orders' },
|
|
48
48
|
'bracket': { script: 'operations/bracket.ts', description: 'Bracket order (entry + TP + SL)' },
|
|
49
49
|
'chase': { script: 'operations/chase.ts', description: 'Chase order with ALO' },
|
|
50
|
+
'spot-buy': { script: 'operations/spot-order.ts', description: 'Spot buy order' },
|
|
51
|
+
'spot-sell': { script: 'operations/spot-order.ts', description: 'Spot sell order' },
|
|
52
|
+
'spot-order': { script: 'operations/spot-order.ts', description: 'Spot order (market or limit)' },
|
|
50
53
|
|
|
51
54
|
// Automations
|
|
52
55
|
'auto': { script: 'auto/cli.ts', description: 'Run/manage trading automations' },
|
|
@@ -88,6 +91,11 @@ Trading Commands:
|
|
|
88
91
|
tpsl Set TP/SL on existing position
|
|
89
92
|
cancel Cancel orders
|
|
90
93
|
|
|
94
|
+
Spot Trading:
|
|
95
|
+
spot-buy Spot buy order
|
|
96
|
+
spot-sell Spot sell order
|
|
97
|
+
spot-order Spot order (market or limit, specify --side)
|
|
98
|
+
|
|
91
99
|
Advanced Execution:
|
|
92
100
|
twap Native TWAP order (exchange-managed)
|
|
93
101
|
twap-cancel Cancel a running TWAP order
|
|
@@ -167,6 +175,14 @@ function main() {
|
|
|
167
175
|
runScript(commands['market'].script, ['--side', 'sell', ...commandArgs]);
|
|
168
176
|
return;
|
|
169
177
|
}
|
|
178
|
+
if (command === 'spot-buy') {
|
|
179
|
+
runScript(commands['spot-order'].script, ['--side', 'buy', ...commandArgs]);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (command === 'spot-sell') {
|
|
183
|
+
runScript(commands['spot-order'].script, ['--side', 'sell', ...commandArgs]);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
170
186
|
|
|
171
187
|
// Handle version
|
|
172
188
|
if (command === '--version' || command === '-v') {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/scripts/core/client.ts
CHANGED
|
@@ -37,6 +37,12 @@ export class HyperliquidClient {
|
|
|
37
37
|
private hip3MaxLeverageMap: Map<string, number> = new Map();
|
|
38
38
|
/** Cached account abstraction mode: 'standard' | 'unified' | 'portfolio' | 'dexAbstraction' */
|
|
39
39
|
private accountMode: string | null = null;
|
|
40
|
+
/** Spot asset index map: coin name → 10000 + spotMeta.universe[i].index */
|
|
41
|
+
private spotAssetMap: Map<string, number> = new Map();
|
|
42
|
+
/** Spot szDecimals map: coin name → base token szDecimals */
|
|
43
|
+
private spotSzDecimalsMap: Map<string, number> = new Map();
|
|
44
|
+
/** Whether spot metadata has been loaded */
|
|
45
|
+
private spotMetaLoaded: boolean = false;
|
|
40
46
|
public verbose: boolean = false;
|
|
41
47
|
|
|
42
48
|
constructor(config?: OpenBrokerConfig) {
|
|
@@ -412,6 +418,68 @@ export class HyperliquidClient {
|
|
|
412
418
|
};
|
|
413
419
|
}
|
|
414
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Load spot metadata into lookup maps.
|
|
423
|
+
* Spot asset index for orders = 10000 + universe[i].index
|
|
424
|
+
* Uses the base token's szDecimals for size rounding.
|
|
425
|
+
*/
|
|
426
|
+
private async loadSpotMeta(): Promise<void> {
|
|
427
|
+
if (this.spotMetaLoaded) return;
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const spotData = await this.getSpotMeta();
|
|
431
|
+
// Build token lookup for szDecimals
|
|
432
|
+
const tokenMap = new Map<number, { name: string; szDecimals: number }>();
|
|
433
|
+
for (const token of spotData.tokens) {
|
|
434
|
+
tokenMap.set(token.index, { name: token.name, szDecimals: token.szDecimals });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
for (const pair of spotData.universe) {
|
|
438
|
+
// pair.name is the market name (e.g., "PURR/USDC", "@107")
|
|
439
|
+
// pair.tokens = [baseTokenIndex, quoteTokenIndex]
|
|
440
|
+
// pair.index is the spot universe index
|
|
441
|
+
const baseToken = tokenMap.get(pair.tokens[0]);
|
|
442
|
+
if (!baseToken) continue;
|
|
443
|
+
|
|
444
|
+
const spotAssetIndex = 10000 + pair.index;
|
|
445
|
+
const quoteTokenIdx = pair.tokens[1];
|
|
446
|
+
|
|
447
|
+
// A token can appear in multiple pairs (e.g., HYPE/USDC, HYPE/USDE, HYPE/USDH).
|
|
448
|
+
// Prefer the USDC pair (quote token index 0) for the primary mapping.
|
|
449
|
+
const existing = this.spotAssetMap.get(baseToken.name);
|
|
450
|
+
if (existing !== undefined && quoteTokenIdx !== 0) {
|
|
451
|
+
// Already have a mapping — skip non-USDC pairs
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this.spotAssetMap.set(baseToken.name, spotAssetIndex);
|
|
456
|
+
this.spotSzDecimalsMap.set(baseToken.name, baseToken.szDecimals);
|
|
457
|
+
|
|
458
|
+
this.log(`Spot: ${baseToken.name} → asset ${spotAssetIndex} (szDecimals: ${baseToken.szDecimals})`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.spotMetaLoaded = true;
|
|
462
|
+
this.log(`Loaded ${this.spotAssetMap.size} spot markets`);
|
|
463
|
+
} catch (e) {
|
|
464
|
+
this.log('Failed to load spot metadata:', e);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Get the spot asset index for a coin, or undefined if not a spot asset */
|
|
469
|
+
getSpotAssetIndex(coin: string): number | undefined {
|
|
470
|
+
return this.spotAssetMap.get(coin);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Get spot szDecimals for a coin */
|
|
474
|
+
getSpotSzDecimals(coin: string): number | undefined {
|
|
475
|
+
return this.spotSzDecimalsMap.get(coin);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Get all loaded spot asset names */
|
|
479
|
+
getSpotAssetNames(): string[] {
|
|
480
|
+
return Array.from(this.spotAssetMap.keys());
|
|
481
|
+
}
|
|
482
|
+
|
|
415
483
|
/**
|
|
416
484
|
* Get user's spot token balances
|
|
417
485
|
*/
|
|
@@ -1598,6 +1666,183 @@ export class HyperliquidClient {
|
|
|
1598
1666
|
return results;
|
|
1599
1667
|
}
|
|
1600
1668
|
|
|
1669
|
+
// ============ Spot Trading ============
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Place a spot order.
|
|
1673
|
+
* Uses the same exchange.order() endpoint but with spot asset indices (10000 + spotIndex).
|
|
1674
|
+
* Spot orders have no leverage, no reduce-only, and builder fee max is 1000 (vs 100 for perps).
|
|
1675
|
+
*
|
|
1676
|
+
* @param coin - Base token symbol (e.g. "PURR", "HYPE")
|
|
1677
|
+
* @param isBuy - True to buy base token, false to sell
|
|
1678
|
+
* @param size - Size in base token units
|
|
1679
|
+
* @param price - Limit price in quote token (usually USDC)
|
|
1680
|
+
* @param orderType - Order type with time-in-force
|
|
1681
|
+
* @param includeBuilder - Whether to include builder fee (default: true)
|
|
1682
|
+
*/
|
|
1683
|
+
async spotOrder(
|
|
1684
|
+
coin: string,
|
|
1685
|
+
isBuy: boolean,
|
|
1686
|
+
size: number,
|
|
1687
|
+
price: number,
|
|
1688
|
+
orderType: { limit: { tif: 'Gtc' | 'Ioc' | 'Alo' } },
|
|
1689
|
+
includeBuilder: boolean = true,
|
|
1690
|
+
): Promise<OrderResponse> {
|
|
1691
|
+
this.requireTrading();
|
|
1692
|
+
await this.loadSpotMeta();
|
|
1693
|
+
|
|
1694
|
+
const assetIndex = this.spotAssetMap.get(coin);
|
|
1695
|
+
if (assetIndex === undefined) {
|
|
1696
|
+
throw new Error(
|
|
1697
|
+
`Unknown spot asset: ${coin}. Available: ${Array.from(this.spotAssetMap.keys()).slice(0, 15).join(', ')}...\n` +
|
|
1698
|
+
`Use "openbroker spot" to see all spot markets.`
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
const szDecimals = this.spotSzDecimalsMap.get(coin)!;
|
|
1703
|
+
|
|
1704
|
+
const orderWire = {
|
|
1705
|
+
a: assetIndex,
|
|
1706
|
+
b: isBuy,
|
|
1707
|
+
p: roundPrice(price, szDecimals, true),
|
|
1708
|
+
s: roundSize(size, szDecimals),
|
|
1709
|
+
r: false, // reduce-only not applicable for spot
|
|
1710
|
+
t: orderType,
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1713
|
+
this.log('Placing spot order:', JSON.stringify(orderWire, null, 2));
|
|
1714
|
+
|
|
1715
|
+
const orderRequest: {
|
|
1716
|
+
orders: typeof orderWire[];
|
|
1717
|
+
grouping: 'na';
|
|
1718
|
+
builder?: BuilderInfo;
|
|
1719
|
+
} = {
|
|
1720
|
+
orders: [orderWire],
|
|
1721
|
+
grouping: 'na',
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
// Add builder fee if configured (spot max is 1000 vs 100 for perps)
|
|
1725
|
+
if (includeBuilder && this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
|
|
1726
|
+
orderRequest.builder = this.builderInfo;
|
|
1727
|
+
this.log('Including builder fee:', this.builderInfo);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
try {
|
|
1731
|
+
const response = await this.exchange.order(orderRequest);
|
|
1732
|
+
this.log('Spot order response:', JSON.stringify(response, null, 2));
|
|
1733
|
+
return response as unknown as OrderResponse;
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
this.log('Spot order error:', error);
|
|
1736
|
+
return {
|
|
1737
|
+
status: 'err',
|
|
1738
|
+
response: error instanceof Error ? error.message : String(error),
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* Place a spot market order (IOC at slippage price).
|
|
1745
|
+
* @param coin - Base token symbol (e.g. "PURR", "HYPE")
|
|
1746
|
+
* @param isBuy - True to buy, false to sell
|
|
1747
|
+
* @param size - Size in base token units
|
|
1748
|
+
* @param slippageBps - Slippage tolerance in basis points (default: config value)
|
|
1749
|
+
*/
|
|
1750
|
+
async spotMarketOrder(
|
|
1751
|
+
coin: string,
|
|
1752
|
+
isBuy: boolean,
|
|
1753
|
+
size: number,
|
|
1754
|
+
slippageBps?: number,
|
|
1755
|
+
): Promise<OrderResponse> {
|
|
1756
|
+
await this.loadSpotMeta();
|
|
1757
|
+
|
|
1758
|
+
// Get the spot pair name (@index or PURR/USDC) for allMids lookup
|
|
1759
|
+
const assetIndex = this.spotAssetMap.get(coin);
|
|
1760
|
+
if (assetIndex === undefined) {
|
|
1761
|
+
throw new Error(`Unknown spot asset: ${coin}. Use "openbroker spot" to see available markets.`);
|
|
1762
|
+
}
|
|
1763
|
+
const spotPairIndex = assetIndex - 10000;
|
|
1764
|
+
// Canonical PURR/USDC is index 0, everything else uses @index
|
|
1765
|
+
const spotCoinKey = spotPairIndex === 0 ? 'PURR/USDC' : `@${spotPairIndex}`;
|
|
1766
|
+
|
|
1767
|
+
// Use allMids for accurate live prices (spotMetaAndAssetCtxs contexts can be misaligned)
|
|
1768
|
+
const mids = await this.getAllMids();
|
|
1769
|
+
const midStr = mids[spotCoinKey];
|
|
1770
|
+
const midPrice = midStr ? parseFloat(midStr) : 0;
|
|
1771
|
+
|
|
1772
|
+
if (!midPrice || midPrice === 0) {
|
|
1773
|
+
throw new Error(`No spot price for ${coin} (${spotCoinKey}). Check if the spot market exists with "openbroker spot --coin ${coin}".`);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Calculate slippage price
|
|
1777
|
+
const slippage = (slippageBps ?? this.config.slippageBps) / 10000;
|
|
1778
|
+
const limitPrice = isBuy
|
|
1779
|
+
? midPrice * (1 + slippage)
|
|
1780
|
+
: midPrice * (1 - slippage);
|
|
1781
|
+
|
|
1782
|
+
this.log(`Spot market order: ${coin} ${isBuy ? 'BUY' : 'SELL'} ${size} @ ${limitPrice} (mid: ${midPrice}, slippage: ${slippage * 100}%)`);
|
|
1783
|
+
|
|
1784
|
+
return this.spotOrder(
|
|
1785
|
+
coin,
|
|
1786
|
+
isBuy,
|
|
1787
|
+
size,
|
|
1788
|
+
limitPrice,
|
|
1789
|
+
{ limit: { tif: 'Ioc' } },
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Place a spot limit order.
|
|
1795
|
+
* @param coin - Base token symbol (e.g. "PURR", "HYPE")
|
|
1796
|
+
* @param isBuy - True to buy, false to sell
|
|
1797
|
+
* @param size - Size in base token units
|
|
1798
|
+
* @param price - Limit price in quote token (usually USDC)
|
|
1799
|
+
* @param tif - Time-in-force (default: Gtc)
|
|
1800
|
+
*/
|
|
1801
|
+
async spotLimitOrder(
|
|
1802
|
+
coin: string,
|
|
1803
|
+
isBuy: boolean,
|
|
1804
|
+
size: number,
|
|
1805
|
+
price: number,
|
|
1806
|
+
tif: 'Gtc' | 'Ioc' | 'Alo' = 'Gtc',
|
|
1807
|
+
): Promise<OrderResponse> {
|
|
1808
|
+
return this.spotOrder(
|
|
1809
|
+
coin,
|
|
1810
|
+
isBuy,
|
|
1811
|
+
size,
|
|
1812
|
+
price,
|
|
1813
|
+
{ limit: { tif } },
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Cancel a spot order by coin and order ID.
|
|
1819
|
+
*/
|
|
1820
|
+
async spotCancel(coin: string, oid: number): Promise<CancelResponse> {
|
|
1821
|
+
this.requireTrading();
|
|
1822
|
+
await this.loadSpotMeta();
|
|
1823
|
+
|
|
1824
|
+
const assetIndex = this.spotAssetMap.get(coin);
|
|
1825
|
+
if (assetIndex === undefined) {
|
|
1826
|
+
throw new Error(`Unknown spot asset: ${coin}`);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
this.log(`Cancelling spot order: ${coin} (asset ${assetIndex}) oid ${oid}`);
|
|
1830
|
+
|
|
1831
|
+
try {
|
|
1832
|
+
const response = await this.exchange.cancel({
|
|
1833
|
+
cancels: [{ a: assetIndex, o: oid }],
|
|
1834
|
+
});
|
|
1835
|
+
this.log('Spot cancel response:', JSON.stringify(response, null, 2));
|
|
1836
|
+
return response as unknown as CancelResponse;
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
this.log('Spot cancel error:', error);
|
|
1839
|
+
return {
|
|
1840
|
+
status: 'err',
|
|
1841
|
+
response: { type: 'cancel', data: { statuses: [error instanceof Error ? error.message : String(error)] } },
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1601
1846
|
// ============ Leverage ============
|
|
1602
1847
|
|
|
1603
1848
|
async updateLeverage(
|
package/scripts/core/ws.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { WebSocketTransport, SubscriptionClient } from '@nktkas/hyperliquid';
|
|
|
5
5
|
import type { ISubscription } from '@nktkas/hyperliquid';
|
|
6
6
|
import type {
|
|
7
7
|
AllMidsWsEvent,
|
|
8
|
+
L2BookWsEvent,
|
|
8
9
|
OrderUpdatesWsEvent,
|
|
9
10
|
UserFillsWsEvent,
|
|
10
11
|
UserEventsWsEvent,
|
|
@@ -14,6 +15,15 @@ import { isMainnet } from './config.js';
|
|
|
14
15
|
// ── Event types ────────────────────────────────────────────────────
|
|
15
16
|
|
|
16
17
|
export interface WsEventMap {
|
|
18
|
+
/** L2 order book snapshot for a specific coin */
|
|
19
|
+
l2Book: {
|
|
20
|
+
coin: string;
|
|
21
|
+
time: number;
|
|
22
|
+
levels: [
|
|
23
|
+
Array<{ px: string; sz: string; n: number }>,
|
|
24
|
+
Array<{ px: string; sz: string; n: number }>,
|
|
25
|
+
];
|
|
26
|
+
};
|
|
17
27
|
/** Mid prices for all assets updated */
|
|
18
28
|
allMids: { mids: Record<string, string> };
|
|
19
29
|
/** Order status changed (filled, canceled, rejected, etc.) */
|
|
@@ -174,6 +184,21 @@ export class WebSocketManager {
|
|
|
174
184
|
return this.trackSub(sub);
|
|
175
185
|
}
|
|
176
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Subscribe to L2 order book snapshots for a specific coin.
|
|
189
|
+
*/
|
|
190
|
+
async subscribeL2Book(coin: string): Promise<ISubscription> {
|
|
191
|
+
const client = this.ensureClient();
|
|
192
|
+
const sub = await client.l2Book({ coin }, (data: L2BookWsEvent) => {
|
|
193
|
+
this.emit('l2Book', {
|
|
194
|
+
coin: data.coin,
|
|
195
|
+
time: data.time,
|
|
196
|
+
levels: data.levels,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
return this.trackSub(sub);
|
|
200
|
+
}
|
|
201
|
+
|
|
177
202
|
// ── User subscriptions ────────────────────────────────────────
|
|
178
203
|
|
|
179
204
|
/**
|
|
@@ -73,9 +73,9 @@ async function main() {
|
|
|
73
73
|
|
|
74
74
|
console.log(
|
|
75
75
|
time.padEnd(20) +
|
|
76
|
-
formatPercent(rate
|
|
77
|
-
formatPercent(annualized
|
|
78
|
-
formatPercent(premium
|
|
76
|
+
formatPercent(rate, 6).padEnd(16) +
|
|
77
|
+
formatPercent(annualized).padEnd(14) +
|
|
78
|
+
formatPercent(premium, 4)
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -85,8 +85,8 @@ async function main() {
|
|
|
85
85
|
|
|
86
86
|
console.log('─'.repeat(60));
|
|
87
87
|
console.log(`Samples: ${history.length}`);
|
|
88
|
-
console.log(`Avg Hourly Rate: ${formatPercent(avgRate
|
|
89
|
-
console.log(`Avg Annualized: ${formatPercent(avgAnnualized
|
|
88
|
+
console.log(`Avg Hourly Rate: ${formatPercent(avgRate, 6)}`);
|
|
89
|
+
console.log(`Avg Annualized: ${formatPercent(avgAnnualized)}`);
|
|
90
90
|
|
|
91
91
|
if (avgRate > 0) {
|
|
92
92
|
console.log('Longs pay shorts');
|
|
@@ -157,16 +157,40 @@ async function main() {
|
|
|
157
157
|
if (args.type === 'all' || args.type === 'spot') {
|
|
158
158
|
try {
|
|
159
159
|
const spotData = await client.getSpotMetaAndAssetCtxs();
|
|
160
|
-
for (let i = 0; i < spotData.meta.universe.length; i++) {
|
|
161
|
-
const pair = spotData.meta.universe[i];
|
|
162
|
-
const ctx = spotData.assetCtxs[i];
|
|
163
|
-
if (!pair || !ctx) continue;
|
|
164
160
|
|
|
165
|
-
|
|
161
|
+
// Build token index → name lookup for matching by base token name
|
|
162
|
+
const tokenNameMap = new Map<number, string>();
|
|
163
|
+
for (const token of spotData.meta.tokens) {
|
|
164
|
+
tokenNameMap.set(token.index, token.name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build ctx map by coin name (contexts have a 'coin' field that matches pair.name).
|
|
168
|
+
// The contexts array can be longer than universe and is NOT aligned by index.
|
|
169
|
+
const ctxMap = new Map<string, (typeof spotData.assetCtxs)[number]>();
|
|
170
|
+
for (const ctx of spotData.assetCtxs) {
|
|
171
|
+
if ((ctx as Record<string, unknown>).coin) {
|
|
172
|
+
ctxMap.set((ctx as Record<string, unknown>).coin as string, ctx);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const pair of spotData.meta.universe) {
|
|
177
|
+
if (!pair) continue;
|
|
178
|
+
const ctx = ctxMap.get(pair.name);
|
|
179
|
+
if (!ctx) continue;
|
|
180
|
+
|
|
181
|
+
// Match against pair name, base token name, and quote token name
|
|
182
|
+
const baseTokenName = tokenNameMap.get(pair.tokens[0]) ?? '';
|
|
183
|
+
const quoteTokenName = tokenNameMap.get(pair.tokens[1]) ?? '';
|
|
184
|
+
const searchable = `${pair.name} ${baseTokenName} ${quoteTokenName}`.toUpperCase();
|
|
185
|
+
|
|
186
|
+
if (searchable.includes(query)) {
|
|
187
|
+
const displayName = baseTokenName && quoteTokenName
|
|
188
|
+
? `${baseTokenName}/${quoteTokenName}`
|
|
189
|
+
: pair.name;
|
|
166
190
|
results.push({
|
|
167
191
|
type: 'spot',
|
|
168
192
|
provider: 'Spot',
|
|
169
|
-
coin:
|
|
193
|
+
coin: displayName,
|
|
170
194
|
price: ctx.markPx,
|
|
171
195
|
volume24h: parseFloat(ctx.dayNtlVlm || '0'),
|
|
172
196
|
});
|
package/scripts/info/spot.ts
CHANGED
|
@@ -127,14 +127,29 @@ async function main() {
|
|
|
127
127
|
|
|
128
128
|
const markets: SpotMarket[] = [];
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
// Build ctx map by coin name — the contexts array is NOT aligned with universe by index.
|
|
131
|
+
// Each context has a 'coin' field matching pair.name.
|
|
132
|
+
const ctxMap = new Map<string, { markPx: string; prevDayPx: string; dayNtlVlm: string; midPx: string }>();
|
|
133
|
+
for (const ctx of spotData.assetCtxs as Array<{ coin?: string; markPx: string; prevDayPx: string; dayNtlVlm: string; midPx: string }>) {
|
|
134
|
+
if (ctx.coin) ctxMap.set(ctx.coin, ctx);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Build token name map for filtering by base token name
|
|
138
|
+
const tokenNameMap = new Map<number, string>();
|
|
139
|
+
for (const token of spotData.meta.tokens) {
|
|
140
|
+
tokenNameMap.set(token.index, token.name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const pair of spotData.meta.universe) {
|
|
144
|
+
if (!pair) continue;
|
|
145
|
+
const ctx = ctxMap.get(pair.name);
|
|
146
|
+
if (!ctx) continue;
|
|
147
|
+
|
|
148
|
+
// Filter by coin — match pair name or base token name
|
|
149
|
+
if (args.coin) {
|
|
150
|
+
const baseTokenName = tokenNameMap.get(pair.tokens[0]) ?? '';
|
|
151
|
+
const searchable = `${pair.name} ${baseTokenName}`.toUpperCase();
|
|
152
|
+
if (!searchable.includes(args.coin)) continue;
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
markets.push({
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Execute a spot order on Hyperliquid
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, parseArgs, checkBuilderFeeApproval } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Open Broker - Spot Order
|
|
10
|
+
========================
|
|
11
|
+
|
|
12
|
+
Buy or sell spot tokens on Hyperliquid.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
npx tsx scripts/operations/spot-order.ts --coin <COIN> --side <buy|sell> --size <SIZE>
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--coin Base token to trade (e.g., PURR, HYPE)
|
|
19
|
+
--side Order side: buy or sell
|
|
20
|
+
--size Order size in base token units
|
|
21
|
+
--price Limit price (omit for market order)
|
|
22
|
+
--tif Time-in-force for limit orders: Gtc, Ioc, Alo (default: Gtc)
|
|
23
|
+
--slippage Slippage tolerance in bps for market orders (default: from config, usually 50 = 0.5%)
|
|
24
|
+
--dry Dry run - show order details without executing
|
|
25
|
+
--verbose Show full API request/response for debugging
|
|
26
|
+
|
|
27
|
+
Environment:
|
|
28
|
+
HYPERLIQUID_PRIVATE_KEY Your wallet private key (0x...)
|
|
29
|
+
HYPERLIQUID_NETWORK "mainnet" or "testnet" (default: mainnet)
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
npx tsx scripts/operations/spot-order.ts --coin PURR --side buy --size 1000
|
|
33
|
+
npx tsx scripts/operations/spot-order.ts --coin HYPE --side sell --size 50 --price 25.50
|
|
34
|
+
npx tsx scripts/operations/spot-order.ts --coin PURR --side buy --size 500 --dry
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const args = parseArgs(process.argv.slice(2));
|
|
40
|
+
|
|
41
|
+
const coin = args.coin as string;
|
|
42
|
+
const side = args.side as string;
|
|
43
|
+
const size = parseFloat(args.size as string);
|
|
44
|
+
const price = args.price ? parseFloat(args.price as string) : undefined;
|
|
45
|
+
const tif = (args.tif as 'Gtc' | 'Ioc' | 'Alo') ?? 'Gtc';
|
|
46
|
+
const slippage = args.slippage ? parseInt(args.slippage as string) : undefined;
|
|
47
|
+
const dryRun = args.dry as boolean;
|
|
48
|
+
|
|
49
|
+
if (!coin || !side || isNaN(size)) {
|
|
50
|
+
printUsage();
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (side !== 'buy' && side !== 'sell') {
|
|
55
|
+
console.error('Error: --side must be "buy" or "sell"');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (size <= 0) {
|
|
60
|
+
console.error('Error: --size must be positive');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isBuy = side === 'buy';
|
|
65
|
+
const client = getClient();
|
|
66
|
+
|
|
67
|
+
if (args.verbose) {
|
|
68
|
+
client.verbose = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log('Open Broker - Spot Order');
|
|
72
|
+
console.log('========================\n');
|
|
73
|
+
|
|
74
|
+
await checkBuilderFeeApproval(client);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Load spot metadata to get the pair index, then use allMids for accurate price
|
|
78
|
+
const spotMeta = await client.getSpotMeta();
|
|
79
|
+
const tokenMap = new Map<number, string>();
|
|
80
|
+
for (const t of spotMeta.tokens) tokenMap.set(t.index, t.name);
|
|
81
|
+
|
|
82
|
+
// Find the USDC-quoted pair for this coin (prefer quote token 0 = USDC)
|
|
83
|
+
let pairName = '';
|
|
84
|
+
let spotCoinKey = '';
|
|
85
|
+
for (const pair of spotMeta.universe) {
|
|
86
|
+
const baseName = tokenMap.get(pair.tokens[0]) ?? '';
|
|
87
|
+
if (baseName.toUpperCase() !== coin.toUpperCase()) continue;
|
|
88
|
+
const quoteName = tokenMap.get(pair.tokens[1]) ?? 'USDC';
|
|
89
|
+
// Prefer USDC pair; if already found a USDC pair, skip non-USDC pairs
|
|
90
|
+
if (pairName && pair.tokens[1] !== 0) continue;
|
|
91
|
+
pairName = `${baseName}/${quoteName}`;
|
|
92
|
+
spotCoinKey = pair.name; // "@107" or "PURR/USDC"
|
|
93
|
+
if (pair.tokens[1] === 0) break; // USDC pair found, stop
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!spotCoinKey) {
|
|
97
|
+
console.error(`Error: No spot market found for ${coin}`);
|
|
98
|
+
console.error('Use "openbroker spot" to see available spot markets.');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Use allMids for live price (spotMetaAndAssetCtxs contexts can be misaligned)
|
|
103
|
+
const mids = await client.getAllMids();
|
|
104
|
+
const midPrice = parseFloat(mids[spotCoinKey] || '0');
|
|
105
|
+
|
|
106
|
+
if (!midPrice || midPrice === 0) {
|
|
107
|
+
console.error(`Error: No spot price for ${coin} (${spotCoinKey})`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isMarket = price === undefined;
|
|
112
|
+
const slippageBps = slippage ?? 50;
|
|
113
|
+
const displayPrice = isMarket
|
|
114
|
+
? (isBuy ? midPrice * (1 + slippageBps / 10000) : midPrice * (1 - slippageBps / 10000))
|
|
115
|
+
: price;
|
|
116
|
+
const notional = midPrice * size;
|
|
117
|
+
|
|
118
|
+
console.log('Order Details');
|
|
119
|
+
console.log('-------------');
|
|
120
|
+
console.log(`Pair: ${pairName}`);
|
|
121
|
+
console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
|
|
122
|
+
console.log(`Size: ${size} ${coin}`);
|
|
123
|
+
console.log(`Mid Price: ${formatUsd(midPrice)}`);
|
|
124
|
+
if (isMarket) {
|
|
125
|
+
console.log(`Type: Market (IOC)`);
|
|
126
|
+
console.log(`Limit Price: ${formatUsd(displayPrice)} (${slippageBps} bps slippage)`);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`Type: Limit (${tif})`);
|
|
129
|
+
console.log(`Limit Price: ${formatUsd(price)}`);
|
|
130
|
+
}
|
|
131
|
+
console.log(`Notional: ~${formatUsd(notional)}`);
|
|
132
|
+
console.log(`Builder Fee: ${client.builderInfo.f / 10} bps`);
|
|
133
|
+
|
|
134
|
+
if (dryRun) {
|
|
135
|
+
console.log('\n🔍 Dry run - order not submitted');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log('\nExecuting...');
|
|
140
|
+
|
|
141
|
+
const response = isMarket
|
|
142
|
+
? await client.spotMarketOrder(coin, isBuy, size, slippage)
|
|
143
|
+
: await client.spotLimitOrder(coin, isBuy, size, price!, tif);
|
|
144
|
+
|
|
145
|
+
console.log('\nResult');
|
|
146
|
+
console.log('------');
|
|
147
|
+
|
|
148
|
+
if (args.verbose || process.env.VERBOSE) {
|
|
149
|
+
console.log('\nFull Response:');
|
|
150
|
+
console.log(JSON.stringify(response, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
154
|
+
const statuses = response.response.data.statuses;
|
|
155
|
+
for (const status of statuses) {
|
|
156
|
+
if (status.filled) {
|
|
157
|
+
const fillSz = parseFloat(status.filled.totalSz);
|
|
158
|
+
const avgPx = parseFloat(status.filled.avgPx);
|
|
159
|
+
const fillNotional = fillSz * avgPx;
|
|
160
|
+
|
|
161
|
+
console.log(`✅ Filled`);
|
|
162
|
+
console.log(` Order ID: ${status.filled.oid}`);
|
|
163
|
+
console.log(` Size: ${fillSz} ${coin}`);
|
|
164
|
+
console.log(` Avg Price: ${formatUsd(avgPx)}`);
|
|
165
|
+
console.log(` Notional: ${formatUsd(fillNotional)}`);
|
|
166
|
+
} else if (status.resting) {
|
|
167
|
+
console.log(`⏳ Resting`);
|
|
168
|
+
console.log(` Order ID: ${status.resting.oid}`);
|
|
169
|
+
} else if (status.error) {
|
|
170
|
+
console.log(`❌ Error: ${status.error}`);
|
|
171
|
+
} else {
|
|
172
|
+
console.log(`⚠️ Unknown status:`);
|
|
173
|
+
console.log(JSON.stringify(status, null, 2));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else if (response.status === 'err') {
|
|
177
|
+
console.log(`❌ API Error: ${response.response || JSON.stringify(response)}`);
|
|
178
|
+
} else {
|
|
179
|
+
console.log(`❌ Unexpected response:`);
|
|
180
|
+
console.log(JSON.stringify(response, null, 2));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Error executing spot order:', error);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
main();
|
package/scripts/plugin/tools.ts
CHANGED
|
@@ -398,14 +398,27 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
|
|
|
398
398
|
if (!typeFilter || typeFilter === 'spot') {
|
|
399
399
|
try {
|
|
400
400
|
const spotData = await client.getSpotMetaAndAssetCtxs();
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
401
|
+
// Build ctx map by coin name — contexts are NOT aligned with universe by index
|
|
402
|
+
const ctxMap = new Map<string, Record<string, string>>();
|
|
403
|
+
for (const ctx of spotData.assetCtxs as Array<Record<string, string>>) {
|
|
404
|
+
if (ctx.coin) ctxMap.set(ctx.coin, ctx);
|
|
405
|
+
}
|
|
406
|
+
// Build token name map
|
|
407
|
+
const tMap = new Map<number, string>();
|
|
408
|
+
for (const t of spotData.meta.tokens) tMap.set(t.index, t.name);
|
|
409
|
+
|
|
410
|
+
for (const pair of spotData.meta.universe) {
|
|
411
|
+
const baseName = tMap.get(pair.tokens[0]) ?? '';
|
|
412
|
+
const quoteName = tMap.get(pair.tokens[1]) ?? '';
|
|
413
|
+
const searchable = `${pair.name} ${baseName} ${quoteName}`.toUpperCase();
|
|
414
|
+
if (searchable.includes(query)) {
|
|
415
|
+
const ctx = ctxMap.get(pair.name);
|
|
416
|
+
const displayName = baseName && quoteName ? `${baseName}/${quoteName}` : pair.name;
|
|
404
417
|
results.push({
|
|
405
|
-
coin:
|
|
418
|
+
coin: displayName,
|
|
406
419
|
type: 'spot',
|
|
407
|
-
markPx:
|
|
408
|
-
dayVolume:
|
|
420
|
+
markPx: ctx?.markPx,
|
|
421
|
+
dayVolume: ctx?.dayNtlVlm,
|
|
409
422
|
});
|
|
410
423
|
}
|
|
411
424
|
}
|
|
@@ -1193,6 +1206,113 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
|
|
|
1193
1206
|
},
|
|
1194
1207
|
},
|
|
1195
1208
|
|
|
1209
|
+
{
|
|
1210
|
+
name: 'ob_spot_buy',
|
|
1211
|
+
description: 'Buy spot tokens on Hyperliquid. Always use dry=true first to preview.',
|
|
1212
|
+
parameters: {
|
|
1213
|
+
type: 'object',
|
|
1214
|
+
properties: {
|
|
1215
|
+
coin: { type: 'string', description: 'Base token symbol (PURR, HYPE, etc.)' },
|
|
1216
|
+
size: { type: 'number', description: 'Order size in base token units' },
|
|
1217
|
+
price: { type: 'number', description: 'Limit price (omit for market order)' },
|
|
1218
|
+
tif: { type: 'string', description: 'Time-in-force for limit: Gtc, Ioc, Alo (default: Gtc)' },
|
|
1219
|
+
slippage: { type: 'number', description: 'Slippage tolerance in bps for market orders (default: 50)' },
|
|
1220
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
1221
|
+
},
|
|
1222
|
+
required: ['coin', 'size'],
|
|
1223
|
+
},
|
|
1224
|
+
async execute(_id, params) {
|
|
1225
|
+
const { getClient } = await import('../core/client.js');
|
|
1226
|
+
const { formatUsd } = await import('../core/utils.js');
|
|
1227
|
+
const client = getClient();
|
|
1228
|
+
|
|
1229
|
+
if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
|
|
1230
|
+
|
|
1231
|
+
const coin = (params.coin as string).toUpperCase();
|
|
1232
|
+
const size = params.size as number;
|
|
1233
|
+
const price = params.price as number | undefined;
|
|
1234
|
+
const isMarket = price === undefined;
|
|
1235
|
+
|
|
1236
|
+
if (params.dry) {
|
|
1237
|
+
// Use allMids for accurate spot price preview
|
|
1238
|
+
await client.getMetaAndAssetCtxs(); // ensure spot meta loaded
|
|
1239
|
+
const spotIdx = client.getSpotAssetIndex(coin);
|
|
1240
|
+
const mids = await client.getAllMids();
|
|
1241
|
+
const spotKey = spotIdx !== undefined ? (spotIdx === 10000 ? 'PURR/USDC' : `@${spotIdx - 10000}`) : '';
|
|
1242
|
+
const midPrice = parseFloat(mids[spotKey] || '0');
|
|
1243
|
+
return json({
|
|
1244
|
+
dryRun: true,
|
|
1245
|
+
action: 'spot_buy',
|
|
1246
|
+
coin,
|
|
1247
|
+
size,
|
|
1248
|
+
type: isMarket ? 'market' : 'limit',
|
|
1249
|
+
midPrice,
|
|
1250
|
+
price: price ?? midPrice,
|
|
1251
|
+
notional: formatUsd(midPrice * size),
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const result = isMarket
|
|
1256
|
+
? await client.spotMarketOrder(coin, true, size, params.slippage as number | undefined)
|
|
1257
|
+
: await client.spotLimitOrder(coin, true, size, price!, (params.tif as 'Gtc' | 'Ioc' | 'Alo') ?? 'Gtc');
|
|
1258
|
+
|
|
1259
|
+
return json({ action: 'spot_buy', coin, size, type: isMarket ? 'market' : 'limit', result });
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
|
|
1263
|
+
{
|
|
1264
|
+
name: 'ob_spot_sell',
|
|
1265
|
+
description: 'Sell spot tokens on Hyperliquid. Always use dry=true first to preview.',
|
|
1266
|
+
parameters: {
|
|
1267
|
+
type: 'object',
|
|
1268
|
+
properties: {
|
|
1269
|
+
coin: { type: 'string', description: 'Base token symbol (PURR, HYPE, etc.)' },
|
|
1270
|
+
size: { type: 'number', description: 'Order size in base token units' },
|
|
1271
|
+
price: { type: 'number', description: 'Limit price (omit for market order)' },
|
|
1272
|
+
tif: { type: 'string', description: 'Time-in-force for limit: Gtc, Ioc, Alo (default: Gtc)' },
|
|
1273
|
+
slippage: { type: 'number', description: 'Slippage tolerance in bps for market orders (default: 50)' },
|
|
1274
|
+
dry: { type: 'boolean', description: 'Preview without executing' },
|
|
1275
|
+
},
|
|
1276
|
+
required: ['coin', 'size'],
|
|
1277
|
+
},
|
|
1278
|
+
async execute(_id, params) {
|
|
1279
|
+
const { getClient } = await import('../core/client.js');
|
|
1280
|
+
const { formatUsd } = await import('../core/utils.js');
|
|
1281
|
+
const client = getClient();
|
|
1282
|
+
|
|
1283
|
+
if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
|
|
1284
|
+
|
|
1285
|
+
const coin = (params.coin as string).toUpperCase();
|
|
1286
|
+
const size = params.size as number;
|
|
1287
|
+
const price = params.price as number | undefined;
|
|
1288
|
+
const isMarket = price === undefined;
|
|
1289
|
+
|
|
1290
|
+
if (params.dry) {
|
|
1291
|
+
await client.getMetaAndAssetCtxs();
|
|
1292
|
+
const spotIdx = client.getSpotAssetIndex(coin);
|
|
1293
|
+
const mids = await client.getAllMids();
|
|
1294
|
+
const spotKey = spotIdx !== undefined ? (spotIdx === 10000 ? 'PURR/USDC' : `@${spotIdx - 10000}`) : '';
|
|
1295
|
+
const midPrice = parseFloat(mids[spotKey] || '0');
|
|
1296
|
+
return json({
|
|
1297
|
+
dryRun: true,
|
|
1298
|
+
action: 'spot_sell',
|
|
1299
|
+
coin,
|
|
1300
|
+
size,
|
|
1301
|
+
type: isMarket ? 'market' : 'limit',
|
|
1302
|
+
midPrice,
|
|
1303
|
+
price: price ?? midPrice,
|
|
1304
|
+
notional: formatUsd(midPrice * size),
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const result = isMarket
|
|
1309
|
+
? await client.spotMarketOrder(coin, false, size, params.slippage as number | undefined)
|
|
1310
|
+
: await client.spotLimitOrder(coin, false, size, price!, (params.tif as 'Gtc' | 'Ioc' | 'Alo') ?? 'Gtc');
|
|
1311
|
+
|
|
1312
|
+
return json({ action: 'spot_sell', coin, size, type: isMarket ? 'market' : 'limit', result });
|
|
1313
|
+
},
|
|
1314
|
+
},
|
|
1315
|
+
|
|
1196
1316
|
{
|
|
1197
1317
|
name: 'ob_cancel',
|
|
1198
1318
|
description: 'Cancel open orders on Hyperliquid',
|