feat(web): quick-trade button actually trades - auto-start + honest status

The lightning button on the symbol panel was the single biggest
"agent does nothing" complaint: it created a trader and a strategy via
direct REST calls, then handed the user a hardcoded reply that read
"我没有自动启动实盘交易。请到 Traders 面板确认风控后手动 Start" —
i.e. the chat bot openly admitted it bypassed the agent and refused to
do the work the user had clearly asked for.

- web/src/lib/hyperliquidQuickTrade.ts: after createStrategy +
  createTrader (or finding an existing trader), call POST
  /api/traders/:id/start immediately. Report `started`, `reusedTrader`,
  and an optional `startError` so the chat reply can be honest about
  what happened — created vs reused, running vs failed, and why.

- web/src/pages/AgentChatPage.tsx: replace the canned "please start
  manually" reply with one that reflects reality. Success path shows
  the symbol, strategy, 5-min scan interval, and how to halt it via
  chat. Failure path surfaces the actual start error and tells the
  user the trader exists but is not running.

- web/src/lib/hyperliquidQuickTrade.ts: per-symbol prompt now routes
  on category. Stocks (category="stock") get a long-only, momentum-
  seeking prompt — break of high, volume spike, support reclaim, sector
  catalyst — because shorting individual US equities through the agent
  is rarely what the user wants. Crypto stays bidirectional but
  disciplined. The trader-level custom_prompt is rewritten in the same
  style and explicitly forbids rotating to other symbols.
This commit is contained in:
tinkle-community
2026-05-29 22:13:51 +08:00
parent 1851508353
commit e4adafa364
2 changed files with 123 additions and 21 deletions

View File

