package kernel import ( "fmt" "nofx/market" "nofx/provider/nofxos" "nofx/store" "strings" "time" ) // ============================================================================ // Prompt Building - System Prompt // ============================================================================ // BuildSystemPrompt builds System Prompt according to strategy configuration func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string) 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) sb.WriteString(GetSchemaPrompt(lang)) sb.WriteString("\n\n") sb.WriteString("---\n\n") // 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 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 writeModeVariant(&sb, variant, zh) // 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 } altcoinPosValueRatio := riskControl.AltcoinMaxPositionValueRatio if altcoinPosValueRatio <= 0 { altcoinPosValueRatio = 1.0 } 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("- Single position hold time ≥ 30-60 minutes\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) 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, 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 → 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 — 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. // // 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") 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() } // 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 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(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("- " + label("EMA indicators", "EMA 指标")) if len(indicators.EMAPeriods) > 0 { sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.EMAPeriods)) } sb.WriteString("\n") } if indicators.EnableMACD { sb.WriteString("- " + label("MACD indicators", "MACD 指标") + "\n") } if indicators.EnableRSI { sb.WriteString("- " + label("RSI indicators", "RSI 指标")) if len(indicators.RSIPeriods) > 0 { sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.RSIPeriods)) } sb.WriteString("\n") } if indicators.EnableATR { sb.WriteString("- " + label("ATR indicators", "ATR 指标")) if len(indicators.ATRPeriods) > 0 { sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.ATRPeriods)) } sb.WriteString("\n") } if indicators.EnableBOLL { sb.WriteString("- " + label("Bollinger Bands (BOLL) - Upper/Middle/Lower bands", "布林带 (BOLL) - 上/中/下轨")) if len(indicators.BOLLPeriods) > 0 { sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.BOLLPeriods)) } sb.WriteString("\n") } if indicators.EnableVolume { sb.WriteString("- " + label("Volume data", "成交量数据") + "\n") } if indicators.EnableOI { sb.WriteString("- " + label("Open Interest (OI) data", "持仓量 (OI) 数据") + "\n") } if indicators.EnableFundingRate { sb.WriteString("- " + label("Funding rate", "资金费率") + "\n") } if len(e.config.CoinSource.StaticCoins) > 0 || e.config.CoinSource.UseAI500 || e.config.CoinSource.UseOITop { sb.WriteString("- " + label("AI500 / OI_Top filter tags (if available)", "AI500 / OI_Top 过滤标记 (如有)") + "\n") } if indicators.EnableQuantData { sb.WriteString("- " + label("Quantitative data (institutional/retail fund flow, position changes, multi-period price changes)", "量化数据 (机构/散户资金流, 持仓变化, 多周期价格变动)") + "\n") } } // ============================================================================ // Prompt Building - User Prompt // ============================================================================ // BuildUserPrompt builds User Prompt based on strategy configuration func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string { var sb strings.Builder // System status sb.WriteString(fmt.Sprintf("Time: %s | Period: #%d | Runtime: %d minutes\n\n", ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)) // BTC market if btcData, hasBTC := ctx.MarketDataMap["BTCUSDT"]; hasBTC { sb.WriteString(fmt.Sprintf("BTC: %.2f (1h: %+.2f%%, 4h: %+.2f%%) | MACD: %.4f | RSI: %.2f\n\n", btcData.CurrentPrice, btcData.PriceChange1h, btcData.PriceChange4h, btcData.CurrentMACD, btcData.CurrentRSI7)) } // Account information sb.WriteString(fmt.Sprintf("Account: Equity %.2f | Balance %.2f (%.1f%%) | PnL %+.2f%% | Margin %.1f%% | Positions %d\n\n", ctx.Account.TotalEquity, ctx.Account.AvailableBalance, (ctx.Account.AvailableBalance/ctx.Account.TotalEquity)*100, ctx.Account.TotalPnLPct, ctx.Account.MarginUsedPct, ctx.Account.PositionCount)) // Recently completed orders (placed before positions to ensure visibility) if len(ctx.RecentOrders) > 0 { sb.WriteString("## Recent Completed Trades\n") for i, order := range ctx.RecentOrders { resultStr := "Profit" if order.RealizedPnL < 0 { resultStr = "Loss" } sb.WriteString(fmt.Sprintf("%d. %s %s | Entry %.4f Exit %.4f | %s: %+.2f USDT (%+.2f%%) | %s→%s (%s)\n", i+1, order.Symbol, order.Side, order.EntryPrice, order.ExitPrice, resultStr, order.RealizedPnL, order.PnLPct, order.EntryTime, order.ExitTime, order.HoldDuration)) } sb.WriteString("\n") } // Historical trading statistics (helps AI understand past performance) if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 { // Get language from strategy config lang := e.GetLanguage() // Win/Loss ratio var winLossRatio float64 if ctx.TradingStats.AvgLoss > 0 { winLossRatio = ctx.TradingStats.AvgWin / ctx.TradingStats.AvgLoss } if lang == LangChinese { sb.WriteString("## 历史交易统计\n") sb.WriteString(fmt.Sprintf("总交易: %d 笔 | 盈利因子: %.2f | 夏普比率: %.2f | 盈亏比: %.2f\n", ctx.TradingStats.TotalTrades, ctx.TradingStats.ProfitFactor, ctx.TradingStats.SharpeRatio, winLossRatio)) sb.WriteString(fmt.Sprintf("总盈亏: %+.2f USDT | 平均盈利: +%.2f | 平均亏损: -%.2f | 最大回撤: %.1f%%\n", ctx.TradingStats.TotalPnL, ctx.TradingStats.AvgWin, ctx.TradingStats.AvgLoss, ctx.TradingStats.MaxDrawdownPct)) // Performance hints based on profit factor, sharpe, and drawdown if ctx.TradingStats.ProfitFactor >= 1.5 && ctx.TradingStats.SharpeRatio >= 1 { sb.WriteString("表现: 良好 - 保持当前策略\n") } else if ctx.TradingStats.ProfitFactor < 1 { sb.WriteString("表现: 需改进 - 提高盈亏比,优化止盈止损\n") } else if ctx.TradingStats.MaxDrawdownPct > 30 { sb.WriteString("表现: 风险偏高 - 减少仓位,控制回撤\n") } else { sb.WriteString("表现: 正常 - 有优化空间\n") } } else { sb.WriteString("## Historical Trading Statistics\n") sb.WriteString(fmt.Sprintf("Total Trades: %d | Profit Factor: %.2f | Sharpe: %.2f | Win/Loss Ratio: %.2f\n", ctx.TradingStats.TotalTrades, ctx.TradingStats.ProfitFactor, ctx.TradingStats.SharpeRatio, winLossRatio)) sb.WriteString(fmt.Sprintf("Total PnL: %+.2f USDT | Avg Win: +%.2f | Avg Loss: -%.2f | Max Drawdown: %.1f%%\n", ctx.TradingStats.TotalPnL, ctx.TradingStats.AvgWin, ctx.TradingStats.AvgLoss, ctx.TradingStats.MaxDrawdownPct)) // Performance hints based on profit factor, sharpe, and drawdown if ctx.TradingStats.ProfitFactor >= 1.5 && ctx.TradingStats.SharpeRatio >= 1 { sb.WriteString("Performance: GOOD - maintain current strategy\n") } else if ctx.TradingStats.ProfitFactor < 1 { sb.WriteString("Performance: NEEDS IMPROVEMENT - improve win/loss ratio, optimize TP/SL\n") } else if ctx.TradingStats.MaxDrawdownPct > 30 { sb.WriteString("Performance: HIGH RISK - reduce position size, control drawdown\n") } else { sb.WriteString("Performance: NORMAL - room for optimization\n") } } sb.WriteString("\n") } // Position information if len(ctx.Positions) > 0 { sb.WriteString("## Current Positions\n") for i, pos := range ctx.Positions { sb.WriteString(e.formatPositionInfo(i+1, pos, ctx)) } } else { sb.WriteString("Current Positions: None\n\n") } // Candidate coins (exclude coins already in positions to avoid duplicate data) positionSymbols := make(map[string]bool) for _, pos := range ctx.Positions { // Normalize symbol to handle both "ETH" and "ETHUSDT" formats normalizedSymbol := market.Normalize(pos.Symbol) positionSymbols[normalizedSymbol] = true } sb.WriteString(fmt.Sprintf("## Candidate Coins (%d coins)\n\n", len(ctx.MarketDataMap))) displayedCount := 0 for _, coin := range ctx.CandidateCoins { // Skip if this coin is already a position (data already shown in positions section) normalizedCoinSymbol := market.Normalize(coin.Symbol) if positionSymbols[normalizedCoinSymbol] { continue } marketData, hasData := ctx.MarketDataMap[coin.Symbol] if !hasData { continue } displayedCount++ sourceTags := e.formatCoinSourceTag(coin.Sources) sb.WriteString(fmt.Sprintf("### %d. %s%s\n\n", displayedCount, coin.Symbol, sourceTags)) sb.WriteString(e.formatMarketData(marketData)) if ctx.QuantDataMap != nil { if quantData, hasQuant := ctx.QuantDataMap[coin.Symbol]; hasQuant { sb.WriteString(e.formatQuantData(quantData)) } } sb.WriteString("\n") } sb.WriteString("\n") // Get language for market data formatting nofxosLang := nofxos.LangEnglish if e.GetLanguage() == LangChinese { nofxosLang = nofxos.LangChinese } // OI Ranking data (market-wide open interest changes) if ctx.OIRankingData != nil { sb.WriteString(nofxos.FormatOIRankingForAI(ctx.OIRankingData, nofxosLang)) } // NetFlow Ranking data (market-wide fund flow) if ctx.NetFlowRankingData != nil { sb.WriteString(nofxos.FormatNetFlowRankingForAI(ctx.NetFlowRankingData, nofxosLang)) } // Price Ranking data (market-wide gainers/losers) if ctx.PriceRankingData != nil { sb.WriteString(nofxos.FormatPriceRankingForAI(ctx.PriceRankingData, nofxosLang)) } sb.WriteString("---\n\n") sb.WriteString("Now please analyze and output your decision (Chain of Thought + JSON)\n") return sb.String() } func (e *StrategyEngine) formatPositionInfo(index int, pos PositionInfo, ctx *Context) string { var sb strings.Builder holdingDuration := "" if pos.UpdateTime > 0 { durationMs := time.Now().UnixMilli() - pos.UpdateTime durationMin := durationMs / (1000 * 60) if durationMin < 60 { holdingDuration = fmt.Sprintf(" | Holding Duration %d min", durationMin) } else { durationHour := durationMin / 60 durationMinRemainder := durationMin % 60 holdingDuration = fmt.Sprintf(" | Holding Duration %dh %dm", durationHour, durationMinRemainder) } } positionValue := pos.Quantity * pos.MarkPrice if positionValue < 0 { positionValue = -positionValue } sb.WriteString(fmt.Sprintf("%d. %s %s | Entry %.4f Current %.4f | Qty %.4f | Position Value %.2f USDT | PnL%+.2f%% | PnL Amount%+.2f USDT | Peak PnL%.2f%% | Leverage %dx | Margin %.0f | Liq Price %.4f%s\n\n", index, pos.Symbol, strings.ToUpper(pos.Side), pos.EntryPrice, pos.MarkPrice, pos.Quantity, positionValue, pos.UnrealizedPnLPct, pos.UnrealizedPnL, pos.PeakPnLPct, pos.Leverage, pos.MarginUsed, pos.LiquidationPrice, holdingDuration)) if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok { sb.WriteString(e.formatMarketData(marketData)) if ctx.QuantDataMap != nil { if quantData, hasQuant := ctx.QuantDataMap[pos.Symbol]; hasQuant { sb.WriteString(e.formatQuantData(quantData)) } } sb.WriteString("\n") } return sb.String() } func (e *StrategyEngine) formatCoinSourceTag(sources []string) string { if len(sources) > 1 { // Multiple signal source combination hasAI500 := false hasOITop := false hasOILow := false hasHyperAll := false hasHyperMain := false for _, s := range sources { switch s { case "ai500": hasAI500 = true case "oi_top": hasOITop = true case "oi_low": hasOILow = true case "hyper_all": hasHyperAll = true case "hyper_main": hasHyperMain = true } } if hasAI500 && hasOITop { return " (AI500+OI_Top dual signal)" } if hasAI500 && hasOILow { return " (AI500+OI_Low dual signal)" } if hasOITop && hasOILow { return " (OI_Top+OI_Low)" } if hasHyperMain && hasAI500 { return " (HyperMain+AI500)" } if hasHyperAll || hasHyperMain { return " (Hyperliquid)" } return " (Multiple sources)" } else if len(sources) == 1 { switch sources[0] { case "ai500": return " (AI500)" case "oi_top": return " (OI_Top OI increase)" case "oi_low": return " (OI_Low OI decrease)" case "static": return " (Manual selection)" case "hyper_all": return " (Hyperliquid All)" case "hyper_main": return " (Hyperliquid Top20)" } } return "" } // ============================================================================ // Market Data Formatting // ============================================================================ func (e *StrategyEngine) formatMarketData(data *market.Data) string { var sb strings.Builder indicators := e.config.Indicators // Clearly label the coin symbol sb.WriteString(fmt.Sprintf("=== %s Market Data ===\n\n", data.Symbol)) sb.WriteString(fmt.Sprintf("current_price = %.4f", data.CurrentPrice)) if indicators.EnableEMA { sb.WriteString(fmt.Sprintf(", current_ema20 = %.3f", data.CurrentEMA20)) } if indicators.EnableMACD { sb.WriteString(fmt.Sprintf(", current_macd = %.3f", data.CurrentMACD)) } if indicators.EnableRSI { sb.WriteString(fmt.Sprintf(", current_rsi7 = %.3f", data.CurrentRSI7)) } sb.WriteString("\n\n") if indicators.EnableOI || indicators.EnableFundingRate { sb.WriteString(fmt.Sprintf("Additional data for %s:\n\n", data.Symbol)) if indicators.EnableOI && data.OpenInterest != nil { sb.WriteString(fmt.Sprintf("Open Interest: Latest: %.2f Average: %.2f\n\n", data.OpenInterest.Latest, data.OpenInterest.Average)) } if indicators.EnableFundingRate { sb.WriteString(fmt.Sprintf("Funding Rate: %.2e\n\n", data.FundingRate)) } } if len(data.TimeframeData) > 0 { timeframeOrder := []string{"1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w"} for _, tf := range timeframeOrder { if tfData, ok := data.TimeframeData[tf]; ok { sb.WriteString(fmt.Sprintf("=== %s Timeframe (oldest → latest) ===\n\n", strings.ToUpper(tf))) e.formatTimeframeSeriesData(&sb, tfData, indicators) } } } else { // Compatible with old data format if data.IntradaySeries != nil { klineConfig := indicators.Klines sb.WriteString(fmt.Sprintf("Intraday series (%s intervals, oldest → latest):\n\n", klineConfig.PrimaryTimeframe)) if len(data.IntradaySeries.MidPrices) > 0 { sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.IntradaySeries.MidPrices))) } if indicators.EnableEMA && len(data.IntradaySeries.EMA20Values) > 0 { sb.WriteString(fmt.Sprintf("EMA indicators (20-period): %s\n\n", formatFloatSlice(data.IntradaySeries.EMA20Values))) } if indicators.EnableMACD && len(data.IntradaySeries.MACDValues) > 0 { sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.IntradaySeries.MACDValues))) } if indicators.EnableRSI { if len(data.IntradaySeries.RSI7Values) > 0 { sb.WriteString(fmt.Sprintf("RSI indicators (7-Period): %s\n\n", formatFloatSlice(data.IntradaySeries.RSI7Values))) } if len(data.IntradaySeries.RSI14Values) > 0 { sb.WriteString(fmt.Sprintf("RSI indicators (14-Period): %s\n\n", formatFloatSlice(data.IntradaySeries.RSI14Values))) } } if indicators.EnableVolume && len(data.IntradaySeries.Volume) > 0 { sb.WriteString(fmt.Sprintf("Volume: %s\n\n", formatFloatSlice(data.IntradaySeries.Volume))) } if indicators.EnableATR { sb.WriteString(fmt.Sprintf("3m ATR (14-period): %.3f\n\n", data.IntradaySeries.ATR14)) } } if data.LongerTermContext != nil && indicators.Klines.EnableMultiTimeframe { sb.WriteString(fmt.Sprintf("Longer-term context (%s timeframe):\n\n", indicators.Klines.LongerTimeframe)) if indicators.EnableEMA { sb.WriteString(fmt.Sprintf("20-Period EMA: %.3f vs. 50-Period EMA: %.3f\n\n", data.LongerTermContext.EMA20, data.LongerTermContext.EMA50)) } if indicators.EnableATR { sb.WriteString(fmt.Sprintf("3-Period ATR: %.3f vs. 14-Period ATR: %.3f\n\n", data.LongerTermContext.ATR3, data.LongerTermContext.ATR14)) } if indicators.EnableVolume { sb.WriteString(fmt.Sprintf("Current Volume: %.3f vs. Average Volume: %.3f\n\n", data.LongerTermContext.CurrentVolume, data.LongerTermContext.AverageVolume)) } if indicators.EnableMACD && len(data.LongerTermContext.MACDValues) > 0 { sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.LongerTermContext.MACDValues))) } if indicators.EnableRSI && len(data.LongerTermContext.RSI14Values) > 0 { sb.WriteString(fmt.Sprintf("RSI indicators (14-Period): %s\n\n", formatFloatSlice(data.LongerTermContext.RSI14Values))) } } } return sb.String() } func (e *StrategyEngine) formatTimeframeSeriesData(sb *strings.Builder, data *market.TimeframeSeriesData, indicators store.IndicatorConfig) { if len(data.Klines) > 0 { sb.WriteString("Time(UTC) Open High Low Close Volume\n") for i, k := range data.Klines { t := time.Unix(k.Time/1000, 0).UTC() timeStr := t.Format("01-02 15:04") marker := "" if i == len(data.Klines)-1 { marker = " <- current" } sb.WriteString(fmt.Sprintf("%-14s %-9.4f %-9.4f %-9.4f %-9.4f %-12.2f%s\n", timeStr, k.Open, k.High, k.Low, k.Close, k.Volume, marker)) } sb.WriteString("\n") } else if len(data.MidPrices) > 0 { sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidPrices))) if indicators.EnableVolume && len(data.Volume) > 0 { sb.WriteString(fmt.Sprintf("Volume: %s\n\n", formatFloatSlice(data.Volume))) } } if indicators.EnableEMA { if len(data.EMA20Values) > 0 { sb.WriteString(fmt.Sprintf("EMA20: %s\n", formatFloatSlice(data.EMA20Values))) } if len(data.EMA50Values) > 0 { sb.WriteString(fmt.Sprintf("EMA50: %s\n", formatFloatSlice(data.EMA50Values))) } } if indicators.EnableMACD && len(data.MACDValues) > 0 { sb.WriteString(fmt.Sprintf("MACD: %s\n", formatFloatSlice(data.MACDValues))) } if indicators.EnableRSI { if len(data.RSI7Values) > 0 { sb.WriteString(fmt.Sprintf("RSI7: %s\n", formatFloatSlice(data.RSI7Values))) } if len(data.RSI14Values) > 0 { sb.WriteString(fmt.Sprintf("RSI14: %s\n", formatFloatSlice(data.RSI14Values))) } } if indicators.EnableATR && data.ATR14 > 0 { sb.WriteString(fmt.Sprintf("ATR14: %.4f\n", data.ATR14)) } if indicators.EnableBOLL && len(data.BOLLUpper) > 0 { sb.WriteString(fmt.Sprintf("BOLL Upper: %s\n", formatFloatSlice(data.BOLLUpper))) sb.WriteString(fmt.Sprintf("BOLL Middle: %s\n", formatFloatSlice(data.BOLLMiddle))) sb.WriteString(fmt.Sprintf("BOLL Lower: %s\n", formatFloatSlice(data.BOLLLower))) } sb.WriteString("\n") } func (e *StrategyEngine) formatQuantData(data *QuantData) string { if data == nil { return "" } indicators := e.config.Indicators if !indicators.EnableQuantOI && !indicators.EnableQuantNetflow { return "" } var sb strings.Builder sb.WriteString(fmt.Sprintf("📊 %s Quantitative Data:\n", data.Symbol)) if len(data.PriceChange) > 0 { sb.WriteString("Price Change: ") timeframes := []string{"5m", "15m", "1h", "4h", "12h", "24h"} parts := []string{} for _, tf := range timeframes { if v, ok := data.PriceChange[tf]; ok { parts = append(parts, fmt.Sprintf("%s: %+.4f%%", tf, v*100)) } } sb.WriteString(strings.Join(parts, " | ")) sb.WriteString("\n") } if indicators.EnableQuantNetflow && data.Netflow != nil { sb.WriteString("Fund Flow (Netflow):\n") timeframes := []string{"5m", "15m", "1h", "4h", "12h", "24h"} if data.Netflow.Institution != nil { if data.Netflow.Institution.Future != nil && len(data.Netflow.Institution.Future) > 0 { sb.WriteString(" Institutional Futures:\n") for _, tf := range timeframes { if v, ok := data.Netflow.Institution.Future[tf]; ok { sb.WriteString(fmt.Sprintf(" %s: %s\n", tf, formatFlowValue(v))) } } } if data.Netflow.Institution.Spot != nil && len(data.Netflow.Institution.Spot) > 0 { sb.WriteString(" Institutional Spot:\n") for _, tf := range timeframes { if v, ok := data.Netflow.Institution.Spot[tf]; ok { sb.WriteString(fmt.Sprintf(" %s: %s\n", tf, formatFlowValue(v))) } } } } if data.Netflow.Personal != nil { if data.Netflow.Personal.Future != nil && len(data.Netflow.Personal.Future) > 0 { sb.WriteString(" Retail Futures:\n") for _, tf := range timeframes { if v, ok := data.Netflow.Personal.Future[tf]; ok { sb.WriteString(fmt.Sprintf(" %s: %s\n", tf, formatFlowValue(v))) } } } if data.Netflow.Personal.Spot != nil && len(data.Netflow.Personal.Spot) > 0 { sb.WriteString(" Retail Spot:\n") for _, tf := range timeframes { if v, ok := data.Netflow.Personal.Spot[tf]; ok { sb.WriteString(fmt.Sprintf(" %s: %s\n", tf, formatFlowValue(v))) } } } } } if indicators.EnableQuantOI && len(data.OI) > 0 { for exchange, oiData := range data.OI { if len(oiData.Delta) > 0 { sb.WriteString(fmt.Sprintf("Open Interest (%s):\n", exchange)) for _, tf := range []string{"5m", "15m", "1h", "4h", "12h", "24h"} { if d, ok := oiData.Delta[tf]; ok { sb.WriteString(fmt.Sprintf(" %s: %+.4f%% (%s)\n", tf, d.OIDeltaPercent, formatFlowValue(d.OIDeltaValue))) } } } } } return sb.String() } func formatFlowValue(v float64) string { sign := "" if v >= 0 { sign = "+" } absV := v if absV < 0 { absV = -absV } if absV >= 1e9 { return fmt.Sprintf("%s%.2fB", sign, v/1e9) } else if absV >= 1e6 { return fmt.Sprintf("%s%.2fM", sign, v/1e6) } else if absV >= 1e3 { return fmt.Sprintf("%s%.2fK", sign, v/1e3) } return fmt.Sprintf("%s%.2f", sign, v) } func formatFloatSlice(values []float64) string { strValues := make([]string, len(values)) for i, v := range values { strValues[i] = fmt.Sprintf("%.4f", v) } return "[" + strings.Join(strValues, ", ") + "]" }