diff --git a/agent/trade.go b/agent/trade.go index 6c48c0ce..a626f0ff 100644 --- a/agent/trade.go +++ b/agent/trade.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "math" + "nofx/market" "nofx/store" "strings" "sync" @@ -328,7 +329,7 @@ func validateTradeAction( maxLeverage := riskControl.AltcoinMaxLeverage maxPositionValueRatio := riskControl.AltcoinMaxPositionValueRatio - if isBTCETHSymbol(trade.Symbol) { + if isMajorTradeSymbol(trade.Symbol) { maxLeverage = riskControl.BTCETHMaxLeverage maxPositionValueRatio = riskControl.BTCETHMaxPositionValueRatio } @@ -375,6 +376,17 @@ func isBTCETHSymbol(symbol string) bool { return strings.HasPrefix(symbol, "BTC") || strings.HasPrefix(symbol, "ETH") } +// isMajorTradeSymbol mirrors trader/auto_trader_risk.isMajorAsset for the +// chat-execute path. BTC/ETH crypto perps and Hyperliquid XYZ assets +// (US stocks, commodities, forex) get the higher BTC/ETH risk tier — their +// per-position caps should not be clamped to the 1x altcoin tier. +func isMajorTradeSymbol(symbol string) bool { + if isBTCETHSymbol(symbol) { + return true + } + return market.IsXyzDexAsset(symbol) +} + // formatTradeConfirmation creates a confirmation message for a pending trade. func formatTradeConfirmation(trade *TradeAction, lang string) string { actionNames := map[string]string{ diff --git a/kernel/engine_position.go b/kernel/engine_position.go index 437aea1a..a1157e69 100644 --- a/kernel/engine_position.go +++ b/kernel/engine_position.go @@ -3,6 +3,7 @@ package kernel import ( "fmt" "nofx/logger" + "nofx/market" ) // ============================================================================ @@ -33,10 +34,18 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi } if d.Action == "open_long" || d.Action == "open_short" { + // Asset tiering for validation: + // - BTC/ETH crypto perps use the BTC/ETH tier (typically 5x equity). + // - Hyperliquid XYZ assets (US equities, commodities, forex) are + // also treated as the higher tier — they are not crypto altcoins + // and the user's quick-trade flow shows them at the higher cap, + // so the validator must match. + // - Everything else is altcoin (1x equity by default). maxLeverage := altcoinLeverage posRatio := altcoinPosRatio maxPositionValue := accountEquity * posRatio - if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { + isMajor := d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" || market.IsXyzDexAsset(d.Symbol) + if isMajor { maxLeverage = btcEthLeverage posRatio = btcEthPosRatio maxPositionValue = accountEquity * posRatio @@ -69,9 +78,12 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi tolerance := maxPositionValue * 0.01 if d.PositionSizeUSD > maxPositionValue+tolerance { - if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { + switch { + case d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT": return fmt.Errorf("BTC/ETH single coin position value cannot exceed %.0f USDT (%.1fx account equity), actual: %.0f", maxPositionValue, posRatio, d.PositionSizeUSD) - } else { + case market.IsXyzDexAsset(d.Symbol): + return fmt.Errorf("%s position value cannot exceed %.0f USDT (%.1fx account equity), actual: %.0f", d.Symbol, maxPositionValue, posRatio, d.PositionSizeUSD) + default: return fmt.Errorf("altcoin single coin position value cannot exceed %.0f USDT (%.1fx account equity), actual: %.0f", maxPositionValue, posRatio, d.PositionSizeUSD) } } diff --git a/kernel/engine_prompt.go b/kernel/engine_prompt.go index 9f1b7ee8..c4ceb682 100644 --- a/kernel/engine_prompt.go +++ b/kernel/engine_prompt.go @@ -18,34 +18,49 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string var sb strings.Builder riskControl := e.config.RiskControl promptSections := e.config.PromptSections + lang := e.GetLanguage() + zh := lang == LangChinese + singleSymbol, primarySymbol := e.singleSymbolInfo() + + // XYZ-only override: when the strategy trades a single Hyperliquid XYZ + // asset (US stocks, commodities, forex), force the entire prompt to + // English regardless of the strategy's stored language. Mixing Chinese + // reasoning with US-equity analysis confuses the LLM (its US-stock + // training is overwhelmingly English) and the user prompt sections + // ended up looking incoherent because some sections respect the + // language flag while legacy stored sections were always English. + if singleSymbol && market.IsXyzDexAsset(primarySymbol) { + zh = false + lang = LangEnglish + } // 0. Data Dictionary & Schema (ensure AI understands all fields) - lang := e.GetLanguage() - schemaPrompt := GetSchemaPrompt(lang) - sb.WriteString(schemaPrompt) + sb.WriteString(GetSchemaPrompt(lang)) sb.WriteString("\n\n") sb.WriteString("---\n\n") - // 1. Role definition (editable) + // 1. Role definition (editable; falls back to a generic intro in the + // correct language so we don't mix EN headings with ZH custom text). if promptSections.RoleDefinition != "" { sb.WriteString(promptSections.RoleDefinition) sb.WriteString("\n\n") + } else if zh { + sb.WriteString("# 你是一名专业的 Hyperliquid USDC 多资产交易 AI\n\n") + sb.WriteString("你的任务是基于提供的市场数据做出交易决策。\n\n") } else { - sb.WriteString("# You are a professional cryptocurrency trading AI\n\n") - sb.WriteString("Your task is to make trading decisions based on provided market data.\n\n") + sb.WriteString("# You are a professional Hyperliquid USDC multi-asset trading AI\n\n") + sb.WriteString("Your task is to make trading decisions based on the provided market data.\n\n") } // 2. Trading mode variant - switch strings.ToLower(strings.TrimSpace(variant)) { - case "aggressive": - sb.WriteString("## Mode: Aggressive\n- Prioritize capturing trend breakouts, can build positions in batches when confidence ≥ 70\n- Allow higher positions, but must strictly set stop-loss and explain risk-reward ratio\n\n") - case "conservative": - sb.WriteString("## Mode: Conservative\n- Only open positions when multiple signals resonate\n- Prioritize cash preservation, must pause for multiple periods after consecutive losses\n\n") - case "scalping": - sb.WriteString("## Mode: Scalping\n- Focus on short-term momentum, smaller profit targets but require quick action\n- If price doesn't move as expected within two bars, immediately reduce position or stop-loss\n\n") - } + writeModeVariant(&sb, variant, zh) - // 3. Hard constraints (risk control) + // 3. Hard constraints (risk control). + // + // `singleSymbol` is true for strategies that deliberately trade just one + // instrument (the quick-create flow, single-asset templates). For those, + // the "BTC/ETH vs Altcoin" two-tier categorization is irrelevant and + // actively misleading — we surface a single position-value limit instead. btcEthPosValueRatio := riskControl.BTCETHMaxPositionValueRatio if btcEthPosValueRatio <= 0 { btcEthPosValueRatio = 5.0 @@ -55,168 +70,422 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string altcoinPosValueRatio = 1.0 } - sb.WriteString("# Hard Constraints (Risk Control)\n\n") - sb.WriteString("## CODE ENFORCED (Backend validation, cannot be bypassed):\n") - sb.WriteString(fmt.Sprintf("- Max Positions: %d coins simultaneously\n", riskControl.MaxPositions)) - sb.WriteString(fmt.Sprintf("- Position Value Limit (Altcoins): max %.0f USDT (= equity %.0f × %.1fx)\n", - accountEquity*altcoinPosValueRatio, accountEquity, altcoinPosValueRatio)) - sb.WriteString(fmt.Sprintf("- Position Value Limit (BTC/ETH): max %.0f USDT (= equity %.0f × %.1fx)\n", - accountEquity*btcEthPosValueRatio, accountEquity, btcEthPosValueRatio)) - sb.WriteString(fmt.Sprintf("- Max Margin Usage: ≤%.0f%%\n", riskControl.MaxMarginUsage*100)) - sb.WriteString(fmt.Sprintf("- Min Position Size: ≥%.0f USDT\n\n", riskControl.MinPositionSize)) - - sb.WriteString("## AI GUIDED (Recommended, you should follow):\n") - sb.WriteString(fmt.Sprintf("- Trading Leverage: Altcoins max %dx | BTC/ETH max %dx\n", - riskControl.AltcoinMaxLeverage, riskControl.BTCETHMaxLeverage)) - sb.WriteString(fmt.Sprintf("- Risk-Reward Ratio: ≥1:%.1f (take_profit / stop_loss)\n", riskControl.MinRiskRewardRatio)) - sb.WriteString(fmt.Sprintf("- Min Confidence: ≥%d to open position\n\n", riskControl.MinConfidence)) - - // Position sizing guidance - sb.WriteString("## Position Sizing Guidance\n") - sb.WriteString("Calculate `position_size_usd` based on your confidence and the Position Value Limits above:\n") - sb.WriteString("- High confidence (≥85): Use 80-100%% of max position value limit\n") - sb.WriteString("- Medium confidence (70-84): Use 50-80%% of max position value limit\n") - sb.WriteString("- Low confidence (60-69): Use 30-50%% of max position value limit\n") - sb.WriteString(fmt.Sprintf("- Example: With equity %.0f and BTC/ETH ratio %.1fx, max is %.0f USDT\n", - accountEquity, btcEthPosValueRatio, accountEquity*btcEthPosValueRatio)) - sb.WriteString("- **DO NOT** just use available_balance as position_size_usd. Use the Position Value Limits!\n\n") + writeHardConstraints(&sb, accountEquity, riskControl, btcEthPosValueRatio, altcoinPosValueRatio, singleSymbol, primarySymbol, zh) // 4. Trading frequency (editable) if promptSections.TradingFrequency != "" { sb.WriteString(promptSections.TradingFrequency) sb.WriteString("\n\n") + } else if zh { + sb.WriteString("# ⏱️ 交易频率提醒\n\n") + sb.WriteString("- 优秀交易员: 每日 2-4 单 ≈ 每小时 0.1-0.2 单\n") + sb.WriteString("- 每小时 > 2 单 = 过度交易\n") + sb.WriteString("- 单笔持仓时长 ≥ 30-60 分钟\n") + sb.WriteString("如果你发现自己每个周期都在交易 → 入场标准过低; 如果不到 30 分钟就平仓 → 太冲动。\n\n") } else { sb.WriteString("# ⏱️ Trading Frequency Awareness\n\n") sb.WriteString("- Excellent traders: 2-4 trades/day ≈ 0.1-0.2 trades/hour\n") - sb.WriteString("- >2 trades/hour = Overtrading\n") + sb.WriteString("- >2 trades/hour = overtrading\n") sb.WriteString("- Single position hold time ≥ 30-60 minutes\n") - sb.WriteString("If you find yourself trading every period → standards too low; if closing positions < 30 minutes → too impatient.\n\n") + sb.WriteString("If you find yourself trading every cycle → standards too low; if closing positions < 30 minutes → too impulsive.\n\n") } // 5. Entry standards (editable) if promptSections.EntryStandards != "" { sb.WriteString(promptSections.EntryStandards) - sb.WriteString("\n\nYou have the following indicator data:\n") - e.writeAvailableIndicators(&sb) - sb.WriteString(fmt.Sprintf("\n**Confidence ≥ %d** required to open positions.\n\n", riskControl.MinConfidence)) + if zh { + sb.WriteString("\n\n你拥有以下指标数据:\n") + } else { + sb.WriteString("\n\nYou have the following indicator data:\n") + } + e.writeAvailableIndicators(&sb, zh) + if zh { + sb.WriteString(fmt.Sprintf("\n**置信度 ≥ %d** 才能开仓。\n\n", riskControl.MinConfidence)) + } else { + sb.WriteString(fmt.Sprintf("\n**Confidence ≥ %d** required to open positions.\n\n", riskControl.MinConfidence)) + } + } else if zh { + sb.WriteString("# 🎯 入场标准 (严格)\n\n") + sb.WriteString("只有当多重信号共振时才开仓。你拥有:\n") + e.writeAvailableIndicators(&sb, zh) + sb.WriteString(fmt.Sprintf("\n请自由使用任何有效的分析方法, 但**置信度 ≥ %d** 才能开仓; 避免低质量行为, 如单一指标、信号矛盾、横盘震荡、平仓后立刻再开等。\n\n", riskControl.MinConfidence)) } else { sb.WriteString("# 🎯 Entry Standards (Strict)\n\n") sb.WriteString("Only open positions when multiple signals resonate. You have:\n") - e.writeAvailableIndicators(&sb) - sb.WriteString(fmt.Sprintf("\nFeel free to use any effective analysis method, but **confidence ≥ %d** required to open positions; avoid low-quality behaviors such as single indicators, contradictory signals, sideways consolidation, reopening immediately after closing, etc.\n\n", riskControl.MinConfidence)) + e.writeAvailableIndicators(&sb, zh) + sb.WriteString(fmt.Sprintf("\nFeel free to use any effective analysis method, but **confidence ≥ %d** is required to open positions; avoid low-quality behaviors such as single-indicator entries, contradictory signals, sideways chop, or re-entering immediately after a close.\n\n", riskControl.MinConfidence)) } // 6. Decision process (editable) if promptSections.DecisionProcess != "" { sb.WriteString(promptSections.DecisionProcess) sb.WriteString("\n\n") + } else if zh { + sb.WriteString("# 📋 决策流程\n\n") + sb.WriteString("1. 检查持仓 → 是否需要止盈止损\n") + sb.WriteString("2. 扫描候选标的 + 多周期 → 是否有强信号\n") + sb.WriteString("3. 先写思维链, 再输出结构化 JSON\n\n") } else { sb.WriteString("# 📋 Decision Process\n\n") - sb.WriteString("1. Check positions → Should we take profit/stop-loss\n") - sb.WriteString("2. Scan candidate coins + multi-timeframe → Are there strong signals\n") + sb.WriteString("1. Check positions → take profit / stop loss?\n") + sb.WriteString("2. Scan candidates + multi-timeframe → are there strong signals?\n") sb.WriteString("3. Write chain of thought first, then output structured JSON\n\n") } - // 7. Output format - sb.WriteString("# Output Format (Strictly Follow)\n\n") - sb.WriteString("**Must use XML tags and to separate chain of thought and decision JSON, avoiding parsing errors**\n\n") - sb.WriteString("## Format Requirements\n\n") - sb.WriteString("\n") - sb.WriteString("Your chain of thought analysis...\n") - sb.WriteString("- Briefly analyze your thinking process \n") - sb.WriteString("\n\n") - sb.WriteString("\n") - sb.WriteString("Step 2: JSON decision array\n\n") - sb.WriteString("```json\n[\n") - // Use the actual configured position value ratio for BTC/ETH in the example - examplePositionSize := accountEquity * btcEthPosValueRatio - sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300},\n", - riskControl.BTCETHMaxLeverage, examplePositionSize)) - sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\"}\n") - sb.WriteString("]\n```\n") - sb.WriteString("\n\n") - sb.WriteString("## Field Description\n\n") - sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n") - sb.WriteString(fmt.Sprintf("- `confidence`: 0-100 (opening recommended ≥ %d)\n", riskControl.MinConfidence)) - sb.WriteString("- Required when opening: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n") - sb.WriteString("- **IMPORTANT**: All numeric values must be calculated numbers, NOT formulas/expressions (e.g., use `27.76` not `3000 * 0.01`)\n\n") + // 7. Output format — schema spec stays in English (this is a parser + // contract; reasoning copy is localized below). + writeOutputFormat(&sb, accountEquity, btcEthPosValueRatio, riskControl, singleSymbol, primarySymbol, zh) - // 8. Custom Prompt - if e.config.CustomPrompt != "" { - sb.WriteString("# 📌 Personalized Trading Strategy\n\n") - sb.WriteString(e.config.CustomPrompt) + // 8. Custom Prompt. + // + // For single-symbol Hyperliquid XYZ assets (US equities, commodities, + // forex), we replace any stored CustomPrompt with a built-in English + // stock-trader template. This serves two purposes: + // 1. The auto-generated CustomPrompt from the quick-create flow used + // to be Chinese (matching UI language), which produced an + // incoherent mixed-language final prompt that confused the LLM. + // 2. It guarantees a stock-specific, US-equity-tuned briefing + // regardless of when the strategy was first created. + customPrompt := e.config.CustomPrompt + if singleSymbol && market.IsXyzDexAsset(primarySymbol) { + customPrompt = buildXYZStockCustomPrompt(primarySymbol) + } + + if customPrompt != "" { + if zh { + sb.WriteString("# 📌 个性化交易策略\n\n") + } else { + sb.WriteString("# 📌 Personalized Trading Strategy\n\n") + } + sb.WriteString(customPrompt) sb.WriteString("\n\n") - sb.WriteString("Note: The above personalized strategy is a supplement to the basic rules and cannot violate the basic risk control principles.\n") + if zh { + sb.WriteString("说明: 上述个性化策略是基础规则的补充, 不能违反基础风控原则。\n") + } else { + sb.WriteString("Note: the above personalized strategy supplements the basic rules and may not violate the core risk controls.\n") + } } return sb.String() } -func (e *StrategyEngine) writeAvailableIndicators(sb *strings.Builder) { +// buildXYZStockCustomPrompt returns the canonical English long-only stock +// briefing the agent uses for single-symbol Hyperliquid USDC perpetuals on +// the XYZ board. This is the quick-trade flow's stance: when the user clicks +// the lightning button they want exposure NOW, not a watch-and-wait analyst. +// We bias the AI toward opening a probing long on every flat cycle, with +// risk guardrails to keep the size sane. Symbol is inlined for LLM grounding +// so it never confuses the trading instrument. +func buildXYZStockCustomPrompt(symbol string) string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Trade ONLY the Hyperliquid USDC perpetual %s (US equity / xyz board).\n\n", symbol)) + sb.WriteString("Core stance: LONG-ONLY, ACTIVE. Do not short. Do not sit on the sidelines.\n\n") + + sb.WriteString("## ⚡ Flat-Account Rule (HIGHEST PRIORITY)\n") + sb.WriteString("If `Current Positions` is None / empty AND no open exposure on this symbol, you MUST open a long position THIS cycle.\n") + sb.WriteString("- The user is paying for the quick-trade flow specifically to be in the market — your default is OPEN, not WAIT.\n") + sb.WriteString("- `wait` / `hold` are only acceptable when you ALREADY have an open position (then it's a hold decision).\n") + sb.WriteString("- If you cannot find a clear breakout setup, take a probing entry at market with reduced size (40-60% of max position value) and a tight 2% stop. Probing > waiting.\n") + sb.WriteString("- Set `confidence` >= 75 so backend risk control accepts the order. Justify the 75 in reasoning even if the technical picture is mixed — the strategic decision to be exposed IS the edge for this flow.\n\n") + + sb.WriteString("## Preferred Entry Conditions (use to size up, not to gate the entry)\n") + sb.WriteString("Size up toward the 80-100% bracket when you see ANY of:\n") + sb.WriteString("- Break of the prior session/intraday high on rising volume.\n") + sb.WriteString("- Pullback to a clearly held intraday support (prior swing low, VWAP, EMA20/50) with a bullish reaction bar.\n") + sb.WriteString("- Sector tape strength (broad US-equity bid, sympathy with peers in the same theme).\n") + sb.WriteString("- Confirmed catalyst: earnings beat, guide up, sector rotation, macro tailwind.\n\n") + + sb.WriteString("## Risk Guardrails (non-negotiable)\n") + sb.WriteString("- Per-trade stop-loss: 1.5-3% from entry. ALWAYS set a numeric `stop_loss`.\n") + sb.WriteString("- Take-profit: target at least R/R 2:1; set a numeric `take_profit`.\n") + sb.WriteString("- Per-trade notional: <= 25% of account equity (probing 10-15%, full 20-25%).\n") + sb.WriteString("- Leverage: 2-3x default, never above 5x. Never go all-in.\n") + sb.WriteString("- Once long, do NOT short the same cycle. Manage the open position first.\n\n") + + sb.WriteString("## Position Management (when already long)\n") + sb.WriteString("- Trail stop to breakeven once +1R, take partial profits at +2R if momentum stalls.\n") + sb.WriteString("- Cut quickly if price breaks the stop or the catalyst thesis fails.\n") + sb.WriteString("- Holding past 30 minutes is fine; flipping in/out every cycle is not.\n\n") + + sb.WriteString("## Discipline\n") + sb.WriteString(fmt.Sprintf("- Single-symbol mandate: never rotate into another ticker. The decision JSON `symbol` MUST be exactly \"%s\".\n", symbol)) + sb.WriteString("- Before every decision: check current price vs prior pivot, volume vs 5m/1h average, and the broader US-equity tape.\n") + sb.WriteString("- If positions are open, prioritize managing them over piling on new ones.") + return sb.String() +} + +// singleSymbolInfo returns (true, "ARM-USDC") for static-coin strategies that +// trade exactly one instrument. Multi-symbol strategies return (false, ""). +// The flag is used to drop crypto-specific "BTC/ETH vs Altcoin" labeling and +// to put the actual trading symbol into the JSON example. +func (e *StrategyEngine) singleSymbolInfo() (bool, string) { + coinSource := e.config.CoinSource + if coinSource.SourceType == "static" && len(coinSource.StaticCoins) == 1 { + return true, strings.ToUpper(strings.TrimSpace(coinSource.StaticCoins[0])) + } + return false, "" +} + +func writeModeVariant(sb *strings.Builder, variant string, zh bool) { + switch strings.ToLower(strings.TrimSpace(variant)) { + case "aggressive": + if zh { + sb.WriteString("## 模式: 激进\n- 优先捕捉趋势突破, 置信度 ≥ 70 时可分批建仓\n- 允许更高仓位, 但必须严格止损并说明风险回报比\n\n") + } else { + sb.WriteString("## Mode: Aggressive\n- Prioritize capturing trend breakouts; may scale in when confidence ≥ 70\n- Allow larger positions, but must strictly set stop-loss and explain the risk-reward ratio\n\n") + } + case "conservative": + if zh { + sb.WriteString("## 模式: 保守\n- 只有当多重信号共振时才开仓\n- 优先保本, 连亏后必须暂停多个周期\n\n") + } else { + sb.WriteString("## Mode: Conservative\n- Open positions only when multiple signals resonate\n- Prioritize capital preservation; pause for multiple periods after consecutive losses\n\n") + } + case "scalping": + if zh { + sb.WriteString("## 模式: 短线\n- 关注短期动量, 利润目标较小但要求迅速行动\n- 价格两根 K 线内未按预期走 → 立即减仓或止损\n\n") + } else { + sb.WriteString("## Mode: Scalping\n- Focus on short-term momentum, smaller profit targets but require quick action\n- If price doesn't move as expected within two bars, immediately reduce position or stop-loss\n\n") + } + } +} + +func writeHardConstraints(sb *strings.Builder, accountEquity float64, riskControl store.RiskControlConfig, btcEthPosValueRatio, altcoinPosValueRatio float64, singleSymbol bool, primarySymbol string, zh bool) { + if zh { + sb.WriteString("# 风控硬约束\n\n") + sb.WriteString("## 代码强制 (后端校验, 无法绕过):\n") + sb.WriteString(fmt.Sprintf("- 最大持仓数: 同时 %d 个标的\n", riskControl.MaxPositions)) + } else { + sb.WriteString("# Hard Constraints (Risk Control)\n\n") + sb.WriteString("## CODE ENFORCED (backend validation, cannot be bypassed):\n") + sb.WriteString(fmt.Sprintf("- Max Positions: %d instruments simultaneously\n", riskControl.MaxPositions)) + } + + if singleSymbol { + // One symbol — pick the higher of the two configured ratios so the + // limit isn't accidentally clamped to the altcoin cap for a stock. + ratio := altcoinPosValueRatio + if btcEthPosValueRatio > ratio { + ratio = btcEthPosValueRatio + } + maxVal := accountEquity * ratio + symLabel := primarySymbol + if zh { + sb.WriteString(fmt.Sprintf("- 单仓最大价值 (%s): %.0f USDT (= 权益 %.0f × %.1fx)\n", symLabel, maxVal, accountEquity, ratio)) + } else { + sb.WriteString(fmt.Sprintf("- Position Value Limit (%s): max %.0f USDT (= equity %.0f × %.1fx)\n", symLabel, maxVal, accountEquity, ratio)) + } + } else { + if zh { + sb.WriteString(fmt.Sprintf("- 单仓最大价值 (山寨币/股票): %.0f USDT (= 权益 %.0f × %.1fx)\n", accountEquity*altcoinPosValueRatio, accountEquity, altcoinPosValueRatio)) + sb.WriteString(fmt.Sprintf("- 单仓最大价值 (BTC/ETH): %.0f USDT (= 权益 %.0f × %.1fx)\n", accountEquity*btcEthPosValueRatio, accountEquity, btcEthPosValueRatio)) + } else { + sb.WriteString(fmt.Sprintf("- Position Value Limit (Altcoin/Stock): max %.0f USDT (= equity %.0f × %.1fx)\n", accountEquity*altcoinPosValueRatio, accountEquity, altcoinPosValueRatio)) + sb.WriteString(fmt.Sprintf("- Position Value Limit (BTC/ETH): max %.0f USDT (= equity %.0f × %.1fx)\n", accountEquity*btcEthPosValueRatio, accountEquity, btcEthPosValueRatio)) + } + } + + if zh { + sb.WriteString(fmt.Sprintf("- 最大保证金占用: ≤%.0f%%\n", riskControl.MaxMarginUsage*100)) + sb.WriteString(fmt.Sprintf("- 最小下单金额: ≥%.0f USDT\n\n", riskControl.MinPositionSize)) + sb.WriteString("## AI 建议 (推荐遵循):\n") + } else { + sb.WriteString(fmt.Sprintf("- Max Margin Usage: ≤%.0f%%\n", riskControl.MaxMarginUsage*100)) + sb.WriteString(fmt.Sprintf("- Min Position Size: ≥%.0f USDT\n\n", riskControl.MinPositionSize)) + sb.WriteString("## AI GUIDED (recommended):\n") + } + + if singleSymbol { + lev := riskControl.AltcoinMaxLeverage + if riskControl.BTCETHMaxLeverage > lev { + lev = riskControl.BTCETHMaxLeverage + } + if zh { + sb.WriteString(fmt.Sprintf("- 交易杠杆 (%s): 最高 %dx\n", primarySymbol, lev)) + } else { + sb.WriteString(fmt.Sprintf("- Trading Leverage (%s): max %dx\n", primarySymbol, lev)) + } + } else { + if zh { + sb.WriteString(fmt.Sprintf("- 交易杠杆: 山寨币/股票 最高 %dx | BTC/ETH 最高 %dx\n", riskControl.AltcoinMaxLeverage, riskControl.BTCETHMaxLeverage)) + } else { + sb.WriteString(fmt.Sprintf("- Trading Leverage: Altcoin/Stock max %dx | BTC/ETH max %dx\n", riskControl.AltcoinMaxLeverage, riskControl.BTCETHMaxLeverage)) + } + } + if zh { + sb.WriteString(fmt.Sprintf("- 风险回报比: ≥1:%.1f (take_profit / stop_loss)\n", riskControl.MinRiskRewardRatio)) + sb.WriteString(fmt.Sprintf("- 最小置信度: ≥%d 才开仓\n\n", riskControl.MinConfidence)) + } else { + sb.WriteString(fmt.Sprintf("- Risk-Reward Ratio: ≥1:%.1f (take_profit / stop_loss)\n", riskControl.MinRiskRewardRatio)) + sb.WriteString(fmt.Sprintf("- Min Confidence: ≥%d to open position\n\n", riskControl.MinConfidence)) + } + + // Position sizing guidance + exampleRatio := btcEthPosValueRatio + if singleSymbol { + exampleRatio = altcoinPosValueRatio + if btcEthPosValueRatio > exampleRatio { + exampleRatio = btcEthPosValueRatio + } + } + if zh { + sb.WriteString("## 仓位大小指引\n") + sb.WriteString("根据置信度和上面的单仓最大价值算出 `position_size_usd`:\n") + sb.WriteString("- 高置信 (≥85): 用最大价值的 80-100%%\n") + sb.WriteString("- 中置信 (70-84): 用最大价值的 50-80%%\n") + sb.WriteString("- 低置信 (60-69): 用最大价值的 30-50%%\n") + sb.WriteString(fmt.Sprintf("- 示例: 权益 %.0f × %.1fx = 最大 %.0f USDT\n", accountEquity, exampleRatio, accountEquity*exampleRatio)) + sb.WriteString("- **不要**直接拿 available_balance 当 position_size_usd, 用上面的单仓最大价值!\n\n") + } else { + sb.WriteString("## Position Sizing Guidance\n") + sb.WriteString("Calculate `position_size_usd` from your confidence and the Position Value Limits above:\n") + sb.WriteString("- High confidence (≥85): use 80-100%% of the position value limit\n") + sb.WriteString("- Medium confidence (70-84): use 50-80%% of the position value limit\n") + sb.WriteString("- Low confidence (60-69): use 30-50%% of the position value limit\n") + sb.WriteString(fmt.Sprintf("- Example: equity %.0f × %.1fx = max %.0f USDT\n", accountEquity, exampleRatio, accountEquity*exampleRatio)) + sb.WriteString("- **DO NOT** just use available_balance as position_size_usd. Use the Position Value Limit!\n\n") + } +} + +func writeOutputFormat(sb *strings.Builder, accountEquity, btcEthPosValueRatio float64, riskControl store.RiskControlConfig, singleSymbol bool, primarySymbol string, zh bool) { + // Output format schema MUST stay English/structural; parser depends on it. + sb.WriteString("# Output Format (Strictly Follow)\n\n") + if zh { + sb.WriteString("**必须使用 XML 标签 分隔思维链和决策 JSON, 避免解析错误**\n\n") + } else { + sb.WriteString("**Must use XML tags and to separate chain of thought and decision JSON, avoiding parsing errors**\n\n") + } + sb.WriteString("## Format Requirements\n\n") + sb.WriteString("\n") + if zh { + sb.WriteString("你的思维链分析...\n- 简明分析你的思考过程\n") + } else { + sb.WriteString("Your chain of thought analysis...\n- Briefly analyze your thinking process\n") + } + sb.WriteString("\n\n") + sb.WriteString("\n") + if zh { + sb.WriteString("步骤 2: JSON 决策数组\n\n") + } else { + sb.WriteString("Step 2: JSON decision array\n\n") + } + sb.WriteString("```json\n[\n") + + // Build a JSON example using the actual trading symbol when the strategy + // is single-symbol. Falls back to the legacy BTC/ETH two-line example + // only for multi-symbol strategies that genuinely have BTC/ETH on tap. + if singleSymbol { + lev := riskControl.AltcoinMaxLeverage + if riskControl.BTCETHMaxLeverage > lev { + lev = riskControl.BTCETHMaxLeverage + } + ratio := btcEthPosValueRatio // already chosen as the larger above when single-symbol + size := accountEquity * ratio + sb.WriteString(fmt.Sprintf(" {\"symbol\": \"%s\", \"action\": \"open_long\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 0, \"take_profit\": 0, \"confidence\": 85, \"risk_usd\": 0},\n", primarySymbol, lev, size)) + sb.WriteString(fmt.Sprintf(" {\"symbol\": \"%s\", \"action\": \"wait\"}\n", primarySymbol)) + } else { + examplePositionSize := accountEquity * btcEthPosValueRatio + sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300},\n", + riskControl.BTCETHMaxLeverage, examplePositionSize)) + sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\"}\n") + } + sb.WriteString("]\n```\n") + sb.WriteString("\n\n") + + if zh { + sb.WriteString("## 字段说明\n\n") + sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n") + sb.WriteString(fmt.Sprintf("- `confidence`: 0-100 (开仓建议 ≥ %d)\n", riskControl.MinConfidence)) + sb.WriteString("- 开仓时必填: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n") + sb.WriteString("- **重要**: 所有数值必须是算好的数字, 不能是公式/表达式 (例如写 `27.76`, 不要写 `3000 * 0.01`)\n") + if singleSymbol { + sb.WriteString(fmt.Sprintf("- **本策略只交易 %s**, JSON 中的 `symbol` 必须**完全等于** `%s`, 不要写成 `%s` 去掉后缀或加 USDT 的变体。\n", primarySymbol, primarySymbol, primarySymbol)) + } + sb.WriteString("\n") + } else { + sb.WriteString("## Field Description\n\n") + sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n") + sb.WriteString(fmt.Sprintf("- `confidence`: 0-100 (opening recommended ≥ %d)\n", riskControl.MinConfidence)) + sb.WriteString("- Required when opening: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n") + sb.WriteString("- **IMPORTANT**: all numeric values must be calculated numbers, NOT formulas/expressions (e.g. use `27.76`, not `3000 * 0.01`)\n") + if singleSymbol { + sb.WriteString(fmt.Sprintf("- **This strategy trades only %s.** The JSON `symbol` MUST match `%s` exactly — do not add USDT/USDC suffix variants.\n", primarySymbol, primarySymbol)) + } + sb.WriteString("\n") + } +} + +func (e *StrategyEngine) writeAvailableIndicators(sb *strings.Builder, zh bool) { indicators := e.config.Indicators kline := indicators.Klines - sb.WriteString(fmt.Sprintf("- %s price series", kline.PrimaryTimeframe)) - if kline.EnableMultiTimeframe { - sb.WriteString(fmt.Sprintf(" + %s K-line series\n", kline.LongerTimeframe)) + label := func(en, zhStr string) string { + if zh { + return zhStr + } + return en + } + + if zh { + sb.WriteString(fmt.Sprintf("- %s 价格序列", kline.PrimaryTimeframe)) + if kline.EnableMultiTimeframe { + sb.WriteString(fmt.Sprintf(" + %s K 线序列\n", kline.LongerTimeframe)) + } else { + sb.WriteString("\n") + } } else { - sb.WriteString("\n") + sb.WriteString(fmt.Sprintf("- %s price series", kline.PrimaryTimeframe)) + if kline.EnableMultiTimeframe { + sb.WriteString(fmt.Sprintf(" + %s K-line series\n", kline.LongerTimeframe)) + } else { + sb.WriteString("\n") + } } if indicators.EnableEMA { - sb.WriteString("- EMA indicators") + sb.WriteString("- " + label("EMA indicators", "EMA 指标")) if len(indicators.EMAPeriods) > 0 { - sb.WriteString(fmt.Sprintf(" (periods: %v)", indicators.EMAPeriods)) + sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.EMAPeriods)) } sb.WriteString("\n") } - if indicators.EnableMACD { - sb.WriteString("- MACD indicators\n") + sb.WriteString("- " + label("MACD indicators", "MACD 指标") + "\n") } - if indicators.EnableRSI { - sb.WriteString("- RSI indicators") + sb.WriteString("- " + label("RSI indicators", "RSI 指标")) if len(indicators.RSIPeriods) > 0 { - sb.WriteString(fmt.Sprintf(" (periods: %v)", indicators.RSIPeriods)) + sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.RSIPeriods)) } sb.WriteString("\n") } - if indicators.EnableATR { - sb.WriteString("- ATR indicators") + sb.WriteString("- " + label("ATR indicators", "ATR 指标")) if len(indicators.ATRPeriods) > 0 { - sb.WriteString(fmt.Sprintf(" (periods: %v)", indicators.ATRPeriods)) + sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.ATRPeriods)) } sb.WriteString("\n") } - if indicators.EnableBOLL { - sb.WriteString("- Bollinger Bands (BOLL) - Upper/Middle/Lower bands") + sb.WriteString("- " + label("Bollinger Bands (BOLL) - Upper/Middle/Lower bands", "布林带 (BOLL) - 上/中/下轨")) if len(indicators.BOLLPeriods) > 0 { - sb.WriteString(fmt.Sprintf(" (periods: %v)", indicators.BOLLPeriods)) + sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.BOLLPeriods)) } sb.WriteString("\n") } - if indicators.EnableVolume { - sb.WriteString("- Volume data\n") + sb.WriteString("- " + label("Volume data", "成交量数据") + "\n") } - if indicators.EnableOI { - sb.WriteString("- Open Interest (OI) data\n") + sb.WriteString("- " + label("Open Interest (OI) data", "持仓量 (OI) 数据") + "\n") } - if indicators.EnableFundingRate { - sb.WriteString("- Funding rate\n") + sb.WriteString("- " + label("Funding rate", "资金费率") + "\n") } - if len(e.config.CoinSource.StaticCoins) > 0 || e.config.CoinSource.UseAI500 || e.config.CoinSource.UseOITop { - sb.WriteString("- AI500 / OI_Top filter tags (if available)\n") + sb.WriteString("- " + label("AI500 / OI_Top filter tags (if available)", "AI500 / OI_Top 过滤标记 (如有)") + "\n") } - if indicators.EnableQuantData { - sb.WriteString("- Quantitative data (institutional/retail fund flow, position changes, multi-period price changes)\n") + sb.WriteString("- " + label("Quantitative data (institutional/retail fund flow, position changes, multi-period price changes)", "量化数据 (机构/散户资金流, 持仓变化, 多周期价格变动)") + "\n") } } diff --git a/trader/auto_trader_risk.go b/trader/auto_trader_risk.go index 937b1839..d73f81ad 100644 --- a/trader/auto_trader_risk.go +++ b/trader/auto_trader_risk.go @@ -3,6 +3,7 @@ package trader import ( "fmt" "nofx/logger" + "nofx/market" "strings" "time" ) @@ -183,6 +184,18 @@ func isBTCETH(symbol string) bool { return strings.HasPrefix(symbol, "BTC") || strings.HasPrefix(symbol, "ETH") } +// isMajorAsset returns true for assets that should use the BTC/ETH higher +// position-value tier rather than the altcoin (1x equity) tier. This covers +// BTC/ETH crypto perps AND Hyperliquid XYZ assets (US equities, commodities, +// forex) — none of which are "altcoins" and all of which deserve the higher +// per-position cap so the AI can actually take meaningful positions. +func isMajorAsset(symbol string) bool { + if isBTCETH(symbol) { + return true + } + return market.IsXyzDexAsset(symbol) +} + // enforcePositionValueRatio checks and enforces position value ratio limits (CODE ENFORCED) // Returns the adjusted position size (capped if necessary) and whether the position was capped // positionSizeUSD: the original position size in USD @@ -195,12 +208,14 @@ func (at *AutoTrader) enforcePositionValueRatio(positionSizeUSD float64, equity riskControl := at.config.StrategyConfig.RiskControl - // Get the appropriate position value ratio limit + // Get the appropriate position value ratio limit. BTC/ETH AND Hyperliquid + // XYZ assets (US stocks etc.) use the higher tier; pure altcoins use the + // lower tier. var maxPositionValueRatio float64 - if isBTCETH(symbol) { + if isMajorAsset(symbol) { maxPositionValueRatio = riskControl.BTCETHMaxPositionValueRatio if maxPositionValueRatio <= 0 { - maxPositionValueRatio = 5.0 // Default: 5x for BTC/ETH + maxPositionValueRatio = 5.0 // Default: 5x for BTC/ETH and XYZ assets } } else { maxPositionValueRatio = riskControl.AltcoinMaxPositionValueRatio