dsh-hyperliquid 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/studies.js ADDED
@@ -0,0 +1,157 @@
1
+ /** Shared chart/agent study definitions. All values are derived from the supplied candle window. */
2
+ export const INDICATOR_IDS = ['volume', 'sma20', 'sma60', 'ema20', 'ema50', 'bollinger20', 'rsi14', 'macd', 'atr14', 'vwap', 'stochastic', 'adx', 'obv']
3
+ export const DEFAULT_INDICATOR_SETTINGS = {
4
+ rsi: { period: 14, lower: 30, upper: 70 }, macd: { fast: 12, slow: 26, signal: 9 },
5
+ bollinger: { period: 20, multiplier: 2 }, stochastic: { period: 14, smoothK: 3, smoothD: 3, lower: 20, upper: 80 },
6
+ adx: { period: 14, smoothing: 14 },
7
+ }
8
+ export function normalizeIndicatorSettings(input = {}) {
9
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some((key) => !Object.hasOwn(DEFAULT_INDICATOR_SETTINGS, key))) throw Error('指标参数无效')
10
+ const result = {}
11
+ for (const [section, defaults] of Object.entries(DEFAULT_INDICATOR_SETTINGS)) {
12
+ const supplied = input[section] ?? {}
13
+ if (typeof supplied !== 'object' || Array.isArray(supplied) || Object.keys(supplied).some((key) => !Object.hasOwn(defaults, key))) throw Error('指标参数字段无效')
14
+ const values = { ...defaults, ...supplied }
15
+ for (const [key, value] of Object.entries(values)) {
16
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw Error(`${section}.${key} 必须为数值`)
17
+ if (key === 'multiplier') { if (value <= 0 || value > 10) throw Error('布林带倍数必须大于 0 且不超过 10') }
18
+ else if (key === 'lower' || key === 'upper') { if (value < 0 || value > 100) throw Error('指标阈值必须为 0–100') }
19
+ else if (!Number.isInteger(value) || value < 1 || value > 500) throw Error('指标周期必须为 1–500 的整数')
20
+ }
21
+ if ('lower' in values && values.lower >= values.upper) throw Error('指标下阈值必须小于上阈值')
22
+ result[section] = values
23
+ }
24
+ if (result.macd.fast >= result.macd.slow) throw Error('MACD 快线周期必须小于慢线周期')
25
+ return result
26
+ }
27
+ function nullableSma(values, period) {
28
+ return values.map((_, i) => {
29
+ if (i < period - 1) return null
30
+ const window = values.slice(i - period + 1, i + 1)
31
+ return window.some((value) => value == null) ? null : window.reduce((sum, value) => sum + value, 0) / period
32
+ })
33
+ }
34
+ function rma(values, period) {
35
+ let prior = null, seed = []
36
+ return values.map((value) => {
37
+ if (value == null) return null
38
+ if (prior == null) { seed.push(value); if (seed.length < period) return null; prior = seed.reduce((sum, item) => sum + item, 0) / period }
39
+ else prior = (prior * (period - 1) + value) / period
40
+ return prior
41
+ })
42
+ }
43
+ export function studyDefinitions(input) {
44
+ const p = normalizeIndicatorSettings(input)
45
+ return [
46
+ ['sma20', 'sma20', 'SMA20'], ['sma60', 'sma60', 'MA60'], ['ema20', 'ema20', 'EMA20'], ['ema50', 'ema50', 'EMA50'],
47
+ ['bollinger20', 'basis', `BOLL ${p.bollinger.period} × ${p.bollinger.multiplier}`], ['bollinger20', 'upper', 'BOLL upper'], ['bollinger20', 'lower', 'BOLL lower'],
48
+ ['rsi14', 'rsi14', `RSI${p.rsi.period}`], ['macd', 'macd', p.macd.fast === 12 && p.macd.slow === 26 && p.macd.signal === 9 ? 'MACD' : `MACD ${p.macd.fast}/${p.macd.slow}/${p.macd.signal}`], ['macd', 'signal', 'Signal'], ['macd', 'histogram', 'Hist'],
49
+ ['atr14', 'atr14', 'ATR14'], ['vwap', 'vwap', 'VWAP (UTC HLC3)'], ['stochastic', 'stochK', `Stoch %K ${p.stochastic.period}/${p.stochastic.smoothK}`], ['stochastic', 'stochD', `Stoch %D ${p.stochastic.smoothD}`],
50
+ ['adx', 'adx', `ADX ${p.adx.period}/${p.adx.smoothing}`], ['adx', 'plusDI', '+DI'], ['adx', 'minusDI', '-DI'], ['obv', 'obv', 'OBV'],
51
+ ]
52
+ }
53
+ function points(rows, values) {
54
+ return values.flatMap((value, index) => value == null || !Number.isFinite(value)
55
+ ? [] : [{ time: Math.floor(rows[index].t / 1000), value }])
56
+ }
57
+
58
+ function sma(values, period) {
59
+ let sum = 0
60
+ return values.map((value, index) => {
61
+ sum += value
62
+ if (index >= period) sum -= values[index - period]
63
+ return index >= period - 1 ? sum / period : null
64
+ })
65
+ }
66
+
67
+ function ema(values, period) {
68
+ const result = Array(values.length).fill(null)
69
+ const alpha = 2 / (period + 1)
70
+ let seed = []
71
+ let prior = null
72
+ for (let i = 0; i < values.length; i += 1) {
73
+ const value = values[i]
74
+ if (value == null) continue
75
+ if (prior == null) {
76
+ seed.push(value)
77
+ if (seed.length < period) continue
78
+ prior = seed.reduce((total, current) => total + current, 0) / period
79
+ } else prior = value * alpha + prior * (1 - alpha)
80
+ result[i] = prior
81
+ }
82
+ return result
83
+ }
84
+
85
+ export function computeStudies(candles, input) {
86
+ const settings = normalizeIndicatorSettings(input)
87
+ const rows = candles.filter((row) => Number.isFinite(row.c) && row.c > 0 && Number.isFinite(row.t))
88
+ const closes = rows.map((row) => row.c)
89
+ const mean = sma(closes, settings.bollinger.period)
90
+ const ema12 = ema(closes, settings.macd.fast)
91
+ const ema26 = ema(closes, settings.macd.slow)
92
+ const macd = ema12.map((value, index) => value == null || ema26[index] == null ? null : value - ema26[index])
93
+ const signal = ema(macd, settings.macd.signal)
94
+ const deviation = mean.map((value, index) => value == null ? null : Math.sqrt(
95
+ closes.slice(index - settings.bollinger.period + 1, index + 1).reduce((total, close) => total + (close - value) ** 2, 0) / settings.bollinger.period,
96
+ ))
97
+ const rsi = Array(rows.length).fill(null)
98
+ let gain = 0; let loss = 0
99
+ for (let i = 1; i < rows.length; i += 1) {
100
+ const delta = closes[i] - closes[i - 1]
101
+ const up = Math.max(delta, 0); const down = Math.max(-delta, 0)
102
+ if (i <= settings.rsi.period) { gain += up; loss += down }
103
+ else { gain = (gain * (settings.rsi.period - 1) + up) / settings.rsi.period; loss = (loss * (settings.rsi.period - 1) + down) / settings.rsi.period }
104
+ if (i === settings.rsi.period) { gain /= settings.rsi.period; loss /= settings.rsi.period }
105
+ if (i >= settings.rsi.period) rsi[i] = loss === 0 ? gain === 0 ? 50 : 100 : 100 - 100 / (1 + gain / loss)
106
+ }
107
+ const point = (values) => points(rows, values)
108
+ const atr = Array(rows.length).fill(null)
109
+ let rangeSum = 0
110
+ for (let i = 1; i < rows.length; i += 1) {
111
+ const range = Math.max(rows[i].h - rows[i].l, Math.abs(rows[i].h - rows[i - 1].c), Math.abs(rows[i].l - rows[i - 1].c))
112
+ if (i <= 14) rangeSum += range
113
+ if (i === 14) atr[i] = rangeSum / 14
114
+ if (i > 14) atr[i] = (atr[i - 1] * 13 + range) / 14
115
+ }
116
+ // UTC sessions, HLC3 weighted by base volume. A partial first day is omitted.
117
+ let day = -1, weighted = 0, volume = 0, complete = false
118
+ const vwap = rows.map((row) => {
119
+ const nextDay = Math.floor(row.t / 86_400_000)
120
+ if (nextDay !== day) {
121
+ // After the first observed day, the exchange can legitimately skip empty
122
+ // midnight bars (e.g. closed HIP-3 markets); the next available bar starts it.
123
+ complete = day >= 0 || row.t % 86_400_000 === 0
124
+ day = nextDay; weighted = 0; volume = 0
125
+ }
126
+ if (!Number.isFinite(row.v) || row.v < 0) return null
127
+ weighted += (row.h + row.l + row.c) / 3 * row.v
128
+ volume += row.v
129
+ return complete && volume > 0 ? weighted / volume : null
130
+ })
131
+ const rawK = rows.map((row, index) => {
132
+ if (index < settings.stochastic.period - 1) return null
133
+ const window = rows.slice(index - settings.stochastic.period + 1, index + 1)
134
+ const low = Math.min(...window.map((bar) => bar.l)), high = Math.max(...window.map((bar) => bar.h))
135
+ return high === low ? null : 100 * (row.c - low) / (high - low)
136
+ })
137
+ const stochK = nullableSma(rawK, settings.stochastic.smoothK)
138
+ const stochD = nullableSma(stochK, settings.stochastic.smoothD)
139
+ const tr = rows.map((row, i) => i === 0 ? null : Math.max(row.h - row.l, Math.abs(row.h - rows[i - 1].c), Math.abs(row.l - rows[i - 1].c)))
140
+ const plus = rows.map((row, i) => { if (!i) return null; const up = row.h - rows[i - 1].h, down = rows[i - 1].l - row.l; return up > down && up > 0 ? up : 0 })
141
+ const minus = rows.map((row, i) => { if (!i) return null; const up = row.h - rows[i - 1].h, down = rows[i - 1].l - row.l; return down > up && down > 0 ? down : 0 })
142
+ const atrDm = rma(tr, settings.adx.period), plusRma = rma(plus, settings.adx.period), minusRma = rma(minus, settings.adx.period)
143
+ const di = (values) => values.map((value, i) => value == null || atrDm[i] == null ? null : atrDm[i] === 0 ? 0 : 100 * value / atrDm[i])
144
+ const plusDI = di(plusRma), minusDI = di(minusRma)
145
+ const dx = plusDI.map((value, i) => value == null || minusDI[i] == null ? null : value + minusDI[i] === 0 ? 0 : 100 * Math.abs(value - minusDI[i]) / (value + minusDI[i]))
146
+ let balance = 0
147
+ const obv = rows.map((row, i) => { if (i) balance += Math.sign(row.c - rows[i - 1].c) * row.v; return balance })
148
+ return {
149
+ stochK: point(stochK), stochD: point(stochD), adx: point(rma(dx, settings.adx.smoothing)), plusDI: point(plusDI), minusDI: point(minusDI), obv: point(obv),
150
+ sma20: point(sma(closes, 20)), basis: point(mean), sma60: point(sma(closes, 60)), ema20: point(ema(closes, 20)), ema50: point(ema(closes, 50)),
151
+ upper: point(mean.map((value, index) => value == null ? null : value + settings.bollinger.multiplier * deviation[index])),
152
+ lower: point(mean.map((value, index) => value == null ? null : value - settings.bollinger.multiplier * deviation[index])),
153
+ rsi14: point(rsi), macd: point(macd), signal: point(signal),
154
+ histogram: point(macd.map((value, index) => value == null || signal[index] == null ? null : value - signal[index])),
155
+ atr14: point(atr), vwap: point(vwap),
156
+ }
157
+ }
package/lib/tools.js CHANGED
@@ -8,9 +8,11 @@
8
8
 
