From f4ee723aa27c129f40a205da0fe351e3008d3360 Mon Sep 17 00:00:00 2001 From: tinklefund Date: Mon, 25 May 2026 01:25:10 +0800 Subject: [PATCH] feat(agent): surface Hyperliquid stock trading context - Add stock symbol panel and agent chat page wiring - Update onboarding and tool visibility for focused trader flows - Tighten related tests around configuration and trader scope --- agent/config_visibility_test.go | 12 +- agent/onboard.go | 4 +- agent/tools.go | 8 +- agent/trader_scope_test.go | 4 +- .../agent/HyperliquidSymbolsPanel.tsx | 259 ++++++++++++++++++ web/src/components/agent/WelcomeScreen.tsx | 24 +- .../components/trader/ModelConfigModal.tsx | 1 - web/src/pages/AgentChatPage.tsx | 98 +++++++ 8 files changed, 386 insertions(+), 24 deletions(-) create mode 100644 web/src/components/agent/HyperliquidSymbolsPanel.tsx diff --git a/agent/config_visibility_test.go b/agent/config_visibility_test.go index df9666f9..d5bfb0e3 100644 --- a/agent/config_visibility_test.go +++ b/agent/config_visibility_test.go @@ -286,7 +286,7 @@ func TestLoadExchangeOptionsHidesInvisibleExchangeRows(t *testing.T) { }).Error; err != nil { t.Fatalf("seed legacy hidden exchange: %v", err) } - if _, err := st.Exchange().Create("default", "okx", "我的主力OKX账户", true, "api-test", "secret-test", "pass-test", false, "", false, "", "", "", "", "", "", 0); err != nil { + if _, err := st.Exchange().Create("default", "okx", "我的主力OKX账户", true, "api-test", "secret-test", "pass-test", false, "", false, false, "", "", "", "", "", "", 0); err != nil { t.Fatalf("create visible exchange: %v", err) } @@ -307,7 +307,7 @@ func TestDescribeExchangeIncludesTypeSpecificVisibleFields(t *testing.T) { } a := New(nil, st, DefaultConfig(), slog.Default()) - hyperID, err := st.Exchange().Create("default", "hyperliquid", "Dex Pro", true, "hyper-api-key", "", "", true, "0xabc", true, "", "", "", "", "", "", 0) + hyperID, err := st.Exchange().Create("default", "hyperliquid", "Dex Pro", true, "hyper-api-key", "", "", true, "0xabc", true, false, "", "", "", "", "", "", 0) if err != nil { t.Fatalf("seed hyperliquid exchange: %v", err) } @@ -321,7 +321,7 @@ func TestDescribeExchangeIncludesTypeSpecificVisibleFields(t *testing.T) { } } - lighterID, err := st.Exchange().Create("default", "lighter", "Lighter Main", false, "", "", "", false, "", true, "", "", "", "wallet-1", "", "lighter-secret", 7) + lighterID, err := st.Exchange().Create("default", "lighter", "Lighter Main", false, "", "", "", false, "", true, false, "", "", "", "wallet-1", "", "lighter-secret", 7) if err != nil { t.Fatalf("seed lighter exchange: %v", err) } @@ -463,7 +463,7 @@ func TestToolUpdateTraderRejectsRenameOutsideManualPanel(t *testing.T) { if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil { t.Fatalf("seed model: %v", err) } - exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, "", "", "", "", "", "", 0) + exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, false, "", "", "", "", "", "", 0) if err != nil { t.Fatalf("seed exchange: %v", err) } @@ -515,7 +515,7 @@ func TestToolCreateTraderResponseHidesLegacyTraderTuningFields(t *testing.T) { if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil { t.Fatalf("seed model: %v", err) } - exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, "", "", "", "", "", "", 0) + exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, false, "", "", "", "", "", "", 0) if err != nil { t.Fatalf("seed exchange: %v", err) } @@ -566,7 +566,7 @@ func TestToolCreateTraderAutoReadsInitialBalanceFromExchange(t *testing.T) { if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil { t.Fatalf("seed model: %v", err) } - exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, "", "", "", "", "", "", 0) + exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, false, "", "", "", "", "", "", 0) if err != nil { t.Fatalf("seed exchange: %v", err) } diff --git a/agent/onboard.go b/agent/onboard.go index b89e5b18..53f6af59 100644 --- a/agent/onboard.go +++ b/agent/onboard.go @@ -455,7 +455,7 @@ func (a *Agent) saveSetupExchange(storeUserID string, state *SetupState) (string storeUserID, ex.ID, true, apiKey, apiSecret, passphrase, false, - hlWallet, hlUnified, + hlWallet, hlUnified, false, "", "", "", "", "", "", 0, ); err != nil { @@ -472,7 +472,7 @@ func (a *Agent) saveSetupExchange(storeUserID string, state *SetupState) (string true, apiKey, apiSecret, passphrase, false, - hlWallet, hlUnified, + hlWallet, hlUnified, false, "", "", "", "", "", "", 0, ) diff --git a/agent/tools.go b/agent/tools.go index f96f9369..ec8e9bc2 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -1516,6 +1516,7 @@ func (a *Agent) toolManageExchangeConfig(storeUserID, argsJSON string) string { testnet, strings.TrimSpace(args.HyperliquidWalletAddr), unified, + false, strings.TrimSpace(args.AsterUser), strings.TrimSpace(args.AsterSigner), strings.TrimSpace(args.AsterPrivateKey), @@ -1647,6 +1648,7 @@ func (a *Agent) toolManageExchangeConfig(storeUserID, argsJSON string) string { testnet, hyperWallet, unified, + existing.HyperliquidBuilderApproved, asterUser, asterSigner, strings.TrimSpace(args.AsterPrivateKey), @@ -2518,9 +2520,13 @@ func (a *Agent) toolStartTrader(storeUserID, traderID string) string { if a.traderManager == nil { return `{"error":"trader manager unavailable"}` } - if _, err := a.store.Trader().GetFullConfig(storeUserID, traderID); err != nil { + fullCfg, err := a.store.Trader().GetFullConfig(storeUserID, traderID) + if err != nil { return fmt.Sprintf(`{"error":"trader not found or inaccessible: %s"}`, err) } + if fullCfg != nil && fullCfg.Exchange != nil && fullCfg.Exchange.ExchangeType == "hyperliquid" && !fullCfg.Exchange.HyperliquidBuilderApproved { + return `{"error":"Hyperliquid trading authorization is incomplete; reconnect Hyperliquid wallet and complete trading authorization before starting this trader"}` + } if existing, err := a.traderManager.GetTrader(traderID); err == nil { if running, ok := existing.GetStatus()["is_running"].(bool); ok && running { return `{"error":"trader is already running"}` diff --git a/agent/trader_scope_test.go b/agent/trader_scope_test.go index 24c6730d..90e083ff 100644 --- a/agent/trader_scope_test.go +++ b/agent/trader_scope_test.go @@ -372,7 +372,7 @@ func TestHydrateCreateTraderSlotReferencesNormalizesExchangeIDFromVisibleName(t } a := New(nil, st, DefaultConfig(), slog.Default()) - exchangeID, err := st.Exchange().Create("default", "okx", "小偶", true, "api-test", "secret-test", "pass", false, "", false, "", "", "", "", "", "", 0) + exchangeID, err := st.Exchange().Create("default", "okx", "小偶", true, "api-test", "secret-test", "pass", false, "", false, false, "", "", "", "", "", "", 0) if err != nil { t.Fatalf("seed exchange: %v", err) } @@ -737,7 +737,7 @@ func TestBuildTraderCreateMissingPromptListsAllMissingSlots(t *testing.T) { if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek AI", true, "sk-test-12345", "", "deepseek-chat"); err != nil { t.Fatalf("seed model: %v", err) } - exchangeID, err := st.Exchange().Create("default", "okx", "OKX 主账户", true, "api-test", "secret-test", "pass", false, "", false, "", "", "", "", "", "", 0) + exchangeID, err := st.Exchange().Create("default", "okx", "OKX 主账户", true, "api-test", "secret-test", "pass", false, "", false, false, "", "", "", "", "", "", 0) if err != nil { t.Fatalf("seed exchange: %v", err) } diff --git a/web/src/components/agent/HyperliquidSymbolsPanel.tsx b/web/src/components/agent/HyperliquidSymbolsPanel.tsx new file mode 100644 index 00000000..35571552 --- /dev/null +++ b/web/src/components/agent/HyperliquidSymbolsPanel.tsx @@ -0,0 +1,259 @@ +import { useEffect, useMemo, useState } from 'react' +import { Search, Zap, TrendingDown, TrendingUp } from 'lucide-react' +import { api } from '../../lib/api' +import type { MarketSymbol } from '../../lib/api/data' + +interface HyperliquidSymbolsPanelProps { + language: string + disabled?: boolean + onTradeSymbol: (symbol: MarketSymbol) => void +} + +function formatVolume(value?: number) { + const n = Number(value || 0) + if (n >= 1e9) return `$${(n / 1e9).toFixed(1)}B` + if (n >= 1e6) return `$${(n / 1e6).toFixed(1)}M` + if (n >= 1e3) return `$${(n / 1e3).toFixed(0)}K` + return n > 0 ? `$${n.toFixed(0)}` : '—' +} + +function formatPrice(value?: number) { + const n = Number(value || 0) + if (!n) return '—' + if (n >= 1000) return `$${n.toLocaleString('en-US', { maximumFractionDigits: 2 })}` + if (n >= 1) return `$${n.toFixed(2)}` + return `$${n.toFixed(5)}` +} + +function formatChange(value?: number) { + const n = Number(value || 0) + if (!Number.isFinite(n) || n === 0) return '—' + return `${n > 0 ? '+' : ''}${n.toFixed(2)}%` +} + +const CATEGORY_LABEL: Record = { + stock: { zh: '股票', en: 'Stocks' }, + commodity: { zh: '大宗', en: 'Commodities' }, + index: { zh: '指数', en: 'Indices' }, + forex: { zh: '外汇', en: 'FX' }, + pre_ipo: { zh: 'Pre-IPO', en: 'Pre-IPO' }, + crypto: { zh: '加密', en: 'Crypto' }, +} + +const CATEGORY_ORDER = ['stock', 'commodity', 'index', 'forex', 'pre_ipo', 'crypto'] + +export function HyperliquidSymbolsPanel({ + language, + disabled, + onTradeSymbol, +}: HyperliquidSymbolsPanelProps) { + const [symbols, setSymbols] = useState([]) + const [query, setQuery] = useState('') + const [category, setCategory] = useState('stock') + const [ranking, setRanking] = useState<'gainers' | 'losers' | 'volume'>('gainers') + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + useEffect(() => { + let cancelled = false + setLoading(true) + api + .getSymbols('hyperliquid-xyz') + .then((res) => { + if (cancelled) return + setSymbols(res.symbols || []) + setError('') + }) + .catch((err) => { + if (cancelled) return + setError(err?.message || 'Failed to load symbols') + }) + .finally(() => { + if (!cancelled) setLoading(false) + }) + return () => { + cancelled = true + } + }, []) + + const categories = useMemo(() => { + const unique = new Set(symbols.map((s) => s.category).filter(Boolean)) + return CATEGORY_ORDER.filter((c) => unique.has(c)) + }, [symbols]) + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase() + return symbols + .filter((s) => (category === 'all' ? true : s.category === category)) + .filter((s) => { + if (!q) return true + return [s.symbol, s.display, s.name, s.category] + .filter(Boolean) + .some((v) => String(v).toLowerCase().includes(q)) + }) + .sort((a, b) => { + if (ranking === 'gainers') return Number(b.change_24h_pct || 0) - Number(a.change_24h_pct || 0) + if (ranking === 'losers') return Number(a.change_24h_pct || 0) - Number(b.change_24h_pct || 0) + return Number(b.volume_24h || 0) - Number(a.volume_24h || 0) + }) + .slice(0, 80) + }, [category, query, ranking, symbols]) + + if (loading) { + return ( +
+ {language === 'zh' ? '正在加载 Hyperliquid 全市场标的…' : 'Loading Hyperliquid markets…'} +
+ ) + } + + if (error) { + return ( +
+ {language === 'zh' ? '标的列表加载失败:' : 'Failed to load symbols: '}{error} +
+ ) + } + + return ( +
+
+ + setQuery(e.target.value)} + placeholder={language === 'zh' ? '搜索 SAMSUNG / TESLA / GOLD…' : 'Search SAMSUNG / TESLA / GOLD…'} + style={{ + width: '100%', + border: 0, + outline: 'none', + background: 'transparent', + color: '#EAECEF', + fontSize: 12, + }} + /> +
+ +
+ {([ + { key: 'gainers', icon: TrendingUp, zh: '涨幅榜', en: 'Gainers' }, + { key: 'losers', icon: TrendingDown, zh: '跌幅榜', en: 'Losers' }, + { key: 'volume', icon: Zap, zh: '成交额', en: 'Volume' }, + ] as const).map((item) => { + const Icon = item.icon + const active = ranking === item.key + return ( + + ) + })} +
+ +
+ {categories.map((c) => ( + + ))} +
+ +
+ {filtered.map((s) => { + const display = s.display || s.symbol + return ( + + ) + })} +
+ +
+ {language === 'zh' + ? `已列出 ${symbols.length} 个 Hyperliquid USDC 标的。默认按涨幅榜排序,也可切换跌幅榜/成交额;点击交易会直接创建固定标的 Trader。` + : `${symbols.length} Hyperliquid USDC markets loaded. Default ranking is 24h gainers; switch to losers/volume. Click Trade to create a fixed-symbol trader directly.`} +
+
+ ) +} diff --git a/web/src/components/agent/WelcomeScreen.tsx b/web/src/components/agent/WelcomeScreen.tsx index 429ed262..fad890dc 100644 --- a/web/src/components/agent/WelcomeScreen.tsx +++ b/web/src/components/agent/WelcomeScreen.tsx @@ -1,9 +1,9 @@ import { motion } from 'framer-motion' import { Zap, - BarChart3, Lightbulb, Search, + Bot, } from 'lucide-react' interface SuggestionCard { @@ -21,16 +21,16 @@ interface WelcomeScreenProps { export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) { const suggestions: SuggestionCard[] = language === 'zh' ? [ - { icon: , title: '分析 BTC 走势', subtitle: '技术分析 + 市场情绪', cmd: '分析一下 BTC 的走势' }, - { icon: , title: '做多 ETH', subtitle: 'Agent 帮你自动下单', cmd: '帮我做多 ETH 0.01 手' }, - { icon: , title: '搜索股票', subtitle: '输入名称或代码即可', cmd: '搜索一下中远海控' }, - { icon: , title: '策略建议', subtitle: '根据当前市场给出建议', cmd: '当前市场适合什么策略?' }, + { icon: , title: '创建美股 Agent', subtitle: '强势股 + 严格风控', cmd: '创建一个美股趋势交易 Agent,默认选择5个强势美股,严格风控' }, + { icon: , title: '一句话建策略', subtitle: '从想法到 Agent', cmd: '我想做美股强趋势突破,帮我生成策略和Agent' }, + { icon: , title: '搜索美股', subtitle: '输入名称或代码即可', cmd: '搜索一下 NVIDIA 和 Apple' }, + { icon: , title: '全球资产策略', subtitle: '美股/黄金/外汇', cmd: '当前美股、黄金、外汇适合什么策略?' }, ] : [ - { icon: , title: 'Analyze BTC', subtitle: 'Technical analysis + sentiment', cmd: 'Analyze BTC price action' }, - { icon: , title: 'Trade ETH', subtitle: 'Agent executes for you', cmd: 'Open a long position on ETH 0.01' }, - { icon: , title: 'Search Stocks', subtitle: 'Enter name or ticker', cmd: 'Search for NVIDIA stock' }, - { icon: , title: 'Strategy Ideas', subtitle: 'Market-based suggestions', cmd: 'What strategy fits the current market?' }, + { icon: , title: 'Create US Stock Agent', subtitle: 'Strong stocks + strict risk', cmd: 'Create a US stock trend-following agent with 5 strong stocks and strict risk control' }, + { icon: , title: 'One-line strategy', subtitle: 'Idea to agent', cmd: 'I want a US stock breakout strategy; build the strategy and agent' }, + { icon: , title: 'Search US stocks', subtitle: 'Name or ticker', cmd: 'Search for NVIDIA and Apple stocks' }, + { icon: , title: 'Global strategy', subtitle: 'Stocks/gold/FX', cmd: 'What strategy fits US stocks, gold, and FX now?' }, ] return ( @@ -72,7 +72,7 @@ export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) { margin: '0 0 8px', letterSpacing: '-0.02em', }}> - {language === 'zh' ? '跟 NOFXi 聊点什么' : 'What can I help with?'} + {language === 'zh' ? '快速创建你的美股 Agent' : 'Create your US stock agent'}

{language === 'zh' - ? '分析行情、执行交易、搜索股票 — 用自然语言就行' - : 'Analyze markets, execute trades, search stocks — just ask'} + ? '美股、大宗、外汇、Pre-IPO — 用自然语言描述策略即可' + : 'US stocks, commodities, FX, Pre-IPO — describe the strategy in plain English'}

diff --git a/web/src/components/trader/ModelConfigModal.tsx b/web/src/components/trader/ModelConfigModal.tsx index 326054e0..fa83bc70 100644 --- a/web/src/components/trader/ModelConfigModal.tsx +++ b/web/src/components/trader/ModelConfigModal.tsx @@ -11,7 +11,6 @@ import { BLOCKRUN_MODELS, CLAW402_MODELS, AI_PROVIDER_CONFIG, - DEFAULT_CLAW402_MODEL, getShortName, } from './model-constants' diff --git a/web/src/pages/AgentChatPage.tsx b/web/src/pages/AgentChatPage.tsx index f0a35de2..0ad2336a 100644 --- a/web/src/pages/AgentChatPage.tsx +++ b/web/src/pages/AgentChatPage.tsx @@ -7,6 +7,7 @@ import { Wallet, Bot, Bookmark, + Zap, ChevronDown, ChevronRight, } from 'lucide-react' @@ -19,6 +20,8 @@ import { WelcomeScreen } from '../components/agent/WelcomeScreen' import { ChatMessages } from '../components/agent/ChatMessages' import { ChatInput, type ChatInputHandle } from '../components/agent/ChatInput' import { UserPreferencesPanel } from '../components/agent/UserPreferencesPanel' +import { HyperliquidSymbolsPanel } from '../components/agent/HyperliquidSymbolsPanel' +import { createHyperliquidQuickTrader } from '../lib/hyperliquidQuickTrade' import { useAgentChatStore } from '../stores/agentChatStore' import type { AgentMessage as Message, AgentStep } from '../types/agent' import { @@ -459,6 +462,7 @@ export function AgentChatPage() { const setDraftText = useAgentChatStore((state) => state.setDraftText) const messagesEndRef = useRef(null) const chatInputRef = useRef(null) + const pendingHyperSymbolRef = useRef(null) // Sidebar section collapse state const [sections, setSections] = useState({ @@ -466,6 +470,7 @@ export function AgentChatPage() { positions: true, traders: false, preferences: true, + hyperliquid: true, }) const toggleSection = (key: keyof typeof sections) => { @@ -577,9 +582,88 @@ export function AgentChatPage() { chatInputRef.current?.focus() } + const tradeHyperliquidSymbol = async (symbol: { symbol: string; display?: string; category?: string }) => { + const label = symbol.display || symbol.symbol + const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + patchMessagesInStore( + (prev) => [ + ...prev, + { + id: nextId(), + role: 'user', + text: + language === 'zh' + ? `快速创建 Hyperliquid ${label} 单标的交易员` + : `Quick-create a Hyperliquid ${label} single-symbol trader`, + time, + }, + { + id: nextId(), + role: 'bot', + text: language === 'zh' ? '正在直接创建策略和 Trader,不再走聊天意图猜测…' : 'Creating the strategy and trader directly, without routing through chat intent guessing…', + time, + streaming: true, + }, + ], + user?.id || storageUserId + ) + try { + const result = await createHyperliquidQuickTrader(symbol, language === 'zh' ? 'zh' : 'en') + patchMessagesInStore( + (prev) => + prev.map((m) => + m.streaming + ? { + ...m, + streaming: false, + time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + text: + language === 'zh' + ? `${result.reusedTrader ? '已找到并复用' : '已创建'} Hyperliquid ${result.display} 单标的 Trader:${result.traderName}\n\n策略:${result.strategyName}\n标的:${result.display}\n\n我没有自动启动实盘交易。请到 Traders 面板确认风控后手动 Start。` + : `${result.reusedTrader ? 'Reused existing' : 'Created'} Hyperliquid ${result.display} single-symbol trader: ${result.traderName}\n\nStrategy: ${result.strategyName}\nSymbol: ${result.display}\n\nLive trading was not auto-started. Review risk controls in Traders, then start manually.`, + } + : m + ), + user?.id || storageUserId + ) + window.dispatchEvent(new CustomEvent('agent-config-refresh')) + } catch (err: any) { + patchMessagesInStore( + (prev) => + prev.map((m) => + m.streaming + ? { + ...m, + streaming: false, + time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + text: `⚠️ ${err?.message || 'Failed to create quick trader'}`, + } + : m + ), + user?.id || storageUserId + ) + } + } + + useEffect(() => { + const params = new URLSearchParams(window.location.search) + const symbol = params.get('hyperSymbol') + if (!symbol || pendingHyperSymbolRef.current === symbol || loading) return + pendingHyperSymbolRef.current = symbol + void tradeHyperliquidSymbol({ + symbol, + display: params.get('hyperDisplay') || symbol, + category: params.get('hyperCategory') || 'stock', + }) + const cleanUrl = `${window.location.pathname}${window.location.hash}` + window.history.replaceState({}, '', cleanUrl) + }, [loading, language]) + const quickActions = language === 'zh' ? [ + { label: '🇺🇸 创建美股Agent', cmd: '创建一个美股趋势交易 Agent,默认选择5个强势美股,严格风控' }, + { label: '🗣 一句话策略', cmd: '我想做美股强趋势突破,帮我生成策略和Agent' }, { label: '💼 持仓', cmd: '/positions' }, { label: '💰 余额', cmd: '/balance' }, { label: '📋 Traders', cmd: '/traders' }, @@ -588,6 +672,8 @@ export function AgentChatPage() { { label: '❓ 帮助', cmd: '/help' }, ] : [ + { label: '🇺🇸 Create US Stock Agent', cmd: 'Create a US stock trend-following agent with 5 strong stocks and strict risk control' }, + { label: '🗣 One-line strategy', cmd: 'I want a US stock breakout strategy; build the strategy and agent' }, { label: '💼 Positions', cmd: '/positions' }, { label: '💰 Balance', cmd: '/balance' }, { label: '📋 Traders', cmd: '/traders' }, @@ -603,6 +689,18 @@ export function AgentChatPage() { title: language === 'zh' ? '市场行情' : 'Market', component: , }, + { + key: 'hyperliquid' as const, + icon: , + title: language === 'zh' ? '美股/全球标的' : 'US & Global Markets', + component: ( + + ), + }, { key: 'positions' as const, icon: ,