dsh-hyperliquid 0.0.1 → 0.1.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.
package/lib/tools.js ADDED
@@ -0,0 +1,411 @@
1
+ /**
2
+ * dsh-hyperliquid — agent tools. Every trading operation the desk exposes to
3
+ * the model routes through the SAME HyperRemote methods the UI would use
4
+ * (one operation, two callers — the UI simply chooses not to render manual
5
+ * controls), so "所有下单操作都是 dsh 会话进行" holds by construction.
6
+ * @module lib/tools.js
7
+ */
8
+
9
+ import { computeIndicators } from './indicators.js'
10
+ import { canonicalMarketCoin } from './market-symbol.js'
11
+
12
+ /** Resolve the calling session id from a tool run context. */
13
+ function sessionIdOf(exec) {
14
+ const agent = exec?.agent
15
+ return agent?.id ?? agent?.session?.id ?? null
16
+ }
17
+
18
+ function fmt(n, d = 2) {
19
+ if (n == null) return '—'
20
+ const v = Number(n)
21
+ return Number.isFinite(v) ? v.toLocaleString('en-US', { maximumFractionDigits: d }) : '—'
22
+ }
23
+
24
+ function shortSide(isBuy) { return isBuy ? 'BUY/LONG' : 'SELL/SHORT' }
25
+
26
+ export function buildToolDefinitions(remote) {
27
+ return [
28
+ {
29
+ name: 'hyper_context',
30
+ description: '一次读取本会话交易上下文:当前币对/周期、价格、盘口和成交、资金费、K 线、账户权益与仓位/挂单、数据时间和风险限额。做交易判断前优先调用。',
31
+ parameters: { type: 'object', properties: { coin: { type: 'string', description: '可选;默认本会话绑定币对或当前交易台币对' } }, additionalProperties: false },
32
+ output: { schema: { type: 'object', additionalProperties: true }, render: (_a, v) => [{ type: 'text', text: v.text }] },
33
+ timeoutMs: 20_000,
34
+ async execute(args, exec) {
35
+ const sessionId = sessionIdOf(exec)
36
+ const row = sessionId ? remote.store.getSession(sessionId) : null
37
+ const coin = canonicalMarketCoin(args?.coin ?? row?.coin ?? remote.desk?.coin)
38
+ if (!coin) throw new Error('无法确定会话币对,请提供 coin')
39
+ const workspace = row ? remote.store.getSessionWorkspace?.(sessionId) ?? null : null
40
+ const interval = workspace?.interval ?? remote.desk?.interval ?? '15m'
41
+ const [markets, snap, account, candleResult, status] = await Promise.all([
42
+ remote.markets(), remote.snapshot({ coin }), remote.account({ coin }), remote.candles({ coin, interval }), remote.status(),
43
+ ])
44
+ const coreMarket = markets.universe.find((m) => m.name === coin)
45
+ const detail = coreMarket ? null : await remote.marketDetail({ coin })
46
+ const market = coreMarket ?? detail?.market
47
+ if (!market) throw new Error(`未知市场 ${coin}`)
48
+ const bid = snap.book?.levels?.[0]?.[0]
49
+ const ask = snap.book?.levels?.[1]?.[0]
50
+ const candles = candleResult.candles.slice(-24)
51
+ const tradable = Boolean(coreMarket)
52
+ const position = account.positions.find((p) => p.coin === coin) ?? null
53
+ const mid = Number(snap.mids?.mids?.[coin] ?? snap.mids?.[coin])
54
+ const midFresh = tradable && snap.observedAt?.mids && Date.now() - snap.observedAt.mids < 10_000 && Number.isFinite(mid) && mid > 0
55
+ const mark = market.ctx?.markPx != null ? Number(market.ctx.markPx) : null
56
+ const change24hPct = Number(market.ctx?.prevDayPx) > 0 && mark != null ? (mark / Number(market.ctx.prevDayPx) - 1) * 100 : null
57
+ const context = {
58
+ workspace: workspace ? { interval: workspace.interval, bookGrouping: workspace.bookGrouping,
59
+ chartTab: workspace.chartTab, indicators: workspace.indicators, priceScale: workspace.priceScale } : null,
60
+ sessionId, coin, interval, marketKind: detail?.market?.kind ?? 'perp', tradable, network: markets.network, mode: account.mode,
61
+ mark, mid: midFresh ? mid : null, change24hPct, funding1h: market.ctx?.funding != null ? Number(market.ctx.funding) : null,
62
+ volume24hUsd: market.ctx?.dayNtlVlm != null ? Number(market.ctx.dayNtlVlm) : null,
63
+ bid: bid ? { px: Number(bid.px), sz: Number(bid.sz) } : null,
64
+ ask: ask ? { px: Number(ask.px), sz: Number(ask.sz) } : null,
65
+ accountMode: account.accountMode ?? null,
66
+ balanceSource: account.balanceSource ?? null,
67
+ accountDex: account.accountDex ?? '',
68
+ positionsCoverage: account.positionsCoverage ?? null,
69
+ accountValue: account.mode === 'paper' ? account.equity : account.balanceSource === 'spot' ? account.usdcBalance : Number(account.marginSummary?.accountValue),
70
+ available: account.mode === 'paper' ? account.availableMargin : account.balanceSource === 'spot'
71
+ ? account.tradeCapacity?.coin === coin ? Math.min(...account.tradeCapacity.availableToTrade.map(Number)) : null
72
+ : account.withdrawable,
73
+ position, positions: account.positions, openOrders: (account.openOrders ?? []).slice(0, 20), candles,
74
+ recentTrades: (Array.isArray(snap.trades) ? snap.trades : []).slice(0, 10),
75
+ bookDepth: { bids: (snap.book?.levels?.[0] ?? []).slice(0, 5), asks: (snap.book?.levels?.[1] ?? []).slice(0, 5) },
76
+ riskLimits: status.riskLimits, autonomousLive: status.autonomousLive, walletConfigured: status.walletConfigured,
77
+ observedAt: { ...snap.observedAt, market: detail?.observedAt ?? markets.observedAt ?? null }, ws: snap.ws, assembledAt: Date.now(),
78
+ }
79
+ const bookAge = context.observedAt?.book ? Math.round((context.assembledAt - context.observedAt.book) / 1000) : null
80
+ const midsAge = context.observedAt?.mids ? Math.round((context.assembledAt - context.observedAt.mids) / 1000) : null
81
+ const marketAge = context.observedAt?.market ? Math.round((context.assembledAt - context.observedAt.market) / 1000) : null
82
+ const freshness = tradable
83
+ ? bookAge == null || midsAge == null || bookAge > 10 || midsAge > 10 ? '数据过期/缺失,停止交易并重新读取' : '数据新鲜'
84
+ : bookAge == null || marketAge == null || bookAge > 10 || marketAge > 60 ? '只读行情过期/缺失' : '只读行情可用'
85
+ const text = [
86
+ `${coin} ${interval} · ${detail?.market?.kind ?? 'perp'}${tradable ? '' : '(只读市场)'} · ${account.mode}/${markets.network} · 读取 ${new Date(context.assembledAt).toISOString()} · 盘口 ${bookAge ?? '?'}s / ${tradable ? 'mid ' + (midsAge ?? '?') : '市场上下文 ' + (marketAge ?? '?')}s · ${freshness}`,
87
+ workspace ? `会话图表:${workspace.chartTab} · 指标=${workspace.indicators.join(',') || '无'} · 刻度=${workspace.priceScale} · 盘口精度=${workspace.bookGrouping}` : '',
88
+ `mark=${fmt(mark)} mid=${fmt(context.mid)} 24h=${change24hPct == null ? '?' : fmt(change24hPct) + '%'} 资金费=${context.funding1h == null ? '不适用' : fmt(context.funding1h * 100, 4) + '%/h'} 24h成交=${fmt(context.volume24hUsd, 0)} USD`,
89
+ `bid=${fmt(context.bid?.px)} ask=${fmt(context.ask?.px)} ${context.balanceSource === 'spot' ? 'USDC余额' : context.accountDex ? `${context.accountDex} DEX 抵押额` : '权益'}=${fmt(context.accountValue)} ${context.balanceSource === 'spot' ? '交易所可交易名义额' : '可用'}=${fmt(context.available)} ${context.accountDex && context.balanceSource !== 'spot' ? '抵押币单位' : 'USD'} · 挂单=${context.openOrders.length}${context.accountMode ? ` · 账户模式=${context.accountMode}` : ''}`,
90
+ !tradable ? account.mode === 'paper'
91
+ ? '当前账户数据不代表该只读市场的余额、仓位或可交易额度。'
92
+ : `当前市场仅可分析;${context.positionsCoverage === 'all-dexs' ? '仓位来自全部永续 DEX 快照' : '账户仓位仅覆盖所选 DEX'}${position ? `;当前仓位=${fmt(position.size, 6)} ${coin}` : ''},不能据此在该市场下单。`
93
+ : position ? `仓位 ${position.size > 0 ? 'LONG' : 'SHORT'} ${fmt(Math.abs(position.size), 6)} ${coin} entry=${fmt(position.entryPx)} uPnL=${fmt(position.uPnl)}` : '当前币对无持仓',
94
+ `风险:允许币对=${(context.riskLimits?.allowedCoins ?? []).join(',')} 单笔≤${fmt(context.riskLimits?.maxOrderUsd)} USD 每日≤${fmt(context.riskLimits?.maxDailyUsd)} USD 杠杆≤${fmt(context.riskLimits?.maxLeverage, 0)}x · 自主执行=${context.autonomousLive ? '开启' : '关闭'}${account.mode === 'paper' ? ' · 模拟盘固定滑点,未模拟盘口深度、手续费、资金费或强平' : ''}`,
95
+ `近 ${candles.length} 根 K 线价格(末根可能未收盘): ${candles.map((c) => fmt(c.c)).join(', ')}`,
96
+ ].join('\n')
97
+ return { text, context }
98
+ },
99
+ },
100
+ {
101
+ name: 'hyper_market',
102
+ description:
103
+ '查询 Hyperliquid 核心永续、HIP-3、现货与 Outcome 行情。action=overview 列出核心永续的 24h 涨跌/成交/资金费;action=book 查盘口;' +
104
+ 'action=catalog 搜索所有市场,支持官方 perpCategories 分类;action=candles 查 K 线(支持 Hyperliquid 官方 1m 至 1M 的 14 种周期);action=indicators 计算 SMA/EMA/RSI/MACD/布林带/ATR/支撑阻力;action=trades 查最近成交。非核心永续目前只读。',
105
+ parameters: {
106
+ type: 'object',
107
+ properties: {
108
+ action: { type: 'string', enum: ['overview', 'catalog', 'book', 'candles', 'indicators', 'trades'], description: '行情查询类型' },
109
+ coin: { type: 'string', description: '官方合约名,以 action=catalog 返回的 name 为准,如 BTC 或 xyz:XYZ100;overview 时可省略' },
110
+ kind: { type: 'string', enum: ['perp', 'hip3', 'spot', 'outcome'], description: 'catalog 市场类型过滤' },
111
+ category: { type: 'string', description: 'catalog 官方分类过滤,如 crypto、stocks、commodities、indices、fx、rates、preipo;tradfi 聚合这些传统金融标签' },
112
+ query: { type: 'string', description: 'catalog 名称搜索' },
113
+ interval: { type: 'string', description: 'K 线周期,默认 1h' },
114
+ limit: { type: 'number', description: 'candles/trades 返回条数上限,默认 48' },
115
+ },
116
+ required: ['action'],
117
+ additionalProperties: false,
118
+ },
119
+ output: { schema: { type: 'object', additionalProperties: true }, render: (_a, v) => [{ type: 'text', text: v.text }] },
120
+ timeoutMs: 15_000,
121
+ async execute(args) {
122
+ const action = args?.action ?? 'overview'
123
+ if (action === 'overview') {
124
+ const { universe } = await remote.markets()
125
+ const scored = universe
126
+ .filter((u) => u.ctx)
127
+ .sort((a, b) => Number(b.ctx.dayNtlVlm ?? 0) - Number(a.ctx.dayNtlVlm ?? 0))
128
+ .slice(0, 20)
129
+ .map((u) => ({
130
+ coin: u.name,
131
+ mark: Number(u.ctx.markPx),
132
+ change24hPct: Number(u.ctx.prevDayPx) ? ((Number(u.ctx.markPx) - Number(u.ctx.prevDayPx)) / Number(u.ctx.prevDayPx)) * 100 : 0,
133
+ vol24hUsd: Number(u.ctx.dayNtlVlm),
134
+ fundingPct1h: Number(u.ctx.funding) * 100,
135
+ openInterestUsd: Number(u.ctx.openInterest) * Number(u.ctx.markPx),
136
+ maxLeverage: u.maxLeverage,
137
+ }))
138
+ return { text: scored.map((s) => `${s.coin} mark=${fmt(s.mark)} 24h=${s.change24hPct.toFixed(2)}% vol=${fmt(s.vol24hUsd, 0)} funding=${s.fundingPct1h.toFixed(4)}%/h OI=${fmt(s.openInterestUsd, 0)} maxLev=${s.maxLeverage}x`).join('\n'), coins: scored }
139
+ }
140
+ if (action === 'catalog') {
141
+ const result = await remote.marketCatalog()
142
+ const query = String(args?.query ?? '').trim().toLowerCase()
143
+ const category = String(args?.category ?? '').trim().toLowerCase()
144
+ const tradfiTags = new Set(['stocks', 'stock', 'equities', 'equity', 'commodities', 'indices', 'fx', 'rates', 'preipo'])
145
+ const limit = Math.max(1, Math.min(Number(args?.limit) || 30, 100))
146
+ const list = result.markets.filter((row) => (!args?.kind || row.kind === args.kind)
147
+ && (!category || (category === 'tradfi' ? tradfiTags.has(String(row.category ?? '').toLowerCase()) : String(row.category ?? '').toLowerCase() === category))
148
+ && (!query || `${row.name} ${row.label} ${row.venue} ${row.category ?? ''}`.toLowerCase().includes(query)))
149
+ .sort((a, b) => Number(b.ctx?.dayNtlVlm ?? 0) - Number(a.ctx?.dayNtlVlm ?? 0)).slice(0, limit)
150
+ return { text: list.map((row) => `${row.label} [${row.kind}${row.category ? ',' + row.category : ''}${row.tradable ? '' : ',只读'}] mark=${row.ctx?.markPx ?? '?'} 24h量=${row.ctx?.dayNtlVlm ?? '?'}`).join('\n') || '没有匹配市场', markets: list, observedAt: result.ctxObservedAt ?? result.observedAt }
151
+ }
152
+ const coin = canonicalMarketCoin(args?.coin)
153
+ if (!coin) throw new Error(`action=${action} 需要 coin`)
154
+ if (action === 'book') {
155
+ const snap = await remote.snapshot({ coin })
156
+ const levels = (side) => (snap.book?.levels?.[side] ?? []).slice(0, 10).map((l) => ({ px: Number(l.px), sz: Number(l.sz) }))
157
+ return { text: `${coin} 盘口\nbids: ${levels(0).map((l) => `${l.px}×${l.sz}`).join(' | ')}\nasks: ${levels(1).map((l) => `${l.px}×${l.sz}`).join(' | ')}`, book: snap.book }
158
+ }
159
+ if (action === 'candles') {
160
+ const { candles } = await remote.candles({ coin, interval: args?.interval ?? '1h' })
161
+ const limit = Math.max(1, Math.min(Number(args?.limit) || 48, 200))
162
+ const tail = candles.slice(-limit)
163
+ return { text: tail.map((c) => new Date(c.t).toISOString().slice(5, 16) + ` O=${fmt(c.o)} H=${fmt(c.h)} L=${fmt(c.l)} C=${fmt(c.c)} V=${fmt(c.v, 3)}`).join('\n'), coin, interval: args?.interval ?? '1h', candles: tail }
164
+ }
165
+ if (action === 'indicators') {
166
+ const interval = args?.interval ?? '1h'
167
+ const { candles } = await remote.candles({ coin, interval })
168
+ const indicators = computeIndicators(candles, { interval })
169
+ if (!indicators.ready) return { text: `${coin} ${interval} 指标数据不足:${indicators.reason};已排除 ${indicators.excludedOpen} 根未收盘 K 线`, indicators }
170
+ const ageMin = Math.max(0, Math.round((Date.now() - indicators.observedAt) / 60_000))
171
+ const text = [
172
+ `${coin} ${interval} · ${indicators.candlesUsed} 根已收盘 K 线 · 最新收盘 ${new Date(indicators.observedAt).toISOString()}(${ageMin} 分钟前) · 排除 ${indicators.excludedOpen} 根形成中 K 线`,
173
+ `收盘=${fmt(indicators.close)} SMA20=${fmt(indicators.sma20)} EMA20=${fmt(indicators.ema20)} EMA50=${fmt(indicators.ema50)}`,
174
+ `RSI14=${fmt(indicators.rsi14, 1)} MACD=${fmt(indicators.macd.line, 4)} signal=${fmt(indicators.macd.signal, 4)} hist=${fmt(indicators.macd.histogram, 4)}`,
175
+ `布林带下/中/上=${fmt(indicators.bollinger20.lower)}/${fmt(indicators.bollinger20.middle)}/${fmt(indicators.bollinger20.upper)} ATR14=${fmt(indicators.atr14)} 量比20=${fmt(indicators.volumeRatio20, 2)}`,
176
+ `邻近支撑=${fmt(indicators.support.price)}(${new Date(indicators.support.time).toISOString()}) 阻力=${fmt(indicators.resistance.price)}(${new Date(indicators.resistance.time).toISOString()})`,
177
+ '指标是历史 K 线的派生值;需要结合最新盘口、账户风险和数据时效验证,不能单独当作下单信号。',
178
+ ].join('\n')
179
+ return { text, indicators, interval, coin }
180
+ }
181
+ if (action === 'trades') {
182
+ const snap = await remote.snapshot({ coin })
183
+ const list = (Array.isArray(snap.trades) ? snap.trades : []).slice(0, Math.max(1, Math.min(Number(args?.limit) || 20, 50)))
184
+ return { text: list.map((t) => `${new Date(t.time).toISOString().slice(11, 19)} ${t.side} ${fmt(t.sz, 4)} @ ${fmt(t.px)}`).join('\n'), trades: list }
185
+ }
186
+ throw new Error(`未知 action: ${action}`)
187
+ },
188
+ },
189
+ {
190
+ name: 'hyper_chart',
191
+ description: '在当前 DSH 交易会话的行情图上画或管理供人查看的水平支撑/阻力线与趋势线。先用 hyper_market candles/indicators 验证价格和时间;标注仅用于解释,不是自动订单或交易信号。',
192
+ parameters: {
193
+ type: 'object', properties: {
194
+ action: { type: 'string', enum: ['add', 'list', 'remove'] },
195
+ coin: { type: 'string', description: '默认会话绑定币对' },
196
+ interval: { type: 'string', description: '默认当前交易台周期' },
197
+ kind: { type: 'string', enum: ['level', 'trend'], description: 'add 必填:水平线或趋势线' },
198
+ role: { type: 'string', enum: ['support', 'resistance', 'neutral'], description: '视觉语义' },
199
+ label: { type: 'string', description: '图上展示的短标签,1-60 字' },
200
+ price: { type: 'number', description: '水平线价格' },
201
+ fromTime: { type: 'number', description: '趋势线起点 K 线时间,epoch ms' },
202
+ fromPrice: { type: 'number', description: '趋势线起点价格' },
203
+ toTime: { type: 'number', description: '趋势线终点 K 线时间,epoch ms' },
204
+ toPrice: { type: 'number', description: '趋势线终点价格' },
205
+ id: { type: 'string', description: 'remove 时使用 list 返回的标注 ID' },
206
+ }, required: ['action'], additionalProperties: false,
207
+ },
208
+ output: { schema: { type: 'object', additionalProperties: true }, render: (_a, v) => [{ type: 'text', text: v.text }] },
209
+ async execute(args, exec) {
210
+ const sessionId = sessionIdOf(exec)
211
+ remote.requireTradingSession(sessionId)
212
+ const coin = canonicalMarketCoin(args?.coin ?? remote.store.getSession(sessionId)?.coin ?? remote.desk.coin)
213
+ const interval = args?.interval ?? remote.store.getSessionWorkspace?.(sessionId)?.interval ?? remote.desk.interval
214
+ if (args?.action === 'list') {
215
+ const result = await remote.chartMarks({ sessionId, coin, interval })
216
+ return { text: result.marks.length ? result.marks.map((mark) => `${mark.id} ${mark.kind} ${mark.role} ${mark.label}`).join('\n') : '当前图表没有标注', ...result }
217
+ }
218
+ if (args?.action === 'add') {
219
+ const mark = remote.addChartMark({ ...args, coin, interval }, sessionId)
220
+ return { text: `已在 ${coin} ${interval} 图表添加 ${mark.kind === 'level' ? '水平线' : '趋势线'}:${mark.label}(${mark.id})`, mark }
221
+ }
222
+ if (args?.action === 'remove') {
223
+ const mark = remote.removeChartMark(args?.id, sessionId)
224
+ return { text: `已删除图表标注:${mark.label}(${mark.id})`, mark }
225
+ }
226
+ throw new Error(`未知 action: ${args?.action}`)
227
+ },
228
+ },
229
+ {
230
+ name: 'hyper_account',
231
+ description: '查询交易账户。action=state 返回权益/持仓;orders 返回挂单;fills 返回成交;orderStatus 用 oid 或 cloid 核对某笔 live 订单,尤其在网络超时后。',
232
+ parameters: {
233
+ type: 'object',
234
+ properties: {
235
+ action: { type: 'string', enum: ['state', 'orders', 'fills', 'orderStatus'], description: '账户查询类型,默认 state' },
236
+ oid: { type: 'string', description: 'orderStatus 必填;交易所 oid 或 0x 开头 128 位 cloid' },
237
+ },
238
+ additionalProperties: false,
239
+ },
240
+ output: { schema: { type: 'object', additionalProperties: true }, render: (_a, v) => [{ type: 'text', text: v.text }] },
241
+ timeoutMs: 15_000,
242
+ async execute(args) {
243
+ const action = args?.action ?? 'state'
244
+ if (action === 'state') {
245
+ const a = await remote.account({ coin: remote.desk?.coin })
246
+ const lines = [`mode=${a.mode}${a.address ? ' ' + a.address : ''}${a.accountDex ? ` DEX=${a.accountDex}` : ''}${a.positionsCoverage ? ` positions=${a.positionsCoverage}` : ''}`]
247
+ if (a.mode === 'paper') lines.push(`现金=${fmt(a.usd)} 权益=${fmt(a.equity)} 已用保证金=${fmt(a.marginUsed)} 可用保证金=${fmt(a.availableMargin)} uPnL=${fmt(a.uPnl)};${a.simulation}`)
248
+ else if (a.balanceSource === 'spot') lines.push(`账户模式=${a.accountMode} USDC余额=${fmt(a.usdcBalance)} 未占用=${fmt(a.unheldUsdc)}${a.tradeCapacity ? ` ${a.tradeCapacity.coin}可交易名义额=${fmt(Math.min(...a.tradeCapacity.availableToTrade.map(Number)))}` : ''}`)
249
+ else if (a.marginSummary) lines.push(`账户模式=${a.accountMode} 账户价值=${fmt(a.marginSummary.accountValue)} 可用=${fmt(a.withdrawable)}`)
250
+ for (const p of a.positions) {
251
+ const leverage = typeof p.leverage === 'number' ? p.leverage : Number(p.leverage?.value ?? p.leverage?.leverage)
252
+ lines.push(`${p.coin} ${p.size > 0 ? 'LONG' : 'SHORT'} sz=${fmt(Math.abs(p.size), 4)} entry=${fmt(p.entryPx)} mark=${fmt(p.markPx)} uPnL=${fmt(p.uPnl)}` +
253
+ `${Number.isFinite(leverage) && leverage > 0 ? ` lev=${leverage}x` : ''}` +
254
+ `${p.returnOnEquity == null ? '' : ` ROE=${fmt(Number(p.returnOnEquity) * 100)}%`}` +
255
+ `${p.liquidationPx == null ? '' : ` liq=${fmt(p.liquidationPx)}`}` +
256
+ `${p.marginUsed == null ? '' : ` margin=${fmt(p.marginUsed)}`}`)
257
+ }
258
+ if (a.positions.length === 0) lines.push('(无持仓)')
259
+ return { text: lines.join('\n'), account: a }
260
+ }
261
+ if (action === 'orders') {
262
+ const o = await remote.orders()
263
+ const rows = o.orders ?? []
264
+ return { text: rows.length ? rows.map((r) => `${r.coin} ${r.side ?? ''} ${fmt(r.origSz ?? r.sz, 4)} @ ${fmt(r.limitPx ?? r.px)} oid=${r.oid}`).join('\n') : '(无挂单)', orders: rows }
265
+ }
266
+ if (action === 'fills') {
267
+ const f = await remote.fills()
268
+ const rows = f.fills ?? []
269
+ return { text: rows.length ? rows.slice(0, 20).map((r) => `${new Date(r.time ?? r.filledTime ?? Date.now()).toISOString().slice(5, 19)} ${r.coin} ${r.side ?? ''} ${fmt(r.sz ?? r.filledSz, 4)} @ ${fmt(r.px ?? r.filledPx)}`).join('\n') : '(无成交)', fills: rows.slice(0, 50) }
270
+ }
271
+ if (action === 'orderStatus') {
272
+ const result = await remote.orderStatus({ oid: args?.oid })
273
+ return { text: JSON.stringify(result), ...result }
274
+ }
275
+ throw new Error(`未知 action: ${action}`)
276
+ },
277
+ },
278
+ {
279
+ name: 'hyper_decide',
280
+ description: '对一个 live 交易意图做服务端预检:读取实时价格、账户权益和仓位,执行币对/单笔/每日/仓位/杠杆硬限制,并生成 30 秒、仅本会话可用的一次性 decisionId。可选 takeProfitPx/stopLossPx 会随主单作为 normalTpsl 分组保护单提交;父单未完全成交时保护单尚未挂出,部分成交后撤销父单会撤销子单,必须核对真实仓位与保护挂单。paper 暂不支持触发单。先独立分析行情与风险,再提交方向、数量和依据;预检通过不等于交易所已成交。',
281
+ parameters: {
282
+ type: 'object',
283
+ properties: {
284
+ coin: { type: 'string' }, isBuy: { type: 'boolean' }, sz: { type: 'number' },
285
+ type: { type: 'string', enum: ['market', 'limit'] }, px: { type: 'number' },
286
+ tif: { type: 'string', enum: ['Gtc', 'Alo'] }, reduceOnly: { type: 'boolean' },
287
+ takeProfitPx: { type: 'number', description: '可选止盈触发价;与 hyper_order 完全一致' },
288
+ stopLossPx: { type: 'number', description: '可选止损触发价;与 hyper_order 完全一致' },
289
+ thesis: { type: 'string', description: '交易依据、反证条件与风险,10-1000 字' },
290
+ },
291
+ required: ['coin', 'isBuy', 'sz', 'thesis'], additionalProperties: false,
292
+ },
293
+ output: { schema: { type: 'object', additionalProperties: true }, render: (_a, v) => [{ type: 'text', text: v.text }] },
294
+ timeoutMs: 20_000,
295
+ async execute(args, exec) {
296
+ const result = await remote.prepareOrder(args, sessionIdOf(exec))
297
+ return { text: `交易预检通过:${result.side} ${result.coin},价格=${fmt(result.mid)},风险估值=${fmt(result.notionalUsd)} USD,当前仓位=${fmt(result.positionSize, 6)},执行后仓位=${fmt(result.nextSize, 6)}。decisionId=${result.decisionId}(30 秒内一次性使用)。`, ...result }
298
+ },
299
+ },
300
+ {
301
+ name: 'hyper_order',
302
+ description:
303
+ '交易下单/撤单/杠杆。action=place:paper 直接执行;live 必须先调用 hyper_decide 并提供同一订单的一次性 decisionId。live 可带 takeProfitPx/stopLossPx,必须与预检完全一致,作为 normalTpsl 分组保护单提交;父单未完全成交时保护单尚未挂出,部分成交后撤销父单会撤销子单,提交后须核对仓位与实际保护挂单。自主 live 仅在宿主显式开启且钱包策略允许时执行,否则触发人工审批。' +
304
+ 'action=cancel 撤单(oid);action=leverage 设置杠杆(1-50)。autonomousLive=false 时 live 撤单和杠杆变更需人工审批;否则仍受 dsh-web3 钱包策略约束。',
305
+ parameters: {
306
+ type: 'object',
307
+ properties: {
308
+ action: { type: 'string', enum: ['place', 'cancel', 'leverage'], description: '交易动作' },
309
+ coin: { type: 'string', description: '合约名,如 BTC' },
310
+ isBuy: { type: 'boolean', description: 'true=买入/做多,false=卖出/做空(place 必填)' },
311
+ sz: { type: 'number', description: '数量(币本位,如 0.5 BTC)(place 必填)' },
312
+ type: { type: 'string', enum: ['market', 'limit'], description: '订单类型,默认 market' },
313
+ px: { type: 'number', description: '限价单价格(type=limit 必填)' },
314
+ tif: { type: 'string', enum: ['Gtc', 'Alo'], description: '限价单时效,默认 Gtc' },
315
+ reduceOnly: { type: 'boolean', description: '只减仓,默认 false' },
316
+ takeProfitPx: { type: 'number', description: '可选止盈触发价;paper 不支持' },
317
+ stopLossPx: { type: 'number', description: '可选止损触发价;paper 不支持' },
318
+ decisionId: { type: 'string', description: 'hyper_decide 返回的本会话一次性决策 ID(live 必填)' },
319
+ reason: { type: 'string', description: 'paper 下单原因,10-1000 字;live 使用 hyper_decide.thesis' },
320
+ oid: { type: 'number', description: '撤单的订单 id(cancel 必填)' },
321
+ leverage: { type: 'number', description: '杠杆倍数 1-50(leverage 必填)' },
322
+ marginMode: { type: 'string', enum: ['cross', 'isolated'], description: '保证金模式,默认 cross' },
323
+ },
324
+ required: ['action'],
325
+ additionalProperties: false,
326
+ },
327
+ output: { schema: { type: 'object', additionalProperties: true }, render: (_a, v) => [{ type: 'text', text: v.text }] },
328
+ timeoutMs: 30_000,
329
+ async execute(args, exec) {
330
+ const action = args?.action
331
+ const sessionId = sessionIdOf(exec)
332
+ if (action === 'place') {
333
+ const coin = canonicalMarketCoin(args?.coin)
334
+ if (!coin) throw new Error('place 需要 coin')
335
+ if (typeof args?.isBuy !== 'boolean') throw new Error('place 需要明确的 isBuy')
336
+ const isBuy = args.isBuy
337
+ const sz = Number(args?.sz)
338
+ const type = args?.type === 'limit' ? 'limit' : 'market'
339
+ const reason = String(args?.reason ?? '').trim()
340
+ if (remote.cfg.tradingMode === 'paper' && (reason.length < 10 || reason.length > 1000)) throw new Error('paper 下单需要 10-1000 字的原因')
341
+ const summary = `${shortSide(isBuy)} ${fmt(sz, 6)} ${coin} @ ${type === 'limit' ? fmt(args?.px) + ' (limit)' : '市价'}${args?.reduceOnly ? ' [只减仓]' : ''} · 会话=${sessionId ?? '未知'}`
342
+ const res = await remote.executeToolOrder({
343
+ coin, isBuy, sz, type, px: args?.px, tif: args?.tif, reduceOnly: args?.reduceOnly,
344
+ takeProfitPx: args?.takeProfitPx, stopLossPx: args?.stopLossPx, decisionId: args?.decisionId,
345
+ reason,
346
+ }, exec, sessionId)
347
+ if (res.mode === 'paper') {
348
+ const f = res.fill
349
+ return {
350
+ text: `✅ paper 成交 ${shortSide(f.isBuy)} ${fmt(f.sz, 6)} ${f.coin} @ ${fmt(f.px)}(名义=${fmt(f.notional)},实现盈亏=${fmt(f.realized)})。中间台已同步。`,
351
+ fill: f,
352
+ }
353
+ }
354
+ const statuses = res.response?.response?.data?.statuses ?? []
355
+ const protectionNote = res.protectionState === 'parent-not-fully-filled'
356
+ ? '\n⚠ 父单未确认完全成交,止盈止损尚不能视为生效;立即核对实际仓位和保护挂单。'
357
+ : res.protectionState === 'verify-active'
358
+ ? '\n止盈止损已随完整成交父单提交;仍需核对实际保护挂单。' : ''
359
+ return { text: `live 订单已提交:${summary}\n交易所回执: ${JSON.stringify(statuses).slice(0, 400)}${protectionNote}`, response: res.response, cloid: res.cloid, protectionState: res.protectionState }
360
+ }
361
+ if (action === 'cancel') {
362
+ const res = await remote.executeToolCancel({ coin: args?.coin, oid: args?.oid }, exec)
363
+ return { text: `撤单已提交(${args?.coin} oid=${args?.oid}): ${JSON.stringify(res.response).slice(0, 300)}` }
364
+ }
365
+ if (action === 'leverage') {
366
+ const res = await remote.executeToolLeverage({ coin: args?.coin, leverage: args?.leverage, marginMode: args?.marginMode }, exec)
367
+ return { text: `杠杆设置已提交:${args?.coin} = ${args?.leverage}x (${args?.marginMode ?? 'cross'})${res.mode === 'paper' ? '(paper 本地生效)' : ''}`, response: res.response ?? null }
368
+ }
369
+ throw new Error(`未知 action: ${action}`)
370
+ },
371
+ },
372
+ {
373
+ name: 'hyper_session',
374
+ description:
375
+ '交易会话上下文。action=status 返回本会话绑定的币对与当前交易台显示状态(用户正在看什么);action=bind 把用户在中间台选中的币对与本会话绑定。' +
376
+ '对话中引用"当前界面/当前币"时先调它同步视角。',
377
+ parameters: {
378
+ type: 'object',
379
+ properties: {
380
+ action: { type: 'string', enum: ['status', 'bind'], description: '会话动作,默认 status' },
381
+ coin: { type: 'string', description: 'action=bind 时绑定 action=catalog 返回的官方合约名,如 xyz:XYZ100' },
382
+ },
383
+ additionalProperties: false,
384
+ },
385
+ output: { schema: { type: 'object', additionalProperties: true }, render: (_a, v) => [{ type: 'text', text: v.text }] },
386
+ timeoutMs: 10_000,
387
+ async execute(args, exec) {
388
+ const sessionId = sessionIdOf(exec)
389
+ const action = args?.action ?? 'status'
390
+ if (action === 'bind') {
391
+ const coin = canonicalMarketCoin(args?.coin)
392
+ if (!coin) throw new Error('bind 需要 coin')
393
+ // The desk follows the agent's bind: one desk, one focus.
394
+ await remote.deskState({ coin }).catch(() => {})
395
+ const row = sessionId
396
+ ? remote.store.registerSession(sessionId, { coin })
397
+ : { coin }
398
+ return { text: `本会话交易视角已绑定到 ${coin}${sessionId ? '(交易台已跟随)' : ''}`, session: row }
399
+ }
400
+ const row = sessionId ? remote.store.getSession(sessionId) : null
401
+ const st = await remote.status()
402
+ const desk = st.desk ?? remote.desk ?? {}
403
+ const lines = [`mode=${st.tradingMode} network=${st.network}${st.address ? ' ' + st.address : ''}`]
404
+ if (desk.coin) lines.push(`交易台当前焦点: ${desk.coin} · K线 ${desk.interval ?? '15m'}`)
405
+ if (row) lines.push(`本会话绑定币对: ${row.coin}`)
406
+ else lines.push('本会话尚未绑定币对(可用 action=bind 绑定,或让用户在中间台选择)')
407
+ return { text: lines.join('\n'), session: row, status: st }
408
+ },
409
+ },
410
+ ]
411
+ }
@@ -0,0 +1,86 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ const TTL_MS = 30_000
6
+
7
+ /** Server-only, one-use trade plans and conservative durable daily budgets. */
8
+ export class TradingGuard {
9
+ constructor(dataDir, limits, { budgetName = 'live-budget.json' } = {}) {
10
+ this.limits = limits
11
+ this.plans = new Map()
12
+ this.budgetFile = join(dataDir, budgetName)
13
+ this.budget = existsSync(this.budgetFile)
14
+ ? JSON.parse(readFileSync(this.budgetFile, 'utf8'))
15
+ : { day: '', usedUsd: 0 }
16
+ if (typeof this.budget.day !== 'string' || !Number.isFinite(this.budget.usedUsd) || this.budget.usedUsd < 0) {
17
+ throw new Error('live 交易额度账本损坏,拒绝启动')
18
+ }
19
+ }
20
+
21
+ validate({ coin, isBuy, sz, price, positionSize, otherExposureUsd = 0, accountValue, leverage, reduceOnly }) {
22
+ const l = this.limits
23
+ if (typeof isBuy !== 'boolean' || !Number.isFinite(sz) || sz <= 0 || !Number.isFinite(price) || price <= 0) throw new Error('订单方向、数量或价格无效')
24
+ if (!Number.isFinite(accountValue) || !Number.isFinite(otherExposureUsd) || otherExposureUsd < 0) throw new Error('无法验证账户权益/敞口,拒绝下单')
25
+ if (!Number.isFinite(positionSize) || !Number.isFinite(leverage) || leverage < 1) throw new Error('持仓或杠杆超出交易权限')
26
+ const signed = (isBuy ? 1 : -1) * sz
27
+ const nextSize = positionSize + signed
28
+ if (reduceOnly && (positionSize === 0 || Math.sign(signed) === Math.sign(positionSize) || sz > Math.abs(positionSize) + 1e-10)) throw new Error('只减仓订单会增加或翻转仓位')
29
+ const riskReducing = positionSize !== 0 && Math.sign(signed) !== Math.sign(positionSize) &&
30
+ (nextSize === 0 || Math.sign(nextSize) === Math.sign(positionSize)) && Math.abs(nextSize) < Math.abs(positionSize)
31
+ if (!riskReducing && !l.allowedCoins.includes(coin)) throw new Error(`交易限额:${coin} 不在允许交易币对内(allowedCoins)`)
32
+ if (!riskReducing && leverage > l.maxLeverage) throw new Error(`交易限额:杠杆 ${leverage}x 超过上限 ${l.maxLeverage}x(maxLeverage)`)
33
+ const notionalUsd = sz * price
34
+ if (!riskReducing && accountValue <= 0) throw new Error('账户权益不足,拒绝开仓')
35
+ if (!riskReducing && notionalUsd > l.maxOrderUsd) throw new Error(`交易限额:单笔名义价值 ${notionalUsd.toFixed(2)} 超过上限 ${l.maxOrderUsd} USD(maxOrderUsd)`)
36
+ if (!riskReducing && Math.abs(nextSize) * price > l.maxPositionUsd) throw new Error(`交易限额:执行后 ${coin} 仓位超过 ${l.maxPositionUsd} USD(maxPositionUsd)`)
37
+ if (!riskReducing && Math.abs(nextSize) * price > accountValue * l.maxLeverage) throw new Error('执行后仓位超过权益允许的杠杆敞口')
38
+ if (!riskReducing && otherExposureUsd + Math.abs(nextSize) * price > accountValue * l.maxLeverage) throw new Error('执行后全账户敞口超过权益允许的杠杆倍数')
39
+ this.rollDay()
40
+ if (!riskReducing && this.budget.usedUsd + notionalUsd > l.maxDailyUsd) throw new Error(`交易限额:今日下单额将超过 ${l.maxDailyUsd} USD(maxDailyUsd,UTC 日重置)`)
41
+ return { notionalUsd, nextSize, riskReducing }
42
+ }
43
+
44
+ rollDay() {
45
+ const day = new Date().toISOString().slice(0, 10)
46
+ if (this.budget.day !== day) this.budget = { day, usedUsd: 0 }
47
+ }
48
+
49
+ create(order, sessionId, preview) {
50
+ const id = randomUUID()
51
+ this.plans.set(id, { order, sessionId, preview, expiresAt: Date.now() + TTL_MS })
52
+ for (const [key, plan] of this.plans) if (plan.expiresAt <= Date.now()) this.plans.delete(key)
53
+ return { decisionId: id, expiresAt: Date.now() + TTL_MS, ...preview }
54
+ }
55
+
56
+ consume(id, order, sessionId) {
57
+ const plan = this.plans.get(id)
58
+ this.plans.delete(id)
59
+ if (!plan || plan.expiresAt <= Date.now()) throw new Error('交易决策已失效,请重新预检')
60
+ if (plan.sessionId !== sessionId || JSON.stringify(plan.order) !== JSON.stringify(order)) throw new Error('订单与预检决策不一致')
61
+ return plan.preview
62
+ }
63
+
64
+ reserve(notionalUsd, { riskReducing = false } = {}) {
65
+ this.rollDay()
66
+ if (riskReducing) return { day: this.budget.day, notionalUsd: 0 }
67
+ if (this.budget.usedUsd + notionalUsd > this.limits.maxDailyUsd) throw new Error('交易限额:今日下单额度已耗尽(maxDailyUsd,UTC 日重置)')
68
+ this.budget.usedUsd += notionalUsd
69
+ mkdirSync(this.limits.dataDir, { recursive: true })
70
+ const tmp = this.budgetFile + '.tmp'
71
+ writeFileSync(tmp, JSON.stringify(this.budget), { mode: 0o600 })
72
+ renameSync(tmp, this.budgetFile)
73
+ return { day: this.budget.day, notionalUsd }
74
+ }
75
+
76
+ /** Return quota only when the exchange definitively rejected the action. */
77
+ release(ticket) {
78
+ this.rollDay()
79
+ if (!ticket || ticket.day !== this.budget.day) return
80
+ this.budget.usedUsd = Math.max(0, this.budget.usedUsd - ticket.notionalUsd)
81
+ mkdirSync(this.limits.dataDir, { recursive: true })
82
+ const tmp = this.budgetFile + '.tmp'
83
+ writeFileSync(tmp, JSON.stringify(this.budget), { mode: 0o600 })
84
+ renameSync(tmp, this.budgetFile)
85
+ }
86
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Hyperliquid Desk",
4
+ "description": "A peer-level Hyperliquid trading platform for dsh: sessions, live market data, and an embedded native conversation for decisions and execution."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Hyperliquid 交易台",
4
+ "description": "与 bots 平级的 Hyperliquid 交易平台:左侧交易会话、中间实时行情与账户、右侧嵌入 DSH 原生会话;交易意图由会话决策执行。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,9 +1,27 @@
1
1
  {
2
2
  "name": "dsh-hyperliquid",
3
- "version": "0.0.1",
4
- "description": "Hyperliquid toolkit for dsh - positions, spot tokens, builder codes, with everything cited. Name reserved; spec below; first release in development.",
5
- "license": "MIT",
6
- "author": "dushaobindoudou",
3
+ "version": "0.1.0",
4
+ "description": "Hyperliquid trading desk for the DeepSeek Harness (dsh) - market workspace, native agent conversation and guarded live / paper trading over the Hyperliquid API.",
5
+ "icon": "./icon.svg",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./client": "./client.js",
11
+ "./package.json": "./package.json",
12
+ "./locale/*.json": "./locale/*.json"
13
+ },
14
+ "files": [
15
+ "index.js",
16
+ "client.js",
17
+ "lib",
18
+ "locale",
19
+ "skills",
20
+ "icon.svg",
21
+ "cordis.patch.yml",
22
+ "README.md",
23
+ "NOTICE"
24
+ ],
7
25
  "keywords": [
8
26
  "dsh",
9
27
  "dsh-plugin",
@@ -11,13 +29,60 @@
11
29
  "cordis-plugin",
12
30
  "hyperliquid",
13
31
  "perps",
14
- "hip-1",
15
- "hip-2",
16
- "builder-codes"
32
+ "trading"
17
33
  ],
34
+ "license": "MIT",
35
+ "author": "dushaobindoudou",
36
+ "scripts": {
37
+ "build": "node scripts/build-client.mjs",
38
+ "build:watch": "node scripts/build-client.mjs --watch",
39
+ "typecheck": "tsc --noEmit -p tsconfig.client.json",
40
+ "test": "node test/signing.test.mjs && node test/signing-web3.test.mjs && node test/exchange-nonce.test.mjs && node test/store.test.mjs && node test/trading-guard.test.mjs && node test/paper-risk.test.mjs && node test/network-switch.test.mjs && node test/account-data.test.mjs && node test/market-data.test.mjs && node test/market-symbol.test.mjs && node test/indicators.test.mjs && node test/chart-marks.test.mjs && node test/history.test.mjs && node test/skill.test.mjs && node test/markers.test.mjs && node test/client-integration.mjs"
41
+ },
42
+ "dependencies": {
43
+ "@deepseek-ai/schemastery": "^3.18.4",
44
+ "@nktkas/hyperliquid": "^0.33.3",
45
+ "@noble/curves": "^1.9.7",
46
+ "@noble/hashes": "^1.8.0"
47
+ },
18
48
  "repository": {
19
49
  "type": "git",
20
- "url": "git+https://github.com/dushaobindoudou/dsh-hyperliquid.git"
50
+ "url": "https://github.com/dushaobindoudou/dsh-plugin.git",
51
+ "directory": "packages/dsh-hyperliquid"
52
+ },
53
+ "homepage": "https://github.com/dushaobindoudou/dsh-plugin/tree/main/packages/dsh-hyperliquid#readme",
54
+ "dsh": {
55
+ "bundle": {
56
+ "patch": "./cordis.patch.yml"
57
+ },
58
+ "client": {
59
+ "platform": "web",
60
+ "immediately": true,
61
+ "inject": [
62
+ "@deepseek-ai/dsh-client-connection",
63
+ "@deepseek-ai/dsh-client-locale",
64
+ "@deepseek-ai/dsh-client-ui-theme",
65
+ "@deepseek-ai/dsh-client-ui-layout",
66
+ "@deepseek-ai/dsh-client-ui-slots",
67
+ "@deepseek-ai/dsh-api-session-controller",
68
+ "@deepseek-ai/dsh-api-workspace-controller",
69
+ "@deepseek-ai/dsh-client-ui-conversation",
70
+ "@deepseek-ai/dsh-client-ui-session",
71
+ "@deepseek-ai/dsh-client-ui-workspace"
72
+ ]
73
+ }
21
74
  },
22
- "homepage": "https://github.com/dushaobindoudou/dsh-hyperliquid#readme"
75
+ "devDependencies": {
76
+ "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
77
+ "@types/react": "^19.3.0",
78
+ "@types/react-dom": "^19.3.0",
79
+ "dsh-web3": "^0.8.1",
80
+ "esbuild": "^0.28.2",
81
+ "ethers": "^6.13.0",
82
+ "lightweight-charts": "^5.2.1",
83
+ "playwright-core": "^1.64.0",
84
+ "react": "^19.3.0",
85
+ "react-dom": "^19.3.0",
86
+ "typescript": "^5.9.3"
87
+ }
23
88
  }