9
9
  import { closedCandles, computeIndicators } from './indicators.js'
10
10
  import { MA_KINDS, MA_SOURCES, maLabel, maLatest, normalizeMaLine, normalizeMaLines } from './moving-average.js'
11
- import { DRAWING_KINDS } from './chart-drawings.js'
11
+ import { INDICATOR_IDS, computeStudies, normalizeIndicatorSettings, studyDefinitions } from './studies.js'
12
+ import { reviewLedger } from './review.js'
13
+ import { DRAWING_KINDS, volumeProfile } from './chart-drawings.js'
12
14
 
13
- const DRAWING_NAMES = { level: '水平线', hray: '水平射线', vline: '垂直线', text: '文本', trend: '趋势线', ray: '射线', extended: '延长线', channel: '平行通道', fib: '斐波那契回撤', rect: '矩形', measure: '价格区间', long: '多头头寸', short: '空头头寸', fibext: '斐波那契扩展', fibchannel: '斐波那契通道', fibtime: '斐波那契时间周期', gannfan: '江恩扇形', pitchfork: '安德鲁叉子线', regression: '回归趋势', arrow: '箭头', callout: '标注气泡', circle: '圆形', triangle: '三角形', brush: '画笔', crossline: '十字线', pricelabel: '价格标签', infoline: '信息线', trendangle: '趋势角度', pricerange: '价格区间', daterange: '日期区间', elliott: '艾略特推动浪', elliottabc: '艾略特调整浪', xabcd: 'XABCD 谐波', abcd: 'ABCD 形态', headshoulders: '头肩形态', gannbox: '江恩箱', gannsquare: '江恩方格', schiff: 'Schiff 叉子线', modschiff: '改良 Schiff 叉子线', icon: '图标' }
15
+ const DRAWING_NAMES = { vprofile: '固定范围成交量分布', level: '水平线', hray: '水平射线', vline: '垂直线', text: '文本', trend: '趋势线', ray: '射线', extended: '延长线', channel: '平行通道', fib: '斐波那契回撤', rect: '矩形', measure: '价格区间', long: '多头头寸', short: '空头头寸', fibext: '斐波那契扩展', fibchannel: '斐波那契通道', fibtime: '斐波那契时间周期', gannfan: '江恩扇形', pitchfork: '安德鲁叉子线', regression: '回归趋势', arrow: '箭头', callout: '标注气泡', circle: '圆形', triangle: '三角形', brush: '画笔', crossline: '十字线', pricelabel: '价格标签', infoline: '信息线', trendangle: '趋势角度', pricerange: '价格区间', daterange: '日期区间', elliott: '艾略特推动浪', elliottabc: '艾略特调整浪', xabcd: 'XABCD 谐波', abcd: 'ABCD 形态', headshoulders: '头肩形态', gannbox: '江恩箱', gannsquare: '江恩方格', schiff: 'Schiff 叉子线', modschiff: '改良 Schiff 叉子线', icon: '图标' }
14
16
  import { canonicalMarketCoin } from './market-symbol.js'
