mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-29 17:11:22 +08:00
1290 lines
58 KiB
Go
1290 lines
58 KiB
Go
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, ", ") + "]"
|
||
}
|