@@ -10,6 +10,10 @@ export interface QuickTradeResult {
symbol: string
display: string
reusedTrader: boolean
/** Whether the trader was successfully started after creation. */
started: boolean
/** Set when start failed — the trader exists but is NOT running. */
startError?: string
}
function compactSymbolName(symbol: string) {
@@ -27,7 +31,30 @@ function pickHyperliquidExchange(exchanges: Exchange[]) {
})
}
function buildSingleSymbolConfig(base: StrategyConfig, symbol: string, language: 'zh' | 'en'): StrategyConfig {
// Returns the custom prompt for the quick-create flow. Stocks default to a
// long-biased, momentum-seeking trader because the Hyperliquid US-stock
// products are designed for one-directional exposure and shorting individual
// equities through the agent has rarely been what the user actually wanted.
// Crypto stays bidirectional but conservative.
function buildQuickCreatePrompt(symbol: string, category: string | undefined, language: 'zh' | 'en'): string {
const isStock = (category || '').toLowerCase() === 'stock'
if (isStock) {
return language === 'zh'
? `只交易 Hyperliquid USDC 永续合约 ${symbol} (美股)。\n\n核心策略: 直接做多, 不做空。\n- 出现以下任一情形主动开多: 突破前期高点、放量上涨、关键支撑位回踩反弹、强势板块同步走强、宏观/财报/事件利好。\n- 不要在没有任何看多依据时硬等"完美进场点"。轻仓试错优于错过趋势。\n- 单标的策略, 不切换到其他股票。\n- 风控: 严格止损 (1-3% 单笔), 利润目标至少 2:1, 单笔不超过权益的 25%, 默认 2-3 倍杠杆, 切勿满仓。\n- 每次决策前必须先调 get_balance, get_positions, get_market_snapshot 拿到实时数据。`
: `Trade only the Hyperliquid USDC perpetual market ${symbol} (US equity).\n\nCore strategy: long-only — do not short.\n- Open longs proactively on any of these: breakout above prior high, volume expansion on up bars, pullback to a key support that holds, strong sector tape, macro/earnings/news catalysts.\n- Don't wait endlessly for the "perfect entry" — a small probing long beats missing a trend.\n- Single-symbol; do not rotate into other stocks.\n- Risk: stop-loss 1-3% per trade, profit target ≥ 2:1, max 25% of equity per trade, default 2-3x leverage, never go all-in.\n- Always call get_balance, get_positions, get_market_snapshot before deciding.`
}
// Crypto / other → keep bidirectional but cautious.
return language === 'zh'
? `只交易 Hyperliquid USDC 永续合约 ${symbol}\n\n核心策略: 多空双向, 谨慎进场。\n- 每次决策前必须调 get_balance, get_positions, get_market_snapshot, 拿到实时价格、成交量、资金费率、持仓量。\n- 趋势明确 + 成交量配合 + 风险回报比 ≥ 2:1 才开仓; 模糊就空仓等。\n- 风控: 严格止损 (1-3% 单笔), 单笔不超过权益的 25%, 默认 3 倍杠杆, 切勿满仓。\n- 单标的策略, 不切换到其他币种。`
: `Trade only the Hyperliquid USDC perpetual market ${symbol}.\n\nCore strategy: bidirectional but disciplined.\n- Always call get_balance, get_positions, get_market_snapshot first for live price, volume, funding, and OI.\n- Open positions only when trend, volume, and a ≥ 2:1 risk/reward all line up; otherwise stay flat.\n- Risk: stop-loss 1-3% per trade, max 25% of equity per trade, default 3x leverage, never go all-in.\n- Single-symbol; do not rotate.`
}
function buildSingleSymbolConfig(
base: StrategyConfig,
symbol: string,
category: string | undefined,
language: 'zh' | 'en'
): StrategyConfig {
const staticCoinSource = {
source_type: 'static' as const,
static_coins: [symbol],
@@ -38,10 +65,7 @@ function buildSingleSymbolConfig(base: StrategyConfig, symbol: string, language:
use_hyper_all: false,
use_hyper_main: false,
}
const customPrompt =
language === 'zh'
? `只交易 Hyperliquid USDC 永续合约 ${symbol}。每次决策必须先检查账户余额、现有持仓、最新价格、趋势、成交量、资金费率和风险限制。没有明确优势时保持观望。单标的策略,不要切换到其他币种。`
: `Trade only the Hyperliquid USDC perpetual market ${symbol}. Before every decision, check balance, current positions, latest price, trend, volume, funding, and risk limits. Stay flat when there is no clear edge. Single-symbol strategy; do not switch to other symbols.`
const customPrompt = buildQuickCreatePrompt(symbol, category, language)
return {
...base,
@@ -61,11 +85,15 @@ function buildSingleSymbolConfig(base: StrategyConfig, symbol: string, language:
}
export async function createHyperliquidQuickTrader(
symbolInput: MarketSymbol | { symbol: string; display?: string },
symbolInput: MarketSymbol | { symbol: string; display?: string; category?: string },
language: 'zh' | 'en'
): Promise<QuickTradeResult> {
const symbol = symbolInput.symbol
const display = symbolInput.display || symbol
// Category drives the strategy bias: stocks default to long-only, crypto
// stays bidirectional. Passed through from the panel button which already
// knows whether each row is a stock or a crypto perpetual.
const category = (symbolInput as { category?: string }).category
const compact = compactSymbolName(display)
const traderName = `HL ${compact} Quick`.slice(0, 50)
const strategyName = `HL ${compact} Strategy`.slice(0, 50)
@@ -93,31 +121,59 @@ export async function createHyperliquidQuickTrader(
)
if (existingTrader) {
const existing = existingTrader as any
const existingId = existing.trader_id || existing.id
const wasRunning = Boolean(existing.is_running)
let started = wasRunning
let startError: string | undefined
if (!wasRunning) {
try {
await api.startTrader(existingId)
started = true
} catch (err: any) {
startError = err?.message || String(err)
}
}
return {
traderId: existing.trader_id || existing.id,
traderId: existingId,
traderName: existing.trader_name || existing.name || traderName,
strategyId: existing.strategy_id || '',
strategyName,
symbol,
display,
reusedTrader: true,
started,
startError,
}
}
let strategy = strategies.find((s: any) => String(s.name || '').toLowerCase() === strategyName.toLowerCase()) as any
if (!strategy?.id) {
const defaultConfig = await api.getDefaultStrategyConfig()
const config = buildSingleSymbolConfig(defaultConfig, symbol, language)
const config = buildSingleSymbolConfig(defaultConfig, symbol, category, language)
const isStock = (category || '').toLowerCase() === 'stock'
const description = language === 'zh'
? isStock
? `Hyperliquid ${display} (美股) 单标的快速做多策略 — 主动捕捉突破、放量、回踩反弹。`
: `Hyperliquid ${display} 单标的快速交易策略 — 多空双向, 等趋势 + 量能确认。`
: isStock
? `Hyperliquid ${display} single-symbol long-only momentum strategy.`
: `Hyperliquid ${display} single-symbol bidirectional trading strategy.`
strategy = await api.createStrategy({
name: strategyName,
description:
language === 'zh'
? `Hyperliquid ${display} 单标的快速交易策略。`
: `Hyperliquid ${display} single-symbol quick trading strategy.`,
description,
config,
} as any)
}
const isStock = (category || '').toLowerCase() === 'stock'
const traderPrompt = language === 'zh'
? isStock
? `固定只交易 Hyperliquid ${symbol} (美股), 单向做多, 不做空。\n关注: 突破、放量、回踩反弹、宏观/财报利好。\n禁: 切换标的、做空、满仓、无止损。`
: `固定只交易 Hyperliquid ${symbol}, 多空双向, 不要扩展到其他标的。每次决策前先看余额、仓位、最新价格。`
: isStock
? `Only trade Hyperliquid ${symbol} (US stock), long-only, no shorting.\nWatch for: breakouts, volume spikes, support reclaims, macro/earnings catalysts.\nForbidden: rotating to other symbols, shorting, going all-in, trading without a stop.`
: `Only trade Hyperliquid ${symbol}; bidirectional, do not expand to other symbols. Re-check balance, positions, and live price before every decision.`
const trader = await api.createTrader({
name: traderName,
ai_model_id: model.id,
@@ -126,19 +182,34 @@ export async function createHyperliquidQuickTrader(
scan_interval_minutes: 5,
trading_symbols: symbol,
show_in_competition: false,
custom_prompt:
language === 'zh'
? `固定只交易 Hyperliquid ${symbol},不要扩展到其他标的。启动前再次检查余额、仓位和风险。`
: `Only trade Hyperliquid ${symbol}; do not expand to other symbols. Re-check balance, positions, and risk before starting.`,
custom_prompt: traderPrompt,
})
const traderId = trader.trader_id || (trader as any).id
// The whole point of the ⚡ button is "do it now". After creating the trader
// we immediately start it; the previous behavior of stopping at "created,
// please start manually" was the single biggest source of the "agent does
// nothing" complaint. If start fails (e.g. wallet not funded yet), we
// report it so the chat reply can be honest about the state.
let started = false
let startError: string | undefined
try {
await api.startTrader(traderId)
started = true
} catch (err: any) {
startError = err?.message || String(err)
}
return {
traderId: trader.trader_id || (trader as any).id,
traderId,
traderName: trader.trader_name || (trader as any).name || traderName,
strategyId: strategy.id,
strategyName,
symbol,
display,
reusedTrader: false,
started,
startError,
}
}

View File

@@ -609,6 +609,40 @@ export function AgentChatPage() {
)
try {
const result = await createHyperliquidQuickTrader(symbol, language === 'zh' ? 'zh' : 'en')
// The reply tells the truth about what happened: created OR reused, AND
// whether the trader is now actually running. If start failed we say so
// explicitly so the user knows there's still work to do — but we no
// longer pretend the user has to "go push a button" when we could've
// done it ourselves.
const isZh = language === 'zh'
let text: string
if (result.started) {
const verb = isZh
? result.reusedTrader
? '已复用并启动'
: '已创建并启动'
: result.reusedTrader
? 'Reused and started'
: 'Created and started'
text = isZh
? `${verb} Hyperliquid ${result.display} 单标的 Trader: ${result.traderName}\n\n• 策略: ${result.strategyName}\n• 标的: ${result.display}\n• 扫描间隔: 每 5 分钟\n\nAgent 已在工作。要看实时持仓在 Dashboard, 想停掉随时回这里说"停掉 ${result.traderName}"。`
: `${verb} Hyperliquid ${result.display} single-symbol trader: ${result.traderName}\n\n• Strategy: ${result.strategyName}\n• Symbol: ${result.display}\n• Scan interval: every 5 min\n\nThe agent is live. Watch positions in Dashboard, or tell me "stop ${result.traderName}" to halt it.`
} else {
const verb = isZh
? result.reusedTrader
? '已找到 Trader'
: '已创建 Trader'
: result.reusedTrader
? 'Found existing trader'
: 'Created trader'
const reason = result.startError ? `\n\n启动失败原因: ${result.startError}` : ''
const reasonEn = result.startError ? `\n\nStart failed: ${result.startError}` : ''
text = isZh
? `${verb}: ${result.traderName}\n\n• 策略: ${result.strategyName}\n• 标的: ${result.display}${reason}\n\n需要先解决启动问题, 我才能让它跑起来。`
: `${verb}: ${result.traderName}\n\n• Strategy: ${result.strategyName}\n• Symbol: ${result.display}${reasonEn}\n\nResolve the start error before the trader can run.`
}
patchMessagesInStore(
(prev) =>
prev.map((m) =>
@@ -617,10 +651,7 @@ export function AgentChatPage() {
...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.`,
text,
}
: m
),