dsh-hyperliquid 0.0.1 → 0.2.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,7 @@
1
+ - insert:
2
+ - id: hyperliquid
3
+ name: dsh-hyperliquid
4
+ config:
5
+ dataDir: '~/.dsh-hyperliquid'
6
+ network: mainnet
7
+ tradingMode: live
package/icon.svg ADDED
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
2
+ <!-- Official Hyperliquid mark (brand kit "Hyperliquid SVG format.zip",
3
+ Hyperliquid_Blob_Green.svg) on the official brand-dark plate.
4
+ Source: https://hyperliquid.gitbook.io/hyperliquid-docs/brand-kit -->
5
+ <rect width="200" height="200" rx="44" fill="#011916"/>
6
+ <path d="M175.11589,99.27891c0,49.46366-30.3687,65.33789-46.47302,51.30413-13.11357-11.5031-17.02471-35.88989-36.81018-38.42055-25.07698-2.99085-27.37764,30.36839-43.94216,30.36839-19.3254,0-23.00642-27.83773-23.00642-42.33179,0-14.72405,4.14115-34.73961,20.47565-34.73961,19.09528,0,20.24566,28.75802,44.17226,27.14754,23.69661-1.61047,24.1567-31.51877,39.80103-44.17224,13.57335-11.27313,45.78283.69019,45.78283,50.84413Z" fill="#97fce4"/>
7
+ </svg>
package/index.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * dsh-hyperliquid — host half.
3
+ *
4
+ * Publishes the `hyper` Remote namespace (Typert, dsh-bots-anchored), registers
5
+ * the hyper_* agent tools, and contributes a short system-prompt section so
6
+ * agents in any session know the trading desk exists and how to drive it.
7
+ * The client half (client.js) renders the peer-level trading platform.
8
+ *
9
+ * @module dsh-hyperliquid
10
+ */
11
+
12
+ import { HyperRemote } from './lib/remote.js'
13
+ import { buildToolDefinitions } from './lib/tools.js'
14
+ import { registerHyperSkill } from './lib/skill.js'
15
+
16
+ export const name = 'dsh-hyperliquid'
17
+
18
+ /** Optional services only: without `tools` the desk still serves the UI. */
19
+ export const inject = []
20
+
21
+ let z = null
22
+ try {
23
+ // Prefer the HOST's schemastery copy (same implementation the loader
24
+ // validates with) resolved through the dsh-bin anchor; fall back to the
25
+ // bundled copy. require(esm) — Node ≥ 22.12.
26
+ const { createRequire } = await import('node:module')
27
+ const { realpathSync } = await import('node:fs')
28
+ try {
29
+ const anchor = realpathSync(process.argv?.[1] ?? '')
30
+ z = createRequire(anchor)('@deepseek-ai/schemastery')
31
+ if (!z || typeof z.object !== 'function') z = null
32
+ } catch { /* anchor unavailable → bundled copy */ }
33
+ if (!z) z = (await import('@deepseek-ai/schemastery')).default
34
+ } catch (err) {
35
+ throw new Error(`[dsh-hyperliquid] schemastery 加载失败:${String(err?.message ?? err)}`)
36
+ }
37
+
38
+ export const Config = z.object({
39
+ dataDir: z.string().description('插件数据目录(交易会话登记表与 paper 账户落盘处)').default('~/.dsh-hyperliquid'),
40
+ network: z.union([z.const('mainnet'), z.const('testnet')]).description('Hyperliquid 网络,默认 mainnet;testnet 可在交易台「更多」中切换').default('mainnet'),
41
+ tradingMode: z.union([z.const('paper'), z.const('live')]).description('交易模式:live 真实签名下单(默认),paper 本地撮合;可在交易台「更多」中切换').default('live'),
42
+ privateKey: z.string().description('Hyperliquid 钱包私钥(signer=local 时使用;建议用 agent wallet 派生密钥;留空则 live 不可用)。也可用环境变量 HYPERLIQUID_PRIVATE_KEY').default(''),
43
+ signer: z.union([z.const('local'), z.const('web3')]).description('签名方式:local 插件内私钥签名;web3 交给 dsh-web3 钱包保险库签名(私钥不出保险库,逐笔过签名策略与审批)').default('local'),
44
+ web3Account: z.string().description('signer=web3 时使用的 dsh-web3 账户 id 或名称').default('main'),
45
+ tradingAddress: z.string().description('Hyperliquid 主账户地址;使用 dsh-web3 API wallet 签名时必填,账户查询必须用主账户地址').default(''),
46
+ autonomousLive: z.boolean().description('允许 agent 在硬性限额内自主提交 live 订单;默认关闭,且仅支持 dsh-web3 签名').default(false),
47
+ allowedCoins: z.string().description('交易限额(paper/live 均生效):允许新开仓的币对,逗号分隔;减仓不受限').default('BTC,ETH'),
48
+ maxOrderUsd: z.number().description('交易限额(paper/live 均生效):单笔名义价值上限 USD').default(100),
49
+ maxDailyUsd: z.number().description('交易限额(paper/live 均生效):每 UTC 日累计开仓名义价值上限 USD,paper 与 live 分账').default(500),
50
+ maxPositionUsd: z.number().description('交易限额(paper/live 均生效):每币最大仓位名义价值 USD').default(500),
51
+ maxLeverage: z.number().description('交易限额(paper/live 均生效):最大杠杆倍数').default(2),
52
+ }).description('Hyperliquid 交易台配置')
53
+
54
+ /**
55
+ * @param ctx - the Cordis plugin context.
56
+ * @param config - validated row config.
57
+ */
58
+ export function apply(ctx, config = {}) {
59
+ const remote = new HyperRemote(ctx, config)
60
+ const disposers = []
61
+
62
+ // Live channel lifecycle: stop the upstream WS on teardown.
63
+ ctx.effect(() => () => remote.stopWs())
64
+
65
+ // Agent tools (optional service — degrade gracefully).
66
+ const tools = typeof ctx?.get === 'function' ? ctx.get('tools') : undefined
67
+ if (tools && typeof tools.register === 'function') {
68
+ for (const def of buildToolDefinitions(remote)) {
69
+ const remove = tools.register(def)
70
+ disposers.push(remove)
71
+ ctx.effect(() => () => { try { remove() } catch { /* already gone */ } })
72
+ }
73
+ } else {
74
+ ctx.logger?.warn?.('[dsh-hyperliquid] tools 服务不可用,仅提供 UI 远端')
75
+ }
76
+
77
+ const skillRemove = registerHyperSkill(typeof ctx?.get === 'function' ? ctx.get('skills') : null)
78
+ if (skillRemove) {
79
+ disposers.push(skillRemove)
80
+ ctx.effect(() => () => { try { skillRemove() } catch { /* already gone */ } })
81
+ }
82
+
83
+ // System-prompt section: make the desk discoverable to every agent, with
84
+ // LIVE desk context (what the user is looking at right now) evaluated per
85
+ // prompt — the desk and the conversation stay in lockstep both ways.
86
+ const systemPrompt = typeof ctx?.get === 'function' ? ctx.get('systemPrompt') : undefined
87
+ if (systemPrompt && typeof systemPrompt.section === 'function') {
88
+ const remove = systemPrompt.section({
89
+ name: 'hyperliquid-desk',
90
+ order: 500,
91
+ text: () => {
92
+ const mode = remote.cfg.tradingMode
93
+ const net = remote.cfg.network
94
+ const lines = [
95
+ '## Hyperliquid 交易台(dsh-hyperliquid)',
96
+ `本 Harness 装有 Hyperliquid 交易台(mode=${mode},network=${net})。用户可在面板看行情并填写交易票据;票据只把意图送入当前会话,所有实际下单只能由你调用工具完成。`,
97
+ '可用工具:hyper_context(会话交易上下文)、hyper_market(详细行情)、hyper_chart(在行情图上画支撑/阻力与趋势线)、hyper_account(账户)、hyper_decide(交易决策预检并返回一次性 decisionId)、hyper_order(执行/撤单/杠杆)、hyper_session(会话视角)。',
98
+ '用户说"当前/界面上这个币"时,先 hyper_context 或 hyper_session status;需要技术判断时用 hyper_market indicators 验证趋势、动量、波动和支撑阻力,可用 hyper_chart 将依据画给用户看。交易前核对行情与账户,说明方向、数量、依据及风险。paper 模式用 hyper_order 执行;live 模式先用 hyper_decide 预检,并用返回的一次性 decisionId 执行 hyper_order。是否可自主执行由宿主配置和钱包策略决定。',
99
+ ]
100
+ if (skillRemove) lines.push('复杂的 Hyperliquid 分析、交易或异常对账,请先加载 hyperliquid-trading skill。')
101
+ // Live desk focus: what coin/interval the desk shows right now.
102
+ try {
103
+ const desk = remote.desk ?? {}
104
+ if (desk.coin) lines.push(`当前交易台焦点:${desk.coin} · K线 ${desk.interval ?? '15m'}。用户消息里说"这个币/当前价格/图上"时指的就是它。`)
105
+ } catch { /* desk context is best-effort */ }
106
+ return lines.join('\n')
107
+ },
108
+ })
109
+ disposers.push(remove)
110
+ }
111
+
112
+ return () => {
113
+ for (const d of disposers) {
114
+ try { d() } catch { /* best effort */ }
115
+ }
116
+ remote.stopWs()
117
+ }
118
+ }
@@ -0,0 +1,23 @@
1
+ export type DrawingKind = 'level' | 'hray' | 'vline' | 'text' | 'trend' | 'ray' | 'extended' | 'fib' | 'rect' | 'measure' | 'channel' | 'long' | 'short' | 'fibext' | 'fibchannel' | 'fibtime' | 'gannfan' | 'pitchfork' | 'regression' | 'arrow' | 'callout' | 'circle' | 'triangle' | 'brush' | 'crossline' | 'pricelabel' | 'infoline' | 'trendangle' | 'pricerange' | 'daterange' | 'elliott' | 'elliottabc' | 'xabcd' | 'abcd' | 'headshoulders' | 'gannbox' | 'gannsquare' | 'schiff' | 'modschiff' | 'icon'
2
+ export type LineStyleName = 'solid' | 'dashed' | 'dotted'
3
+ export interface DrawingStyle { color?: string; lineWidth?: number; lineStyle?: LineStyleName; locked?: boolean; hidden?: boolean; shared?: boolean; alert?: boolean }
4
+ export declare const DRAWING_POINTS: Record<DrawingKind, number>
5
+ export declare const DRAWING_KINDS: DrawingKind[]
6
+ export declare const DRAWING_CLICKS: Record<DrawingKind, number>
7
+ export declare const LINE_STYLES: LineStyleName[]
8
+ export declare function drawingStyle(request: unknown): DrawingStyle
9
+ export declare const FIB_LEVELS: number[]
10
+ export declare function drawingGeometry(kind: string, request: unknown, now?: number): Record<string, unknown>
11
+ export declare const BRUSH_MIN: number
12
+ export declare const BRUSH_MAX: number
13
+ export declare const TEXT_KINDS: DrawingKind[]
14
+ export declare const FIB_EXT_LEVELS: number[]
15
+ export declare const FIB_TIME_SERIES: number[]
16
+ export declare const GANN_RATIOS: Array<[number, number]>
17
+ export declare const ICON_NAMES: string[]
18
+ export declare const GANN_BOX_LEVELS: number[]
19
+ export declare const STICKERS: Record<'trading' | 'faces' | 'symbols', string[]>
20
+ export declare function isIconLabel(label: unknown): boolean
21
+ export declare const ALERT_KINDS: DrawingKind[]
22
+ export declare function linePriceAt(mark: unknown, time: number): number | null
23
+ export declare function isSingleEmoji(value: unknown): boolean
@@ -0,0 +1,159 @@
1
+ /**
2
+ * dsh-hyperliquid — chart drawing kinds shared by the host (validation, the
3
+ * hyper_chart tool) and the browser chart (tools menu, rendering).
4
+ *
5
+ * level / trend keep their original flat fields (price; fromTime…toPrice);
6
+ * every other kind stores `points: [{ time, price }]` in click order.
7
+ * @module lib/chart-drawings
8
+ */
9
+
10
+ /** kind → number of anchor points (level: a price only). */
11
+ export const DRAWING_POINTS = {
12
+ level: 1, hray: 1, vline: 1, text: 1, trend: 2, ray: 2, extended: 2, fib: 2, rect: 2, measure: 2, channel: 3,
13
+ // Positions store [entry, stop, target]; the desk creates them with 2 clicks (target defaults to 2R).
14
+ long: 3, short: 3,
15
+ // Fibonacci family, Gann, pitchfork, regression.
16
+ fibext: 3, fibchannel: 3, fibtime: 2, gannfan: 2, pitchfork: 3, regression: 2,
17
+ // Annotation and shapes. brush is freehand: 0 = variable (BRUSH_MIN..BRUSH_MAX points).
18
+ arrow: 2, callout: 2, circle: 2, triangle: 3, brush: 0,
19
+ // More lines and measures: cross line, price label, info line, trend angle, price-only and date-only ranges.
20
+ crossline: 1, pricelabel: 1, infoline: 2, trendangle: 2, pricerange: 2, daterange: 2,
21
+ // Advanced: Elliott waves, harmonic / chart patterns, Gann box & square, pitchfork variants, icons.
22
+ elliott: 6, elliottabc: 4, xabcd: 5, abcd: 4, headshoulders: 7,
23
+ gannbox: 2, gannsquare: 2, schiff: 3, modschiff: 3, icon: 1,
24
+ }
25
+ /** Icon stickers (the drawing's label holds the icon name). */
26
+ export const ICON_NAMES = ['flag', 'star', 'up', 'down', 'warning', 'check', 'cross', 'fire', 'target', 'bell', 'pin', 'clock', 'dollar', 'shield', 'bolt', 'heart', 'question', 'info']
27
+ /** Sticker library (TradingView's emoji stickers): category → emoji. An icon label is a name or `emoji:<char>`. */
28
+ export const STICKERS = {
29
+ trading: ['📈', '📉', '💰', '💸', '🚀', '🎯', '🔥', '💎', '🐂', '🐻', '🐋', '🚨', '🔔', '⏰', '📌', '🧭', '🛑', '🏦', '⚖️', '🪙'],
30
+ faces: ['😀', '😂', '😎', '🤔', '😱', '😭', '😡', '🥳', '🤯', '🙏', '👍', '👎', '👀', '💪', '🫡', '🤝'],
31
+ symbols: ['⭐', '❗', '❓', '💡', '🔒', '🔓', '⛔', '🟢', '🔴', '🟡', '⬆️', '⬇️', '↗️', '↘️', '🏁', '🎉', '✅', '❌', '⚠️', '♻️', '⏸️', '▶️'],
32
+ }
33
+ const STICKER_SET = new Set(Object.values(STICKERS).flat())
34
+ /** Whether an icon drawing's label names a built-in icon or a library sticker. */
35
+ export function isIconLabel(label) {
36
+ if (ICON_NAMES.includes(label)) return true
37
+ if (typeof label !== 'string' || !label.startsWith('emoji:')) return false
38
+ const emoji = label.slice(6)
39
+ return STICKER_SET.has(emoji) || isSingleEmoji(emoji)
40
+ }
41
+
42
+ /** Any one emoji (a single grapheme that is pictographic, or a regional-indicator flag), so the whole emoji set works as stickers. */
43
+ export function isSingleEmoji(value) {
44
+ if (typeof value !== 'string' || !value || value.length > 32 || !/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(value)) return false
45
+ return [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(value)].length === 1
46
+ }
47
+ /** Gann box divisions on both axes (TradingView defaults). */
48
+ export const GANN_BOX_LEVELS = [0, 0.25, 0.382, 0.5, 0.618, 0.75, 1]
49
+ /** Drawings a price can cross: alerts fire when the mid price crosses them (once, like TradingView). */
50
+ export const ALERT_KINDS = ['level', 'hray', 'trend', 'ray', 'extended']
51
+
52
+ /** The line's price at `time`, or null when the drawing does not exist at that time. */
53
+ export function linePriceAt(mark, time) {
54
+ if (mark.kind === 'level') return Number(mark.price)
55
+ const [a, b] = mark.kind === 'trend'
56
+ ? [{ time: mark.fromTime, price: mark.fromPrice }, { time: mark.toTime, price: mark.toPrice }]
57
+ : mark.points ?? []
58
+ if (!a) return null
59
+ if (mark.kind === 'hray') return time >= a.time ? a.price : null
60
+ if (!b || b.time === a.time) return null
61
+ if (mark.kind === 'trend' && (time < Math.min(a.time, b.time) || time > Math.max(a.time, b.time))) return null
62
+ if (mark.kind === 'ray' && (time - a.time) * (b.time - a.time) < 0) return null
63
+ return a.price + (b.price - a.price) * (time - a.time) / (b.time - a.time)
64
+ }
65
+
66
+ export const BRUSH_MIN = 2
67
+ export const BRUSH_MAX = 500
68
+ /** Kinds whose label is free text shown on the chart (up to 200 characters). */
69
+ export const TEXT_KINDS = ['text', 'callout']
70
+ /** Clicks that create a drawing when they differ from its stored anchor count. */
71
+ export const DRAWING_CLICKS = { ...Object.fromEntries(Object.entries(DRAWING_POINTS)), long: 2, short: 2 }
72
+ export const DRAWING_KINDS = Object.keys(DRAWING_POINTS)
73
+ /** Fibonacci retracement levels (0 = second point, 1 = first point, as in TradingView). */
74
+ export const FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]
75
+ /** Trend-based Fibonacci extension levels (projected from the third point, TradingView defaults). */
76
+ export const FIB_EXT_LEVELS = [0, 0.382, 0.618, 1, 1.272, 1.618, 2, 2.618]
77
+ /** Fibonacci time zones: bar multiples of the first two points' spacing. */
78
+ export const FIB_TIME_SERIES = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
79
+ /** Gann fan angles as price/time ratios relative to the 1×1 line. */
80
+ export const GANN_RATIOS = [[1, 8], [1, 4], [1, 3], [1, 2], [1, 1], [2, 1], [3, 1], [4, 1], [8, 1]]
81
+
82
+ const OLDEST = Date.UTC(2015, 0, 1)
83
+
84
+ /**
85
+ * Validate the geometry of one drawing request (throws on invalid input).
86
+ * @returns the geometry fields to persist for that kind.
87
+ */
88
+ export function drawingGeometry(kind, request, now = Date.now()) {
89
+ if (!DRAWING_KINDS.includes(kind)) throw new Error(`画线类型必须为 ${DRAWING_KINDS.join('/')}`)
90
+ const latest = now + 365 * 86_400_000
91
+ const point = (time, price, label) => {
92
+ const t = Number(time), p = Number(price)
93
+ if (!Number.isFinite(t) || t < OLDEST || t > latest) throw new Error(`${label}时间必须为 2015 年至今后一年内的毫秒时间戳`)
94
+ if (!Number.isFinite(p) || p <= 0) throw new Error(`${label}价格必须为正数`)
95
+ return { time: t, price: p }
96
+ }
97
+ if (kind === 'level') {
98
+ const price = Number(request?.price)
99
+ if (!Number.isFinite(price) || price <= 0) throw new Error('水平线 price 必须为正数')
100
+ return { price }
101
+ }
102
+ if (kind === 'trend') {
103
+ const from = point(request?.fromTime, request?.fromPrice, '趋势线起点')
104
+ const to = point(request?.toTime, request?.toPrice, '趋势线终点')
105
+ if (to.time <= from.time) throw new Error('趋势线终点时间必须晚于起点')
106
+ return { fromTime: from.time, fromPrice: from.price, toTime: to.time, toPrice: to.price }
107
+ }
108
+ const need = DRAWING_POINTS[kind]
109
+ const raw = request?.points
110
+ if (kind === 'brush') {
111
+ if (!Array.isArray(raw) || raw.length < BRUSH_MIN || raw.length > BRUSH_MAX) throw new Error(`brush 需要 ${BRUSH_MIN}-${BRUSH_MAX} 个 points`)
112
+ } else if (!Array.isArray(raw) || raw.length !== need) throw new Error(`${kind} 需要 ${need} 个 points({ time, price })`)
113
+ const points = raw.map((item, index) => point(item?.time, item?.price, `第 ${index + 1} 个点`))
114
+ if (['trend', 'ray', 'extended', 'channel', 'fibchannel', 'gannfan', 'regression', 'fibtime', 'infoline', 'trendangle'].includes(kind) && points[0].time === points[1].time) throw new Error('前两个点的时间不能相同')
115
+ if ((kind === 'regression' || kind === 'fibtime' || kind === 'gannfan') && points[1].time < points[0].time) throw new Error('第二个点必须晚于第一个点')
116
+ if (kind === 'long' || kind === 'short') {
117
+ const [entry, stop, target] = points.map((item) => item.price)
118
+ const ok = kind === 'long' ? stop < entry && entry < target : target < entry && entry < stop
119
+ if (!ok) throw new Error(kind === 'long' ? '多头头寸需要 止损 < 入场 < 止盈' : '空头头寸需要 止盈 < 入场 < 止损')
120
+ if (points[1].time <= points[0].time) throw new Error('头寸右边界时间必须晚于入场时间')
121
+ }
122
+ return { points }
123
+ }
124
+
125
+ const COLOR = /^#[0-9a-fA-F]{6}$/
126
+ export const LINE_STYLES = ['solid', 'dashed', 'dotted']
127
+
128
+ /** Validate optional appearance fields; absent keys are left untouched. */
129
+ export function drawingStyle(request) {
130
+ const style = {}
131
+ if (request?.color != null) {
132
+ if (typeof request.color !== 'string' || !COLOR.test(request.color)) throw new Error('画线颜色必须为 #RRGGBB')
133
+ style.color = request.color.toLowerCase()
134
+ }
135
+ if (request?.lineWidth != null) {
136
+ const width = Number(request.lineWidth)
137
+ if (!Number.isInteger(width) || width < 1 || width > 4) throw new Error('线宽必须为 1-4 的整数')
138
+ style.lineWidth = width
139
+ }
140
+ if (request?.lineStyle != null) {
141
+ if (!LINE_STYLES.includes(request.lineStyle)) throw new Error(`线型必须为 ${LINE_STYLES.join('/')}`)
142
+ style.lineStyle = request.lineStyle
143
+ }
144
+ for (const flag of ['shared', 'alert']) {
145
+ if (request?.[flag] == null) continue
146
+ if (typeof request[flag] !== 'boolean') throw new Error(`${flag} 必须为布尔值`)
147
+ style[flag] = request[flag]
148
+ }
149
+ if (style.alert && request?.kind != null && !ALERT_KINDS.includes(request.kind)) throw new Error(`只有 ${ALERT_KINDS.join('/')} 支持价格提醒`)
150
+ if (request?.hidden != null) {
151
+ if (typeof request.hidden !== 'boolean') throw new Error('hidden 必须为布尔值')
152
+ style.hidden = request.hidden
153
+ }
154
+ if (request?.locked != null) {
155
+ if (typeof request.locked !== 'boolean') throw new Error('locked 必须为布尔值')
156
+ style.locked = request.locked
157
+ }
158
+ return style
159
+ }