15
17
 
16
18
  /** Resolve the calling session id from a tool run context. */
@@ -19,6 +21,8 @@ function sessionIdOf(exec) {
19
21
  return agent?.id ?? agent?.session?.id ?? null
20
22
  }
21
23
 
24
+ const INTERVAL_MS = { '1m': 60e3, '3m': 180e3, '5m': 300e3, '15m': 900e3, '30m': 1800e3, '1h': 3600e3, '2h': 7200e3, '4h': 14400e3, '8h': 28800e3, '12h': 43200e3, '1d': 86400e3, '3d': 259200e3, '1w': 604800e3, '1M': 2592000e3 }
25
+
22
26
  function fmt(n, d = 2) {
23
27
  if (n == null) return '—'
24
28
  const v = Number(n)
@@ -72,7 +76,7 @@ function buildTools(remote) {
72
76
  const change24hPct = Number(market.ctx?.prevDayPx) > 0 && mark != null ? (mark / Number(market.ctx.prevDayPx) - 1) * 100 : null
73
77
  const context = {
74
78
  workspace: workspace ? { interval: workspace.interval, bookGrouping: workspace.bookGrouping,
75
- chartTab: workspace.chartTab, indicators: workspace.indicators, priceScale: workspace.priceScale } : null,
79
+ chartTab: workspace.chartTab, chartType: workspace.chartType ?? 'candles', indicators: workspace.indicators, indicatorSettings: normalizeIndicatorSettings(workspace.indicatorSettings), research: workspace.research ?? null, priceScale: workspace.priceScale } : null,
76
80
  sessionId, coin, interval, marketKind: detail?.market?.kind ?? 'perp', tradable, network: markets.network, mode: account.mode,
77
81
  mark, mid: midFresh ? mid : null, change24hPct, funding1h: market.ctx?.funding != null ? Number(market.ctx.funding) : null,
78
82
  volume24hUsd: market.ctx?.dayNtlVlm != null ? Number(market.ctx.dayNtlVlm) : null,
@@ -100,14 +104,14 @@ function buildTools(remote) {
100
104
  : bookAge == null || marketAge == null || bookAge > 10 || marketAge > 60 ? '只读行情过期/缺失' : '只读行情可用'
101
105
  const text = [
102
106
  `${coin} ${interval} · ${detail?.market?.kind ?? 'perp'}${tradable ? '' : '(只读市场)'} · ${account.mode}/${markets.network} · 读取 ${new Date(context.assembledAt).toISOString()} · 盘口 ${bookAge ?? '?'}s / ${tradable ? 'mid ' + (midsAge ?? '?') : '市场上下文 ' + (marketAge ?? '?')}s · ${freshness}`,
103
- workspace ? `会话图表:${workspace.chartTab} · 指标=${workspace.indicators.join(',') || '无'} · 刻度=${workspace.priceScale} · 盘口精度=${workspace.bookGrouping}` : '',
107
+ workspace ? `会话图表:${workspace.chartTab}/${workspace.chartType ?? 'candles'} · 指标=${workspace.indicators.join(',') || '无'} · 刻度=${workspace.priceScale} · 盘口精度=${workspace.bookGrouping}` : '',
104
108
  `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`,
105
109
  `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}` : ''}`,
106
110
  !tradable ? account.mode === 'paper'
107
111
  ? '当前账户数据不代表该只读市场的余额、仓位或可交易额度。'
108
112
  : `当前市场仅可分析;${context.positionsCoverage === 'all-dexs' ? '仓位来自全部永续 DEX 快照' : '账户仓位仅覆盖所选 DEX'}${position ? `;当前仓位=${fmt(position.size, 6)} ${coin}` : ''},不能据此在该市场下单。`
109
113
  : position ? `仓位 ${position.size > 0 ? 'LONG' : 'SHORT'} ${fmt(Math.abs(position.size), 6)} ${coin} entry=${fmt(position.entryPx)} uPnL=${fmt(position.uPnl)}` : '当前币对无持仓',
110
- `风险:允许币对=${(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' ? ' · 模拟盘固定滑点,未模拟盘口深度、手续费、资金费或强平' : ''}`,
114
+ `风险:允许币对=${(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' ? ' · 模拟盘配置的滑点与手续费,未模拟盘口深度、资金费或强平' : ''}`,
111
115
  `近 ${candles.length} 根 K 线价格(末根可能未收盘): ${candles.map((c) => fmt(c.c)).join(', ')}`,
112
116
  ].join('\n')
113
117
  return { text, context }
@@ -117,7 +121,7 @@ function buildTools(remote) {
117
121
  name: 'hyper_market',
118
122
  description:
119
123
  '查询 Hyperliquid 核心永续、HIP-3、现货与 Outcome 行情。action=overview 列出核心永续的 24h 涨跌/成交/资金费;action=book 查盘口;' +
120
- 'action=catalog 搜索所有市场,支持官方 perpCategories 分类;action=candles 查 K 线(支持 Hyperliquid 官方 1m 至 1M 的 14 种周期);action=indicators 计算 SMA/EMA/RSI/MACD/布林带/ATR/支撑阻力;action=trades 查最近成交。非核心永续目前只读。',
124
+ 'action=catalog 搜索所有市场,支持官方 perpCategories 分类;action=candles 查 K 线(支持 Hyperliquid 官方 1m 至 1M 的 14 种周期);action=indicators 计算 SMA/EMA/RSI/MACD/布林带/ATR/Stochastic/ADX/OBV/支撑阻力,遵循当前会话指标参数;action=trades 查最近成交。非核心永续目前只读。',
121
125
  parameters: {
122
126
  type: 'object',
123
127
  properties: {
@@ -182,23 +186,29 @@ function buildTools(remote) {
182
186
  if (action === 'indicators') {
183
187
  const interval = args?.interval ?? '1h'
184
188
  const { candles } = await remote.candles({ coin, interval })
189
+ let session = null
190
+ try { session = sessionIdOf(exec) } catch { /* not a trading session */ }
191
+ const workspace = session && remote.store.getSession?.(session) ? remote.store.getSessionWorkspace(session) : null
192
+ const settings = normalizeIndicatorSettings(workspace?.indicatorSettings)
193
+ const closedWindow = closedCandles(candles, { interval })
194
+ const closed = closedWindow.rows
195
+ const studies = computeStudies(closed, settings)
196
+ const configured = { settings, values: Object.fromEntries(Object.entries(studies).map(([key, rows]) => [key, rows.at(-1)?.time === Math.floor((closed.at(-1)?.t ?? 0) / 1000) ? rows.at(-1).value : null])), candlesUsed: closed.length, observedAt: closed.length ? closedWindow.closeTime(closed.at(-1)) : null }
185
197
  const indicators = computeIndicators(candles, { interval })
186
- if (!indicators.ready) return { text: `${coin} ${interval} 指标数据不足:${indicators.reason};已排除 ${indicators.excludedOpen} 根未收盘 K 线`, indicators }
198
+ if (!indicators.ready) return { text: `${coin} ${interval} 指标数据不足:${indicators.reason};已排除 ${indicators.excludedOpen} 根未收盘 K 线`, indicators, configured }
187
199
  const ageMin = Math.max(0, Math.round((Date.now() - indicators.observedAt) / 60_000))
188
200
  const text = [
189
201
  `${coin} ${interval} · ${indicators.candlesUsed} 根已收盘 K 线 · 最新收盘 ${new Date(indicators.observedAt).toISOString()}(${ageMin} 分钟前) · 排除 ${indicators.excludedOpen} 根形成中 K 线`,
190
202
  `收盘=${fmt(indicators.close)} SMA20=${fmt(indicators.sma20)} EMA20=${fmt(indicators.ema20)} EMA50=${fmt(indicators.ema50)}`,
191
- `RSI14=${fmt(indicators.rsi14, 1)} MACD=${fmt(indicators.macd.line, 4)} signal=${fmt(indicators.macd.signal, 4)} hist=${fmt(indicators.macd.histogram, 4)}`,
192
- `布林带下/中/上=${fmt(indicators.bollinger20.lower)}/${fmt(indicators.bollinger20.middle)}/${fmt(indicators.bollinger20.upper)} ATR14=${fmt(indicators.atr14)} 量比20=${fmt(indicators.volumeRatio20, 2)}`,
203
+ '会话参数(已收盘):' + studyDefinitions(settings).filter(([id]) => ['rsi14', 'macd', 'bollinger20', 'stochastic', 'adx', 'obv'].includes(id)).map(([, key, label]) => `${label}=${configured.values[key] == null ? '数据不足' : fmt(configured.values[key], 4)}`).join(' · '),
204
+ `ATR14=${fmt(indicators.atr14)} 量比20=${fmt(indicators.volumeRatio20, 2)}`,
193
205
  `邻近支撑=${fmt(indicators.support.price)}(${new Date(indicators.support.time).toISOString()}) 阻力=${fmt(indicators.resistance.price)}(${new Date(indicators.resistance.time).toISOString()})`,
194
206
  ]
195
- let session = null
196
- try { session = sessionIdOf(exec) } catch { /* not a trading session */ }
197
207
  const requested = Array.isArray(args?.ma) ? normalizeMaLines(args.ma) : (session && remote.store.getSession?.(session) ? remote.store.getSessionWorkspace(session).maLines ?? [] : [])
198
208
  const ma = requested.length ? maLatest(closedCandles(candles, { interval }).rows, requested) : []
199
209
  if (ma.length) text.push('均线(最新已收盘):' + ma.map((row) => `${row.label}=${row.value == null ? '数据不足' : fmt(row.value)}${row.line.smoothing ? `→平滑${fmt(row.smoothed)}` : ''}`).join(' '))
200
210
  text.push('指标是历史 K 线的派生值;需要结合最新盘口、账户风险和数据时效验证,不能单独当作下单信号。')
201
- return { text: text.join('\n'), indicators, ma, interval, coin }
211
+ return { text: text.join('\n'), configured, indicators: { ...indicators, rsi14: configured.values.rsi14, macd: { line: configured.values.macd, signal: configured.values.signal, histogram: configured.values.histogram }, bollinger20: { lower: configured.values.lower, middle: configured.values.basis, upper: configured.values.upper }, parameterSettings: settings }, ma, interval, coin }
202
212
  }
203
213
  if (action === 'trades') {
204
214
  const snap = await remote.snapshot({ coin })
@@ -210,10 +220,12 @@ function buildTools(remote) {
210
220
  },
211
221
  {
212
222
  name: 'hyper_chart',
213
- description: '在当前 DSH 交易会话的行情图上画或管理供人查看的水平支撑/阻力线、趋势线与均线。kind=ma 在图上叠加一条按 K 线实时计算的均线:maType 支持 sma/ema/smma(平滑均线,即 RMA/Wilder)/wma,period 1-500,source 默认 close,可选 smoothingType+smoothingPeriod 再对均线做一次平滑(同 TradingView 均线指标的 Smoothing);均线按会话保存,随周期切换重算,最多 8 条,add 会返回最新已收盘值。画线工具:level 水平线(price)、trend 趋势线(fromTime/fromPrice/toTime/toPrice),其余用 points=[{time,price}]:hray 水平射线/vline 垂直线 1 点,ray 射线/extended 延长线/fib 斐波那契回撤(第 1 点=1、第 2 点=0)/rect 矩形/measure 价格区间 2 点,channel 平行通道 3 点(第 3 点定通道宽度),text 文本 1 点(label 为文本,最多 200 字),long/short 多空头寸 3 点 [入场, 止损, 止盈](第 2 点时间为右边界);fibext 斐波那契扩展 3 点(A→B 波段,自 C 投射)、fibchannel 斐波那契通道 3 点、fibtime 斐波那契时间周期 2 点、gannfan 江恩扇形 2 点(第 2 点定 1×1)、pitchfork 安德鲁叉子线 3 点、regression 回归趋势 2 点(时间区间)、arrow 箭头 2 点、callout 标注气泡 2 点(锚点+文本框位置,label 为文本)、circle 圆形 2 点(圆心+边缘)、triangle 三角形 3 点、brush 画笔 2-500 点;crossline 十字线、pricelabel 价格标签 1 点;infoline 信息线、trendangle 趋势角度、pricerange 价格区间、daterange 日期区间 2 点;elliott 推动浪 6 点(0-5)、elliottabc 调整浪 4 点(0-A-B-C)、xabcd 谐波 5 点、abcd 4 点、headshoulders 头肩 7 点(左底-左肩-颈1-头-颈2-右肩-右底)、gannbox/gannsquare 2 点对角、schiff/modschiff 叉子线 3 点、icon 图标 1 点(label 为 flag/star/up/down/warning/check/cross/fire/target/bell,或贴图库 emoji:📈/🚀/🐂/🐻/⚠️ 等)。update 按 id 修改 points/price/label 或样式 color/lineWidth(1-4)/lineStyle(solid/dashed/dotted)/locked。画线按会话和标的保存,在该标的所有周期显示。先用 hyper_market candles/indicators 验证价格和时间;标注仅用于解释,不是自动订单或交易信号。',
223
+ description: '在当前 DSH 交易会话的行情图上画或管理供人查看的水平支撑/阻力线、趋势线与均线,并切换图表周期与副图指标。action=view 设置周期(interval)并用 show/hide 开关指标(rsi14、macd、bollinger20、volume、atr14、vwap、stochastic、adx、obv 等,图上实时计算,不要自己算数值再画线)。kind=ma 在图上叠加一条按 K 线实时计算的均线:maType 支持 sma/ema/smma(平滑均线,即 RMA/Wilder)/wma,period 1-500,source 默认 close,可选 smoothingType+smoothingPeriod 再对均线做一次平滑(同 TradingView 均线指标的 Smoothing);均线按会话保存,随周期切换重算,最多 8 条,add 会返回最新已收盘值。画线工具:level 水平线(price)、trend 趋势线(fromTime/fromPrice/toTime/toPrice),其余用 points=[{time,price}]:hray 水平射线/vline 垂直线 1 点,ray 射线/extended 延长线/fib 斐波那契回撤(第 1 点=1、第 2 点=0)/rect 矩形/measure 价格区间 2 点,vprofile 固定范围成交量分布 2 点(只取两点的时间区间,返回 POC 与 70% 价值区 VAH/VAL),channel 平行通道 3 点(第 3 点定通道宽度),text 文本 1 点(label 为文本,最多 200 字),long/short 多空头寸 3 点 [入场, 止损, 止盈](第 2 点时间为右边界);fibext 斐波那契扩展 3 点(A→B 波段,自 C 投射)、fibchannel 斐波那契通道 3 点、fibtime 斐波那契时间周期 2 点、gannfan 江恩扇形 2 点(第 2 点定 1×1)、pitchfork 安德鲁叉子线 3 点、regression 回归趋势 2 点(时间区间)、arrow 箭头 2 点、callout 标注气泡 2 点(锚点+文本框位置,label 为文本)、circle 圆形 2 点(圆心+边缘)、triangle 三角形 3 点、brush 画笔 2-500 点;crossline 十字线、pricelabel 价格标签 1 点;infoline 信息线、trendangle 趋势角度、pricerange 价格区间、daterange 日期区间 2 点;elliott 推动浪 6 点(0-5)、elliottabc 调整浪 4 点(0-A-B-C)、xabcd 谐波 5 点、abcd 4 点、headshoulders 头肩 7 点(左底-左肩-颈1-头-颈2-右肩-右底)、gannbox/gannsquare 2 点对角、schiff/modschiff 叉子线 3 点、icon 图标 1 点(label 为 flag/star/up/down/warning/check/cross/fire/target/bell,或贴图库 emoji:📈/🚀/🐂/🐻/⚠️ 等)。update 按 id 修改 points/price/label 或样式 color/lineWidth(1-4)/lineStyle(solid/dashed/dotted)/locked。画线按会话和标的保存,在该标的所有周期显示。先用 hyper_market candles/indicators 验证价格和时间;标注仅用于解释,不是自动订单或交易信号。',
214
224
  parameters: {
215
225
  type: 'object', properties: {
216
- action: { type: 'string', enum: ['add', 'list', 'update', 'remove'] },
226
+ action: { type: 'string', enum: ['add', 'list', 'update', 'remove', 'view'], description: 'view:切换图表周期与副图指标' },
227
+ show: { type: 'array', items: { type: 'string', enum: INDICATOR_IDS }, description: 'action=view:要显示的指标' },
228
+ hide: { type: 'array', items: { type: 'string', enum: INDICATOR_IDS }, description: 'action=view:要隐藏的指标' },
217
229
  coin: { type: 'string', description: '默认会话绑定币对' },
218
230
  interval: { type: 'string', description: '默认当前交易台周期' },
219
231
  kind: { type: 'string', enum: [...DRAWING_KINDS, 'ma'], description: 'add 必填:画线类型或 ma 均线' },
@@ -225,6 +237,8 @@ function buildTools(remote) {
225
237
  smoothingPeriod: { type: 'number', description: 'kind=ma 可选:平滑周期' },
226
238
  color: { type: 'string', description: '可选 #RRGGBB 颜色(画线或均线)' },
227
239
  lineWidth: { type: 'number', description: '画线线宽 1-4' },
240
+ rows: { type: 'number', description: 'kind=vprofile:价格行数 6-200,默认 48' },
241
+ valueArea: { type: 'number', description: 'kind=vprofile:价值区成交量占比 0.5-0.95,默认 0.7' },
228
242
  lineStyle: { type: 'string', enum: ['solid', 'dashed', 'dotted'], description: '画线线型' },
229
243
  locked: { type: 'boolean', description: 'update:锁定后用户不能误拖动' },
230
244
  hidden: { type: 'boolean', description: 'update:在图上隐藏(保留在画线列表中)' },
@@ -259,6 +273,10 @@ function buildTools(remote) {
259
273
  const lines = [...result.marks.map((mark) => `${mark.id} ${mark.interval} ${mark.kind} ${mark.role} ${mark.label}`), ...ma.map((row) => `${maText(row)}(${coin} ${interval} 最新已收盘)`)]
260
274
  return { text: lines.length ? lines.join('\n') : '当前标的没有标注或均线', ...result, maLines: ma }
261
275
  }
276
+ if (args?.action === 'view') {
277
+ const view = remote.setSessionChartView(sessionId, { interval: args?.interval, show: args?.show ?? [], hide: args?.hide ?? [] })
278
+ return { text: `图表周期 ${view.interval},显示指标:${view.indicators.join(', ') || '无'}`, interval: view.interval, indicators: view.indicators }
279
+ }
262
280
  if (args?.action === 'add' && args?.kind === 'ma') {
263
281
  const line = normalizeMaLine({ kind: args.maType, period: args.period, source: args.source, color: args.color,
264
282
  smoothing: args.smoothingType != null || args.smoothingPeriod != null ? { kind: args.smoothingType, period: args.smoothingPeriod } : null })
@@ -272,13 +290,31 @@ function buildTools(remote) {
272
290
  return { text: `已移除均线:${maLabel(removed)}(${removed.id})`, line: removed }
273
291
  }
274
292
  if (args?.action === 'update') {
275
- const patch = Object.fromEntries(['points', 'price', 'fromTime', 'fromPrice', 'toTime', 'toPrice', 'label', 'color', 'lineWidth', 'lineStyle', 'locked', 'hidden', 'shared', 'alert'].filter((key) => args?.[key] != null).map((key) => [key, args[key]]))
293
+ const patch = Object.fromEntries(['points', 'price', 'fromTime', 'fromPrice', 'toTime', 'toPrice', 'label', 'color', 'lineWidth', 'lineStyle', 'locked', 'hidden', 'shared', 'alert', 'rows', 'valueArea'].filter((key) => args?.[key] != null).map((key) => [key, args[key]]))
276
294
  const mark = remote.editChartMark(args?.id, patch, sessionId)
277
295
  return { text: `已更新${DRAWING_NAMES[mark.kind] ?? mark.kind}:${mark.label}(${mark.id})`, mark }
278
296
  }
279
297
  if (args?.action === 'add') {
298
+ // Fetch the range first: a failed market request must not leave a saved drawing behind.
299
+ let summary = null
300
+ if (args?.kind === 'vprofile') {
301
+ const times = (Array.isArray(args?.points) ? args.points : []).map((point) => Number(point?.time)).filter(Number.isFinite).sort((x, y) => x - y)
302
+ if (times.length === 2) {
303
+ const { candles } = await remote.candles({ coin, interval, startMs: times[0], endMs: times[1] + 1 })
304
+ const inRange = candles.filter((c) => c.t >= times[0] && c.t <= times[1])
305
+ const profile = volumeProfile(inRange.map((c) => ({ open: c.o, high: c.h, low: c.l, close: c.c, volume: c.v })), { rows: Number(args?.rows) || 48, valueArea: Number(args?.valueArea) || 0.7 })
306
+ const step = INTERVAL_MS[interval] ?? (inRange.length > 1 ? inRange[1].t - inRange[0].t : 0)
307
+ const partial = !inRange.length || (step > 0 && inRange[0].t - times[0] > step)
308
+ summary = { profile, partial, bars: inRange.length, from: inRange[0]?.t ?? null }
309
+ }
310
+ }
280
311
  const mark = remote.addChartMark({ ...args, coin, interval }, sessionId)
281
- return { text: `已在 ${coin} ${interval} 图表添加${DRAWING_NAMES[mark.kind] ?? mark.kind}:${mark.label}(${mark.id})`, mark }
312
+ const text = `已在 ${coin} ${interval} 图表添加${DRAWING_NAMES[mark.kind] ?? mark.kind}:${mark.label}(${mark.id})`
313
+ if (!summary) return { text, mark }
314
+ if (!summary.profile) return { text: `${text};区间内没有成交量数据,无法统计成交量分布`, mark }
315
+ const { profile } = summary
316
+ const note = `按 ${summary.bars} 根 ${interval} K 线估算(每根成交量均匀分摊到其高低区间)${summary.partial ? `;交易所只返回 ${new Date(summary.from).toISOString().slice(0, 16)} 之后的 K 线,区间前段缺失` : ''}`
317
+ return { text: `${text};POC=${fmt(profile.poc)} 价值区 VAH=${fmt(profile.vah)} VAL=${fmt(profile.val)}(${Math.round((Number(args?.valueArea) || 0.7) * 100)}% 成交量);${note}`, mark, profile: { poc: profile.poc, vah: profile.vah, val: profile.val, bars: summary.bars, partial: summary.partial } }
282
318
  }
283
319
  if (args?.action === 'remove') {
284
320
  const mark = remote.removeChartMark(args?.id, sessionId)
@@ -289,17 +325,19 @@ function buildTools(remote) {
289
325
  },
290
326
  {
291
327
  name: 'hyper_account',
292
- description: '查询交易账户。action=state 返回权益/持仓;orders 返回挂单;fills 返回成交;orderStatus 用 oid 或 cloid 核对某笔 live 订单,尤其在网络超时后。',
328
+ description: '查询交易账户。action=state 返回权益/持仓;orders 返回挂单;fills 返回成交;orderStatus 用 oid 或 cloid 核对某笔 live 订单,尤其在网络超时后。review 返回最近 7/30/90 天成交复盘、订单数量核对、费用和资金费覆盖缺口;不是完整账户收益率。',
293
329
  parameters: {
294
330
  type: 'object',
295
331
  properties: {
296
- action: { type: 'string', enum: ['state', 'orders', 'fills', 'orderStatus'], description: '账户查询类型,默认 state' },
332
+ action: { type: 'string', enum: ['state', 'orders', 'fills', 'orderStatus', 'review'], description: '账户查询类型,默认 state' },
297
333
  oid: { type: 'string', description: 'orderStatus 必填;交易所 oid 或 0x 开头 128 位 cloid' },
334
+ days: { type: 'number', enum: [7, 30, 90], description: 'review 窗口,默认 30 天' },
335
+ coin: { type: 'string', description: 'review 可选,仅核对该标的;省略为全部' },
298
336
  },
299
337
  additionalProperties: false,
300
338
  },
301
339
  output: { schema: { type: 'object', additionalProperties: true }, render: (_a, v) => [{ type: 'text', text: v.text }] },
302
- timeoutMs: 15_000,
340
+ timeoutMs: 60_000,
303
341
  async execute(args) {
304
342
  const action = args?.action ?? 'state'
305
343
  if (action === 'state') {
@@ -329,6 +367,13 @@ function buildTools(remote) {
329
367
  const rows = f.fills ?? []
330
368
  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) }
331
369
  }
370
+ if (action === 'review') {
371
+ const days = args?.days ?? 30
372
+ if (![7, 30, 90].includes(days)) throw Error('复盘支持最近 7/30/90 天')
373
+ const evidence = await remote.tradeHistory({ includeFunding: true, days })
374
+ const review = reviewLedger(evidence, args?.coin ? canonicalMarketCoin(args.coin) : '')
375
+ return { text: `窗口复盘(不是账户收益率):${JSON.stringify({ coverage: evidence.coverage, mode: evidence.mode, network: evidence.network, ...review })}`, evidence, review }
376
+ }
332
377
  if (action === 'orderStatus') {
333
378
  const result = await remote.orderStatus({ oid: args?.oid })
334
379
  return { text: JSON.stringify(result), ...result }
@@ -0,0 +1,3 @@
1
+ export interface ResearchSettings { layout: 'single' | 'dual' | 'quad'; intervals: import('../src/client/types').Interval[]; syncTime: boolean; syncCursor: boolean }
2
+ export declare function normalizeResearch(input?: unknown): ResearchSettings
3
+ export declare function normalizePaperCosts(input?: unknown): { feeBps: number; slippageBps: number }
@@ -0,0 +1,12 @@
1
+ const INTERVALS = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M']
2
+ export function normalizeResearch(input = {}) {
3
+ const next = { layout: 'single', intervals: ['1h', '4h', '1d'], syncTime: true, syncCursor: true, ...input }
4
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some((key) => !['layout', 'intervals', 'syncTime', 'syncCursor'].includes(key)) || !['single', 'dual', 'quad'].includes(next.layout)
5
+ || !Array.isArray(next.intervals) || next.intervals.length !== 3 || next.intervals.some((iv) => !INTERVALS.includes(iv)) || typeof next.syncTime !== 'boolean' || typeof next.syncCursor !== 'boolean') throw Error('多周期研究配置无效')
6
+ return next
7
+ }
8
+ /** Simulation assumptions; not the exchange account's actual fee tier. */
9
+ export function normalizePaperCosts(input = { feeBps: 4.5, slippageBps: 5 }) {
10
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).length !== 2 || !['feeBps', 'slippageBps'].every((key) => typeof input[key] === 'number' && Number.isFinite(input[key]) && input[key] >= 0 && input[key] <= 100)) throw Error('模拟费用和滑点必须为 0–100 bps')
11
+ return { feeBps: input.feeBps, slippageBps: input.slippageBps }
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hyperliquid",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
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
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "icon.svg",
21
21
  "cordis.patch.yml",
22
22
  "README.md",
23
- "NOTICE"
23
+ "NOTICE",
24
+ "docs/research-review-2026-10-16.md"
24
25
  ],
25
26
  "keywords": [
26
27
  "dsh",
@@ -37,8 +38,10 @@
37
38
  "build": "node scripts/build-client.mjs",
38
39
  "build:watch": "node scripts/build-client.mjs --watch",
39
40
  "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/risk-settings.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/moving-average.test.mjs && node test/strategy-theories.test.mjs && node test/chart-marks.test.mjs && node test/chart-drawings.test.mjs && node test/click-guard.test.mjs && node test/drawing-sync-alerts.test.mjs && node test/tool-output.test.mjs && node test/chart-reference-chip.test.mjs && node test/dsh-extensions.test.mjs && node test/history.test.mjs && node test/skill.test.mjs && node test/markers.test.mjs && node test/client-integration.mjs",
41
- "smoke:drawing": "bash scripts/drawing-smoke/run.sh"
41
+ "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/risk-settings.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/research.test.mjs && node test/indicators.test.mjs && node test/moving-average.test.mjs && node test/strategy-theories.test.mjs && node test/native-session-list.test.mjs && node test/chart-basics.test.mjs && node test/chart-marks.test.mjs && node test/chart-drawings.test.mjs && node test/click-guard.test.mjs && node test/drawing-sync-alerts.test.mjs && node test/tool-output.test.mjs && node test/chart-reference-chip.test.mjs && node test/dsh-extensions.test.mjs && node test/history.test.mjs && node test/skill.test.mjs && node test/markers.test.mjs && node test/client-integration.mjs",
42
+ "smoke:drawing": "bash scripts/drawing-smoke/run.sh",
43
+ "smoke:chart": "DRAWING_SMOKE_ONLY=basics bash scripts/drawing-smoke/run.sh",
44
+ "smoke:research": "DRAWING_SMOKE_ONLY=research bash scripts/drawing-smoke/run.sh"
42
45
  },
43
46
  "dependencies": {
44
47
  "@deepseek-ai/schemastery": "^3.18.4",
@@ -10,16 +10,17 @@ description: 在 DSH 交易会话中分析 Hyperliquid 实时行情、账户与
10
10
  ## 建立上下文
11
11
 
12
12
  1. 先调用 `hyper_context`,确认会话币种、周期、网络、paper/live 模式、实时价格、盘口、资金费、K 线、账户权益、仓位、挂单、数据时间和风险限额。用户明确指定其他币种时,传入 `coin`;需要改变会话焦点时再用 `hyper_session bind`。
13
- 2. 做深度分析时按需调用 `hyper_market overview/book/candles/indicators/trades`;管理订单时调用 `hyper_account state/orders/fills/orderStatus`。不得把截图、记忆价格或上一次回答当作实时数据。
13
+ 2. 做深度分析时按需调用 `hyper_market overview/book/candles/indicators/trades`;管理订单时调用 `hyper_account state/orders/fills/orderStatus/review`。不得把截图、记忆价格或上一次回答当作实时数据。
14
14
  3. 发现市场时用 `hyper_market action=catalog`,可按 `kind` 和官方 `category` 检索 HIP-3 的 Crypto、TradFi、Pre-IPO 标签。后续工具调用必须使用目录返回的 `name`,不要把展示名缩写为 API 参数;例如 XYZ100 的合约名是 `xyz:XYZ100`。目录中的 HIP-3、现货和 Outcome 仅支持行情,不能因为价格或分类可见就调用下单工具;分类缺失时不要凭名称自行归类。
15
15
  4. 检查盘口和 mid 的观测时间、WS 状态、买卖价差、资金费、可用余额以及现有仓位。数据缺失、过期、盘口异常或账户不可读时,先说明限制并停止执行交易。
16
16
  5. 读取 `accountMode` 与 `balanceSource`:Standard 的核心永续权益来自永续账户;Unified/Portfolio 的 USDC 余额来自现货账户状态,`tradeCapacity` 是交易所对当前核心永续币对返回的可交易额度。不要把旧的永续 `marginSummary` 当成 Unified/Portfolio 账户总权益,也不要把 USDC 余额称为整个 Portfolio 的估值。切换币对后重新读取额度。
17
17
 
18
18
  ## 技术指标与图表
19
19
 
20
- 1. 用 `hyper_market action=indicators` 读取对应币种和周期的 SMA20、EMA20/50、RSI14、MACD、布林带、ATR14、20 根量比以及最近摆动高低点推导的支撑/阻力。工具只用交易所 `T` 收盘时间已过去的 K 线计算,并返回 `excludedOpen`;图表上的实时末根 K 线仍可能形成中。先检查 `ready`、已收盘 K 线数量和 `observedAt`(最新收盘时间),并按周期判断数据是否过期;旧数据或少于 50 根已收盘 K 线不可作为入场依据。需要跨周期验证时分别查询当前周期与更高周期,不把两者混作同一信号。
20
+ 1. 用 `hyper_market action=indicators` 读取对应币种和周期的 SMA20、EMA20/50、RSI、MACD、布林带、ATR14、Stochastic、ADX/DMI、OBV、20 根量比;RSI/MACD/BOLL 等遵循 hyper_context.workspace.indicatorSettings,configured 返回实际参数和值。OBV 从已加载首根零基准累积,历史窗口不同绝对值不可直接比较;其他递归指标也有预热差异。工具还返回最近摆动高低点推导的支撑/阻力。工具只用交易所 `T` 收盘时间已过去的 K 线计算,并返回 `excludedOpen`;图表上的实时末根 K 线仍可能形成中。先检查 `ready`、已收盘 K 线数量和 `observedAt`(最新收盘时间),并按周期判断数据是否过期;旧数据或少于 50 根已收盘 K 线不可作为入场依据。需要跨周期验证时分别查询当前周期与更高周期,不把两者混作同一信号。
21
21
  2. 将趋势、动量、波动和量能分开判断:例如价格相对 EMA20/50、MACD 与 RSI 是否同向,价格是否接近支撑/阻力,量比是否支持突破,ATR 所示正常波动是否足以触及止损。指标相互矛盾、价格已经远离合理入场区、盘口过薄或止损所需风险超过限额时,选择观望。支撑/阻力是历史区间,不是保证成交或反弹的价格。
22
22
  3. 给人解释图表时可用 `hyper_chart action=add` 画水平支撑/阻力线、两点趋势线,或用 points 画射线、平行通道、斐波那契回撤(第 1 点为波段起点)、矩形区间与价格区间测量、文本备注,或用 long/short 标出入场、止损与止盈(points 为 [入场, 止损, 止盈]);波段结构可用 fibext(第 3 点为回撤点)、pitchfork、regression(按收盘价拟合 ±2σ)、gannfan / gannbox 表达,浪型与形态用 elliott(0-5)、elliottabc、xabcd、abcd、headshoulders 标出,关键位用 callout / pricelabel / icon 标注;已有画线用 `action=update` 按 id 调整,不必删除重画;用户要求盯某个价位或趋势线时,可对 level/hray/trend/ray/extended 设置 `alert=true`(穿越时提醒,触发一次),需要在同币种所有会话可见时设置 `shared=true`,标签写明依据和周期;画线前用实际 K 线核实价格与时间(趋势线使用 epoch 毫秒)。每条线只属于当前 DSH 交易会话、币种和周期,可用 `list/remove` 管理。图表标注不触发交易,也不替代 `hyper_decide` 的实时预检。需要平滑均线(SMMA/RMA)或任意周期的 SMA/EMA/WMA 时,用 `hyper_chart action=add kind=ma maType=smma period=…` 叠加到图上(可加 smoothingType/smoothingPeriod 二次平滑),用 `hyper_market action=indicators` 读取其最新已收盘值;不要再建议用户去外部图表工具。
23
+ 均线直接用 `hyper_chart action=add kind=ma`(maType/period),RSI、MACD、布林带等副图指标用 `hyper_chart action=view show=["rsi14","macd"]`(可带 interval 切周期),图表会实时计算;不要自己算均线再用多段趋势线拼接。复原别人的截图画线时,先用 `hyper_market candles` 找到对应的高低点时间与价格,再用 points 锚定到真实 K 线。
23
24
 
24
25
  ## 策略研究框架
25
26
 
@@ -34,11 +35,15 @@ description: 在 DSH 交易会话中分析 Hyperliquid 实时行情、账户与
34
35
  ## 决策与执行
35
36
 
36
37
  - 先陈述方向、入场条件、数量、杠杆、止损/退出条件、账户影响及不交易的条件。没有足够依据时明确选择观望;不要仅因用户问“能否买”就默认买入。
37
- - paper 模式可在核对后用 `hyper_order place`,并传入 10–1000 字的 `reason` 记录本次下单依据;与 live 共用允许币对、单笔、每日、仓位、杠杆限额,并单独记录 paper 日额度和保证金占用。paper 只支持立即成交的限价单,不模拟静置挂单、撤单或止盈止损触发。撮合仍是固定滑点,不模拟盘口深度、手续费、资金费或强平;不要据此推断 live 收益。减仓在额度耗尽或权益不足时仍可退出。
38
+ - paper 模式可在核对后用 `hyper_order place`,并传入 10–1000 字的 `reason` 记录本次下单依据;与 live 共用允许币对、单笔、每日、仓位、杠杆限额,并单独记录 paper 日额度和保证金占用。paper 只支持立即成交的限价单,不模拟静置挂单、撤单或止盈止损触发。撮合使用交易设置中的模拟滑点与手续费,手续费实际扣权益,新成交保留当时成本假设;不模拟盘口深度、资金费或强平;不要据此推断 live 收益。减仓在额度耗尽或权益不足时仍可退出。
38
39
  - live 模式先用 `hyper_decide` 提交完整交易依据和订单参数,再在同一会话、30 秒内用返回的 `decisionId` 调用 `hyper_order place`。两次调用的方向、数量、价格、只减仓及止盈止损参数必须一致。决定放弃就不要调用执行工具。
39
40
  - `hyper_order cancel` 先用 `hyper_account orders` 或 `orderStatus` 核对目标;`hyper_order leverage` 先核对市场与账户限制。自主执行是否放行由插件 `autonomousLive`、硬性限额和 dsh-web3 钱包策略共同决定;不能绕过拒绝或审批。
40
41
  - live 止盈/止损与主单使用 `normalTpsl` 分组提交,目前仅在 testnet 开放。子单仅在父单完全成交,或部分成交后因保证金不足被撤时才会挂出;部分成交后手动撤销父单会连同子单撤销。IOC 等可能留下部分成交仓位却没有生效的保护单。提交后用 `hyper_account orderStatus`、`orders` 和 `state` 核对主单、保护单和仓位;回执部分成功、超时或未知时尤其要先对账,未确认保护前不要声称仓位已有止损。
41
42
 
43
+ ## 交易复盘
44
+
45
+ 用 `hyper_account action=review days=7/30/90` 取得窗口证据,可传 coin 限定标的。先说明 coverage 中的成交/委托截断、资金费缺口和未知字段,再讨论原因与执行;订单按标的和 oid 匹配,fee 已含 builderFee,不能重复扣除。仅核心 USDC 永续合并净额,其他币种分开查看。未记录的原因不得事后编造成原始依据,窗口净额不得称为账户收益率,平仓成交笔数不得当作完整往返胜率。多周期研究读取 workspace.research 后逐周期调用指标,说明各自最新已收盘时点、趋势和冲突。
46
+
42
47
  ## 回答格式
43
48
 
44
49
  区分“计划”“已提交”“挂单中”“已成交”“已撤销”和“状态未知”。引用本次工具读到的价格、数据时间及账户状态;不要把签名成功当作成交。简明说明决策依据、主要风险和下一步。