mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user