Files
nofx/kernel/engine_prompt.go
2026-06-27 00:37:59 +08:00

1290 lines
58 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package kernel
import (
"fmt"
"nofx/market"
"nofx/provider/nofxos"
"nofx/provider/vergex"
"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
// System prompts are intentionally English-only. UI copy can be localized,
// but the model contract should stay language-stable for an international
// open-source project and for reproducible trading behavior.
lang := LangEnglish
zh := false
singleSymbol, primarySymbol := e.singleSymbolInfo()
if e.usesVergexSignalPrompt() {
return e.buildVergexSystemPrompt(accountEquity, variant, lang, zh, singleSymbol, primarySymbol)
}
// 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).
roleDefinition := englishOnlyPromptSection(promptSections.RoleDefinition)
if roleDefinition != "" {
sb.WriteString(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)
tradingFrequency := englishOnlyPromptSection(promptSections.TradingFrequency)
if tradingFrequency != "" {
sb.WriteString(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("- 单笔持仓时长 ≥ 45-90 分钟\n")
sb.WriteString("如果你发现自己每个周期都在交易 → 入场标准过低; 如果不到 45 分钟就平仓 → 太冲动。\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 ≥ 45-90 minutes\n")
sb.WriteString("If you find yourself trading every cycle → standards too low; if closing positions < 45 minutes → too impulsive.\n\n")
}
// 5. Entry standards (editable)
entryStandards := englishOnlyPromptSection(promptSections.EntryStandards)
if entryStandards != "" {
sb.WriteString(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)
decisionProcess := englishOnlyPromptSection(promptSections.DecisionProcess)
if decisionProcess != "" {
sb.WriteString(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 := englishOnlyPromptSection(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()
}
func (e *StrategyEngine) usesVergexSignalPrompt() bool {
if e == nil || e.config == nil {
return false
}
coinSource := e.config.CoinSource
sourceType := strings.ToLower(strings.TrimSpace(coinSource.SourceType))
return sourceType == "vergex_signal" ||
sourceType == "claw402" ||
sourceType == "claw402_vergex" ||
coinSource.VergexMarketType != "" ||
coinSource.VergexChain != "" ||
coinSource.VergexLimit > 0
}
func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant string, lang Language, zh bool, singleSymbol bool, primarySymbol string) string {
var sb strings.Builder
riskControl := e.config.RiskControl
writeVergexSchemaPrompt(&sb, zh)
sb.WriteString("\n\n---\n\n")
if zh {
sb.WriteString("# 你是 NOFX Claw402 自动交易员\n\n")
sb.WriteString("你的任务是交易 Claw402.ai/Vergex 本轮榜单返回的 Hyperliquid 可交易标的。只允许交易本轮候选标的和已有持仓,不要自行发明代码或切换到榜单外标的。\n\n")
sb.WriteString("# 决策数据优先级\n\n")
sb.WriteString("1. Claw402.ai Signal Ranking: 决定本轮候选池、排名、方向和类别。\n")
sb.WriteString("2. Claw402.ai Signal Lab: 用于确认趋势、动量、事件或模型信号;这是开仓前的核心确认数据。\n")
sb.WriteString("3. Claw402.ai Cost/Liquidation Heatmap: 用于识别清算密集区、成本区、止损位置和止盈目标。\n")
sb.WriteString("4. 原始 OHLCV K 线: 用于验证入场时机、趋势结构、波动和风险回报。\n\n")
sb.WriteString("# 交易原则\n\n")
sb.WriteString("- 先管理已有持仓,再考虑新开仓。\n")
sb.WriteString("- 开仓需要 Signal Lab、热力图和 K 线方向大体一致;任一关键数据缺失或互相冲突时,默认等待。\n")
sb.WriteString("- 不要把 Claw402 排名当作唯一买入理由;排名只是候选池,开仓必须经过详情数据和 K 线确认。\n")
sb.WriteString("- 本轮 Candidate Coins 中的标的都是允许交易的候选;如果某个标的详情缺失,只能降低置信度或等待,不能说它不属于可交易范围。\n")
sb.WriteString("- 如果 Signal Lab 或热力图没有出现在该标的的 Vergex Claw402 Signals 里,必须在 reasoning 中说明缺失;如果已经出现,则不能声称该标的缺少该数据。\n")
sb.WriteString("- 防止频繁开平仓:非止损或强止盈情况下,开仓后至少持有 45 分钟;小亏小赚的噪音区优先持有到 90 分钟;平仓后同一标的 90 分钟内不重新进场;每小时最多 1 次新开仓。\n")
sb.WriteString("- 止损必须放在无效点之外;止盈优先放在热力图阻力/清算区域或满足风险回报的位置。\n\n")
} else {
sb.WriteString("# You are the NOFX Claw402 auto-trader\n\n")
sb.WriteString("Trade only Hyperliquid instruments returned by this cycle's Claw402.ai/Vergex board. You may trade only the current candidate symbols and existing positions; never invent tickers or rotate outside the provided universe.\n\n")
sb.WriteString("# Decision Data Priority\n\n")
sb.WriteString("1. Claw402.ai Signal Ranking: candidate pool, rank, direction and category.\n")
sb.WriteString("2. Claw402.ai Signal Lab: trend, momentum, event/model confirmation; this is the core pre-entry confirmation source.\n")
sb.WriteString("3. Claw402.ai Cost/Liquidation Heatmap: crowded liquidation/cost zones, stop placement and target zones.\n")
sb.WriteString("4. Raw OHLCV candles: entry timing, trend structure, volatility and risk/reward validation.\n\n")
sb.WriteString("# Trading Rules\n\n")
sb.WriteString("- Manage existing positions before opening new ones.\n")
sb.WriteString("- Open only when Signal Lab, heatmap and raw candles broadly agree; wait when key data is missing or contradictory.\n")
sb.WriteString("- Ranking alone is not an entry reason; it only defines the candidate pool.\n")
sb.WriteString("- Every symbol in Candidate Coins is part of the allowed trading universe; missing detail can lower confidence or trigger waiting, but does not make the symbol non-tradable.\n")
sb.WriteString("- If Signal Lab or heatmap is absent from that symbol's Vergex Claw402 Signals, state it in reasoning; if it is present, never claim the symbol lacks that data.\n")
sb.WriteString("- Avoid churn: unless stopping out or taking a strong profit, hold new positions for at least 45 minutes; avoid flat/noise closes until roughly 90 minutes; after closing a symbol, wait 90 minutes before re-entry; open at most 1 new position per hour.\n")
sb.WriteString("- Stops must sit beyond invalidation; targets should prefer heatmap resistance/liquidation zones or valid risk/reward levels.\n\n")
}
writeModeVariant(&sb, variant, zh)
altcoinPosValueRatio := riskControl.AltcoinMaxPositionValueRatio
if altcoinPosValueRatio <= 0 {
altcoinPosValueRatio = 1.0
}
writeVergexHardConstraints(&sb, accountEquity, riskControl, altcoinPosValueRatio, zh)
writeVergexOutputFormat(&sb, accountEquity, riskControl, altcoinPosValueRatio, singleSymbol, primarySymbol, zh)
customPrompt := englishOnlyPromptSection(e.config.CustomPrompt)
if customPrompt != "" {
sb.WriteString("# User Preference\n\n")
sb.WriteString(customPrompt)
sb.WriteString("\n\n")
}
return sb.String()
}
func englishOnlyPromptSection(section string) string {
trimmed := strings.TrimSpace(section)
if trimmed == "" {
return ""
}
if detectLanguage(trimmed) == LangChinese {
return ""
}
return trimmed
}
func writeVergexSchemaPrompt(sb *strings.Builder, zh bool) {
if zh {
sb.WriteString("# Claw402.ai TradeFi 数据说明\n\n")
sb.WriteString("- Equity: 账户总权益,包含浮动盈亏,单位 USDT。\n")
sb.WriteString("- Balance: 可用余额,用于判断还能否开新仓,单位 USDT。\n")
sb.WriteString("- Margin: 当前保证金使用率,越高风险越大。\n")
sb.WriteString("- Position: 当前持仓,包含方向、进场价、杠杆、未实现盈亏、强平价。\n")
sb.WriteString("- Claw402 Ranking: 本轮可交易候选池、排名、方向和类别。\n")
sb.WriteString("- Signal Lab: Claw402 对单个标的的深度信号,用于确认趋势和质量。\n")
sb.WriteString("- Cost/Liquidation Heatmap: 成本区与清算密集区,用于止损、止盈和拥挤风险判断。\n")
sb.WriteString("- Raw OHLCV Kline: 原始 K 线,用于确认趋势结构、入场位置和风险回报。\n")
} else {
sb.WriteString("# Claw402.ai TradeFi Data Guide\n\n")
sb.WriteString("- Equity: total account value including unrealized PnL, in USDT.\n")
sb.WriteString("- Balance: available balance for new positions, in USDT.\n")
sb.WriteString("- Margin: current margin usage; higher means more risk.\n")
sb.WriteString("- Position: current holdings with side, entry, leverage, unrealized PnL and liquidation price.\n")
sb.WriteString("- Claw402 Ranking: tradable candidate pool, rank, direction and category for this cycle.\n")
sb.WriteString("- Signal Lab: per-symbol Claw402 deep signal used to confirm trend and quality.\n")
sb.WriteString("- Cost/Liquidation Heatmap: cost and liquidation clusters used for stops, targets and crowding risk.\n")
sb.WriteString("- Raw OHLCV Kline: raw candles used for trend structure, entry timing and risk/reward.\n")
}
}
func writeVergexHardConstraints(sb *strings.Builder, accountEquity float64, riskControl store.RiskControlConfig, tradeFiPositionValueRatio float64, zh bool) {
maxPositionValue := accountEquity * tradeFiPositionValueRatio
if zh {
sb.WriteString("# 风控硬约束\n\n")
sb.WriteString("## 后端强制\n")
sb.WriteString(fmt.Sprintf("- 最大持仓数: 同时 %d 个 Claw402 候选标的\n", riskControl.MaxPositions))
sb.WriteString(fmt.Sprintf("- 单仓最大名义价值: %.0f USDT (= 权益 %.0f × %.1fx)\n", maxPositionValue, accountEquity, tradeFiPositionValueRatio))
sb.WriteString(fmt.Sprintf("- 最大保证金占用: ≤%.0f%%\n", riskControl.MaxMarginUsage*100))
sb.WriteString(fmt.Sprintf("- 最小下单金额: ≥%.0f USDT\n\n", riskControl.MinPositionSize))
sb.WriteString("## AI 建议\n")
sb.WriteString(fmt.Sprintf("- 交易杠杆: Claw402 候选标的最高 %dx\n", riskControl.AltcoinMaxLeverage))
sb.WriteString(fmt.Sprintf("- 风险回报比: ≥1:%.1f\n", riskControl.MinRiskRewardRatio))
sb.WriteString(fmt.Sprintf("- 最小置信度: ≥%d 才能开仓\n\n", riskControl.MinConfidence))
sb.WriteString("# 仓位大小\n\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("- 不要直接把 available_balance 当作 position_size_usd。\n\n")
} else {
sb.WriteString("# Hard Risk Constraints\n\n")
sb.WriteString("## Backend enforced\n")
sb.WriteString(fmt.Sprintf("- Max positions: %d Claw402 candidate instruments at the same time\n", riskControl.MaxPositions))
sb.WriteString(fmt.Sprintf("- Max notional per position: %.0f USDT (= equity %.0f × %.1fx)\n", maxPositionValue, accountEquity, tradeFiPositionValueRatio))
sb.WriteString(fmt.Sprintf("- Max margin usage: ≤%.0f%%\n", riskControl.MaxMarginUsage*100))
sb.WriteString(fmt.Sprintf("- Min order size: ≥%.0f USDT\n\n", riskControl.MinPositionSize))
sb.WriteString("## AI guided\n")
sb.WriteString(fmt.Sprintf("- Leverage: every open position must use exactly %dx\n", riskControl.AltcoinMaxLeverage))
sb.WriteString(fmt.Sprintf("- Risk/reward: ≥1:%.1f\n", riskControl.MinRiskRewardRatio))
sb.WriteString(fmt.Sprintf("- Min confidence to open: ≥%d\n\n", riskControl.MinConfidence))
sb.WriteString("# Position Sizing\n\n")
sb.WriteString("For every `open_long` or `open_short`, use the full max notional per position.\n")
sb.WriteString("- Do not scale position_size_usd down by confidence.\n")
sb.WriteString("- Do not open small probe positions.\n")
sb.WriteString("- If the setup is not strong enough for full size, output `wait`.\n")
sb.WriteString("- Do not use available_balance directly as position_size_usd.\n\n")
}
}
func writeVergexOutputFormat(sb *strings.Builder, accountEquity float64, riskControl store.RiskControlConfig, tradeFiPositionValueRatio float64, singleSymbol bool, primarySymbol string, zh bool) {
exampleSymbol := "xyz:NVDA"
secondSymbol := "xyz:AAPL"
if singleSymbol && strings.TrimSpace(primarySymbol) != "" {
exampleSymbol = primarySymbol
secondSymbol = primarySymbol
}
positionSize := accountEquity * tradeFiPositionValueRatio
leverage := riskControl.AltcoinMaxLeverage
if leverage <= 0 {
leverage = 1
}
sb.WriteString("# Output Format (Strictly Follow)\n\n")
if zh {
sb.WriteString("必须使用 XML 标签 <reasoning> 和 <decision> 分隔简明分析和决策 JSON。\n\n")
sb.WriteString("方向必须由数据决定:上涨结构确认时可以 `open_long`,下跌结构确认时可以 `open_short`;不要默认只做多或只做空。\n\n")
} else {
sb.WriteString("Use XML tags <reasoning> and <decision> to separate concise analysis from the decision JSON.\n\n")
sb.WriteString("Direction must be data-driven: use `open_long` for confirmed upside structures and `open_short` for confirmed downside structures; never default to long-only or short-only behavior.\n\n")
}
sb.WriteString("<reasoning>\n")
if zh {
sb.WriteString("简明说明: Claw402 排名、Signal Lab、热力图、K 线是否一致;如果缺数据或冲突,说明为什么等待。\n")
} else {
sb.WriteString("Briefly state whether Claw402 ranking, Signal Lab, heatmap and candles agree; if data is missing or conflicting, explain why you wait.\n")
}
sb.WriteString("</reasoning>\n\n")
sb.WriteString("<decision>\n")
sb.WriteString("```json\n[\n")
if singleSymbol {
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"%s\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 0, \"take_profit\": 0, \"confidence\": 85, \"risk_usd\": 0}\n", exampleSymbol, leverage, positionSize))
} else {
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", exampleSymbol, leverage, positionSize))
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"%s\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 0, \"take_profit\": 0, \"confidence\": 85, \"risk_usd\": 0}\n", secondSymbol, leverage, positionSize))
}
sb.WriteString("]\n```\n")
sb.WriteString("</decision>\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("- 所有数值必须是算好的数字,不能写公式。\n")
if singleSymbol {
sb.WriteString(fmt.Sprintf("- 本策略只交易 `%s`JSON 的 symbol 必须完全等于它。\n", exampleSymbol))
} else {
sb.WriteString("- JSON 的 symbol 必须完全来自本轮候选标的或已有持仓;`xyz:` 标的保留前缀core crypto 标的不要添加 `xyz:` 或 `USDT` 后缀。\n")
}
sb.WriteString("\n")
} else {
sb.WriteString("## Field Requirements\n\n")
sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n")
sb.WriteString(fmt.Sprintf("- `confidence`: 0-100; recommended ≥ %d to open\n", riskControl.MinConfidence))
sb.WriteString("- Required when opening: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n")
sb.WriteString("- All numeric values must be calculated numbers, not formulas.\n")
if singleSymbol {
sb.WriteString(fmt.Sprintf("- This strategy trades only `%s`; JSON symbol must match it exactly.\n", exampleSymbol))
} else {
sb.WriteString("- JSON symbols must exactly match current candidates or existing positions; keep `xyz:` on XYZ instruments, and do not add `xyz:` or `USDT` to core crypto symbols.\n")
}
sb.WriteString("\n")
}
}
// buildXYZStockCustomPrompt returns the canonical English directional stock
// briefing the agent uses for single-symbol Hyperliquid USDC perpetuals on
// the XYZ board. 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: DIRECTIONAL, SIGNAL-DRIVEN. You may open long or short; never force a trade when Signal Lab, liquidation structure and candles disagree.\n\n")
sb.WriteString("## Flat-Account Rule\n")
sb.WriteString("If `Current Positions` is None / empty, evaluate both directions from scratch.\n")
sb.WriteString("- Use `open_long` only when upside continuation or bullish reversal is confirmed.\n")
sb.WriteString("- Use `open_short` only when downside continuation or bearish reversal is confirmed.\n")
sb.WriteString("- Use `wait` when neither side meets the minimum confidence and risk/reward threshold.\n")
sb.WriteString("- Do not raise confidence just to force an order; confidence must reflect the evidence.\n\n")
sb.WriteString("## Long Entry Conditions\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("## Short Entry Conditions\n")
sb.WriteString("- Breakdown below intraday support or value area with expanding volume.\n")
sb.WriteString("- Failed breakout, lower high, or bearish rejection at resistance.\n")
sb.WriteString("- Signal Lab / liquidation structure shows downside fuel, trapped longs, or weak support below.\n")
sb.WriteString("- Negative catalyst: earnings miss, guide down, sector weakness, macro headwind.\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("- Do not flip directly from long to short or short to long in the same cycle. Manage or close the open position first.\n\n")
sb.WriteString("## Position Management\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 45 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" || coinSource.SourceType == "vergex_signal") && 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 标签 <reasoning> 和 <decision> 分隔思维链和决策 JSON, 避免解析错误**\n\n")
} else {
sb.WriteString("**Must use XML tags <reasoning> and <decision> to separate chain of thought and decision JSON, avoiding parsing errors**\n\n")
}
sb.WriteString("## Format Requirements\n\n")
sb.WriteString("<reasoning>\n")
if zh {
sb.WriteString("你的思维链分析...\n- 简明分析你的思考过程\n")
} else {
sb.WriteString("Your chain of thought analysis...\n- Briefly analyze your thinking process\n")
}
sb.WriteString("</reasoning>\n\n")
sb.WriteString("<decision>\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("</decision>\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))
}
}
if ctx.VergexDataMap != nil {
if vergexData, hasVergex := ctx.VergexDataMap[coin.Symbol]; hasVergex {
sb.WriteString(e.formatVergexData(vergexData))
}
}
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 briefly and output the decision 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))
}
}
if ctx.VergexDataMap != nil {
if vergexData, hasVergex := ctx.VergexDataMap[pos.Symbol]; hasVergex {
sb.WriteString(e.formatVergexData(vergexData))
}
}
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)"
case "vergex_signal":
return " (Vergex Signal)"
}
if strings.HasPrefix(sources[0], "hyper_rank") {
return " (Hyperliquid Dynamic Rank)"
}
}
return ""
}
func (e *StrategyEngine) formatVergexData(data *vergex.MarketAnalysis) string {
if data == nil {
return ""
}
var sb strings.Builder
sb.WriteString("\nVergex Claw402 Signals:\n")
sb.WriteString(vergex.FormatAnalysisForAI(data))
return sb.String()
}
// ============================================================================
// 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, ", ") + "]"
}