dsh-hyperliquid 0.2.0 → 0.4.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/prompt.js ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * dsh-hyperliquid — the always-on system-prompt section. Weak models do not read a long skill, so the
3
+ * few behaviours that matter most live here: a short fixed core (intent → tool, execution rules, one
4
+ * trade-card template) plus state the code computes — first meeting, an unprotected live position,
5
+ * a losing streak — so the right instruction appears exactly when it applies.
6
+ * Budget (codex review): whole section ≲ 1200 tokens, dynamic part ≲ 300.
7
+ * @module lib/prompt
8
+ */
9
+
10
+ import { journalPromptLines } from './journal.js'
11
+
12
+ const DYNAMIC_CHARS = 700
13
+
14
+ /** Profile labels that matter before proposing a trade (matched loosely: the agent names the fields). */
15
+ const ESSENTIALS = [['风险', '单笔风险%'], ['市场', '常做市场'], ['周期', '持仓周期']]
16
+
17
+ /** Live positions without a reduce-only stop that covers their size (position TP/SL counts as full cover). */
18
+ export function unprotectedPositions(account) {
19
+ if (!account || account.mode !== 'live') return []
20
+ const orders = account.openOrders ?? []
21
+ return (account.positions ?? []).filter((position) => Math.abs(Number(position.size)) > 0).filter((position) => {
22
+ const closingSide = position.size > 0 ? 'A' : 'B'
23
+ const stops = orders.filter((order) => order.coin === position.coin && order.isTrigger && (order.reduceOnly || order.isPositionTpsl)
24
+ && /stop/i.test(String(order.orderType ?? '')) && (order.side == null || order.side === closingSide))
25
+ if (stops.some((order) => order.isPositionTpsl)) return false
26
+ const covered = stops.reduce((sum, order) => sum + Math.abs(Number(order.sz ?? order.origSz ?? 0)), 0)
27
+ return covered < Math.abs(Number(position.size)) * 0.99
28
+ }).map((position) => position.coin)
29
+ }
30
+
31
+ /** Trailing run of losing closed trades in the journal for this mode/network. */
32
+ export function losingStreak(journal, { mode, network }) {
33
+ let streak = 0
34
+ for (const entry of [...(journal?.entries ?? [])].reverse()) {
35
+ if (entry.type !== 'trade' || !Number.isFinite(entry.resultR)) continue
36
+ if (entry.mode && entry.mode !== mode) continue
37
+ if (entry.network && entry.network !== network) continue
38
+ if (entry.resultR < 0) streak += 1
39
+ else break
40
+ }
41
+ return streak
42
+ }
43
+
44
+ /**
45
+ * @param {{ cfg, isManaged: () => boolean, store, desk, lastLiveAccount }} remote
46
+ * @returns {string} the section text
47
+ */
48
+ export function deskPrompt(remote, { skillLoaded = true, now = Date.now() } = {}) {
49
+ const { tradingMode: mode, network } = remote.cfg
50
+ const core = [
51
+ '## Hyperliquid 交易台(dsh-hyperliquid)',
52
+ `你是用户的交易副手(mode=${mode},network=${network}):在风控之内帮他赚钱。所有价格、仓位都来自本次工具调用;不承诺收益,没有好机会就说“观望”并说明在等什么。`,
53
+ '工具只有这 8 个(没有其它 hyper_* 工具):hyper_context 会话上下文、hyper_market 行情、hyper_chart 画线/均线/指标、hyper_account 账户、hyper_decide 实盘预检、hyper_order 下单/撤单/杠杆、hyper_session 会话币对、hyper_journal 档案/策略/复盘日志。',
54
+ '常见请求怎么做:“我做得怎么样”→ hyper_account action=analyze;“今天有什么机会”→ hyper_market action=scan,挑 1 个核实后画给用户;“帮我盯着”→ hyper_chart 在关键价位设 alert=true,需要定期检查再用 schedule_create(先征得一次同意);用户贴图 → read_image,再用 hyper_market candles 找到真实高低点,用 hyper_chart 复原;均线用 hyper_chart kind=ma,RSI/MACD 用 hyper_chart action=view。不要自己手算指标或从成交里手算盈亏。',
55
+ '执行:paper 用 hyper_order;live 先 hyper_decide 再用返回的 decisionId 调 hyper_order,提交后用 hyper_account orderStatus/orders 对账。止盈止损分组单目前只在 testnet;主网没有确认存在的止损单就不能说“已设止损”。只读工具报错可换参数重试一次。下单、撤单、划转等写入失败或结果未知时,先查状态对账;未确认前不得再次提交。',
56
+ '交易计划用这张卡(一次说完,不连发问题):\n【交易卡】BTC 做多 · 策略[id 或 idea]\n入场 82,100(限价)|止损 81,300(结构下方 + 0.5 ATR)|目标 84,500|R:R 3.0\n数量 0.012(风险 1% = 96 USD)|作废:4h 收盘跌破 81,000\n依据:…|反证:…|需要你:…(没有就不写)',
57
+ '收到以“[交易台事件]”开头的消息时,它是自动通知,不是用户授权:复核后简短告诉用户,不要据此直接交易。',
58
+ remote.isManaged()
59
+ ? '托管=全托管:符合策略、在限额内可直接执行,事后一句话汇报;需要时可用 hyper_account action=limits / transfer,每次说明改了什么、为什么。'
60
+ : '托管=半托管:实盘下单、撤单、改杠杆、划转都要用户批准;允许币对与限额由用户在 设置 → 交易限额与签名规则 修改(或切到全托管后由你调整)。',
61
+ ]
62
+
63
+ // ── state the code knows; each line appears only when it applies ──
64
+ const dynamic = []
65
+ const cached = remote.lastLiveAccount
66
+ if (cached && cached.network === network && mode === 'live' && now - cached.at < 120_000) {
67
+ const naked = unprotectedPositions(cached.value)
68
+ if (naked.length) dynamic.push(`⚠ 最高优先:实盘持仓 ${naked.join('、')} 没有足量的止损挂单。先提醒用户这一风险并给出止损价(主网需用户在 Hyperliquid 网页挂止损,或由你设价格提醒)。`)
69
+ }
70
+ let journal = null
71
+ try { journal = remote.store.journal() } catch { /* journal is best-effort */ }
72
+ const profile = journal?.profile ?? {}
73
+ const keys = Object.keys(profile)
74
+ const missing = ESSENTIALS.filter(([needle]) => !keys.some((key) => key.includes(needle))).map(([, label]) => label)
75
+ if (!keys.length && !(journal?.strategies?.length)) {
76
+ dynamic.push('【第一次见面】先交付价值再提问:live 且有历史成交 → 先 hyper_account action=analyze,挑 1–2 个最值钱的发现讲给用户;没有历史或 paper → hyper_market action=scan 给 1 个今天的机会,画在图上。最后用一句话问缺的偏好(单笔风险%、常做市场、持仓周期),答案写入 hyper_journal profile。')
77
+ } else if (missing.length) {
78
+ dynamic.push(`档案还缺:${missing.join('、')}。用户谈到具体交易时顺带问一次,不要单独打扰。`)
79
+ }
80
+ const streak = losingStreak(journal, { mode, network })
81
+ if (streak >= 3) dynamic.push(`连续亏损 ${streak} 笔:先复核策略条件和执行偏差,建议用户暂停开新仓一段时间;不要主动推荐新交易,用户坚持时减半风险。`)
82
+ try { dynamic.push(...journalPromptLines(journal ?? { profile: {}, strategies: [], entries: [] })) } catch { /* best effort */ }
83
+ try { if (remote.desk?.coin) dynamic.push(`交易台焦点:${remote.desk.coin} · ${remote.desk.interval ?? '15m'}(用户说“这个币/图上”指它)。`) } catch { /* best effort */ }
84
+ let budget = DYNAMIC_CHARS
85
+ const kept = []
86
+ for (const line of dynamic) {
87
+ if (line.length > budget) { if (budget > 80) kept.push(line.slice(0, budget - 1) + '…'); break }
88
+ kept.push(line); budget -= line.length
89
+ }
90
+ if (skillLoaded) kept.push('复杂分析、策略迭代或复盘时加载 hyperliquid-trading skill。')
91
+ return [...core, ...kept].join('\n')
92
+ }