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
This commit is contained in:
tinklefund
2026-05-25 01:25:10 +08:00
parent 5bdffee3b0
commit f4ee723aa2
8 changed files with 386 additions and 24 deletions

View File

@@ -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)
}

View File

@@ -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,
)

View File

@@ -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"}`

View File

@@ -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)
}

View File

@@ -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<string, { zh: string; en: string }> = {
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<MarketSymbol[]>([])
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 (
<div style={{ padding: 12, color: '#848E9C', fontSize: 12 }}>
{language === 'zh' ? '正在加载 Hyperliquid 全市场标的…' : 'Loading Hyperliquid markets…'}
</div>
)
}
if (error) {
return (
<div style={{ padding: 12, color: '#F6465D', fontSize: 12 }}>
{language === 'zh' ? '标的列表加载失败:' : 'Failed to load symbols: '}{error}
</div>
)
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 10px',
borderRadius: 10,
border: '1px solid rgba(255,255,255,0.06)',
background: 'rgba(255,255,255,0.025)',
}}
>
<Search size={13} color="#6c6c82" />
<input
value={query}
onChange={(e) => 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,
}}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 6 }}>
{([
{ 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 (
<button
key={item.key}
onClick={() => setRanking(item.key)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 5,
padding: '7px 6px',
borderRadius: 10,
border: active ? '1px solid rgba(240,185,11,0.55)' : '1px solid rgba(255,255,255,0.06)',
background: active ? 'rgba(240,185,11,0.12)' : 'rgba(255,255,255,0.025)',
color: active ? '#F0B90B' : '#848E9C',
fontSize: 11,
fontWeight: 700,
cursor: 'pointer',
}}
>
<Icon size={12} />
{language === 'zh' ? item.zh : item.en}
</button>
)
})}
</div>
<div className="hide-scrollbar" style={{ display: 'flex', gap: 6, overflowX: 'auto' }}>
{categories.map((c) => (
<button
key={c}
onClick={() => setCategory(c)}
style={{
padding: '5px 8px',
borderRadius: 999,
border: c === category ? '1px solid rgba(240,185,11,0.55)' : '1px solid rgba(255,255,255,0.06)',
background: c === category ? 'rgba(240,185,11,0.12)' : 'rgba(255,255,255,0.025)',
color: c === category ? '#F0B90B' : '#848E9C',
fontSize: 10.5,
whiteSpace: 'nowrap',
cursor: 'pointer',
}}
>
{c === 'all'
? language === 'zh'
? '全部'
: 'All'
: CATEGORY_LABEL[c]?.[language === 'zh' ? 'zh' : 'en'] || c}
</button>
))}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 340, overflowY: 'auto', paddingRight: 2 }}>
{filtered.map((s) => {
const display = s.display || s.symbol
return (
<button
key={`${s.exchange || 'hyper'}-${s.symbol}`}
disabled={disabled}
onClick={() => onTradeSymbol(s)}
style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
gap: 8,
textAlign: 'left',
padding: '10px 11px',
borderRadius: 10,
border: '1px solid rgba(255,255,255,0.05)',
background: 'rgba(255,255,255,0.025)',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.6 : 1,
}}
title={language === 'zh' ? `点击让 Agent 交易 ${display}` : `Ask agent to trade ${display}`}
>
<div style={{ minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ color: '#EAECEF', fontWeight: 700, fontSize: 12.5 }}>{display}</span>
<span style={{ color: '#4c4c62', fontSize: 10 }}>{s.category}</span>
{!!s.maxLeverage && <span style={{ color: '#F0B90B', fontSize: 10 }}>{s.maxLeverage}x</span>}
</div>
<div style={{ display: 'flex', gap: 6, alignItems: 'center', color: '#6c6c82', fontSize: 10.5, marginTop: 3 }}>
<span style={{ color: '#6c6c82', fontSize: 10.5 }}>
Vol {formatVolume(s.volume_24h)} · {formatPrice(s.mark_price)}
</span>
<span style={{ color: Number(s.change_24h_pct || 0) >= 0 ? '#0ECB81' : '#F6465D', fontSize: 10.5, fontWeight: 700 }}>
{formatChange(s.change_24h_pct)}
</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', color: '#F0B90B', gap: 4, fontSize: 11, fontWeight: 700 }}>
<Zap size={13} />
{language === 'zh' ? '交易' : 'Trade'}
</div>
</button>
)
})}
</div>
<div style={{ color: '#4c4c62', fontSize: 10.5, lineHeight: 1.45 }}>
{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.`}
</div>
</div>
)
}

View File

@@ -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: <BarChart3 size={18} />, title: '分析 BTC 走势', subtitle: '技术分析 + 市场情绪', cmd: '分析一下 BTC 的走势' },
{ icon: <Zap size={18} />, title: '做多 ETH', subtitle: 'Agent 帮你自动下单', cmd: '帮我做多 ETH 0.01 手' },
{ icon: <Search size={18} />, title: '搜索股', subtitle: '输入名称或代码即可', cmd: '搜索一下中远海控' },
{ icon: <Lightbulb size={18} />, title: '策略建议', subtitle: '根据当前市场给出建议', cmd: '当前市场适合什么策略?' },
{ icon: <Bot size={18} />, title: '创建美股 Agent', subtitle: '强势股 + 严格风控', cmd: '创建一个美股趋势交易 Agent默认选择5个强势美股严格风控' },
{ icon: <Zap size={18} />, title: '一句话建策略', subtitle: '从想法到 Agent', cmd: '我想做美股强趋势突破帮我生成策略和Agent' },
{ icon: <Search size={18} />, title: '搜索股', subtitle: '输入名称或代码即可', cmd: '搜索一下 NVIDIA 和 Apple' },
{ icon: <Lightbulb size={18} />, title: '全球资产策略', subtitle: '美股/黄金/外汇', cmd: '当前美股、黄金、外汇适合什么策略?' },
]
: [
{ icon: <BarChart3 size={18} />, title: 'Analyze BTC', subtitle: 'Technical analysis + sentiment', cmd: 'Analyze BTC price action' },
{ icon: <Zap size={18} />, title: 'Trade ETH', subtitle: 'Agent executes for you', cmd: 'Open a long position on ETH 0.01' },
{ icon: <Search size={18} />, title: 'Search Stocks', subtitle: 'Enter name or ticker', cmd: 'Search for NVIDIA stock' },
{ icon: <Lightbulb size={18} />, title: 'Strategy Ideas', subtitle: 'Market-based suggestions', cmd: 'What strategy fits the current market?' },
{ icon: <Bot size={18} />, 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: <Zap size={18} />, title: 'One-line strategy', subtitle: 'Idea to agent', cmd: 'I want a US stock breakout strategy; build the strategy and agent' },
{ icon: <Search size={18} />, title: 'Search US stocks', subtitle: 'Name or ticker', cmd: 'Search for NVIDIA and Apple stocks' },
{ icon: <Lightbulb size={18} />, 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'}
</h1>
<p style={{
fontSize: 13.5,
@@ -81,8 +81,8 @@ export function WelcomeScreen({ language, onSend }: WelcomeScreenProps) {
lineHeight: 1.5,
}}>
{language === 'zh'
? '分析行情、执行交易、搜索股票 — 用自然语言就行'
: 'Analyze markets, execute trades, search stocks — just ask'}
? '美股、大宗、外汇、Pre-IPO — 用自然语言描述策略即可'
: 'US stocks, commodities, FX, Pre-IPO — describe the strategy in plain English'}
</p>
</motion.div>

View File

@@ -11,7 +11,6 @@ import {
BLOCKRUN_MODELS,
CLAW402_MODELS,
AI_PROVIDER_CONFIG,
DEFAULT_CLAW402_MODEL,
getShortName,
} from './model-constants'

View File

@@ -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<HTMLDivElement>(null)
const chatInputRef = useRef<ChatInputHandle>(null)
const pendingHyperSymbolRef = useRef<string | null>(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: <MarketTicker />,
},
{
key: 'hyperliquid' as const,
icon: <Zap size={14} />,
title: language === 'zh' ? '美股/全球标的' : 'US & Global Markets',
component: (
<HyperliquidSymbolsPanel
language={language}
disabled={loading}
onTradeSymbol={tradeHyperliquidSymbol}
/>
),
},
{
key: 'positions' as const,
icon: <Wallet size={14} />,