Files
nofx/decision/strategy_engine.go
T
tinkle-community 5cff32e4f2 Feature/custom strategy (#1172)
* feat: add Strategy Studio with multi-timeframe support
- Add Strategy Studio page with three-column layout for strategy management
- Support multi-timeframe K-line data selection (5m, 15m, 1h, 4h, etc.)
- Add GetWithTimeframes() function in market package for fetching multiple timeframes
- Add TimeframeSeriesData struct for storing per-timeframe technical indicators
- Update formatMarketData() to display all selected timeframes in AI prompt
- Add strategy API endpoints for CRUD operations and test run
- Integrate real AI test runs with configured AI models
- Support custom AI500 and OI Top API URLs from strategy config
* docs: add Strategy Studio screenshot to README files
* fix: correct strategy-studio.png filename case in README
* refactor: remove legacy signal source config and simplify trader creation
- Remove signal source configuration from traders page (now handled by strategy)
- Remove advanced options (legacy config) from TraderConfigModal
- Rename default strategy to "默认山寨策略" with AI500 coin pool URL
- Delete SignalSourceModal and SignalSourceWarning components
- Clean up related stores, hooks, and page components
2025-12-06 07:20:11 +08:00

720 lines
23 KiB
Go
Raw 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 decision
import (
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/logger"
"nofx/market"
"nofx/pool"
"nofx/store"
"strings"
"time"
)
// StrategyEngine 策略执行引擎
// 负责基于策略配置动态获取数据和组装 Prompt
type StrategyEngine struct {
config *store.StrategyConfig
}
// NewStrategyEngine 创建策略执行引擎
func NewStrategyEngine(config *store.StrategyConfig) *StrategyEngine {
return &StrategyEngine{config: config}
}
// GetCandidateCoins 根据策略配置获取候选币种
func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
var candidates []CandidateCoin
symbolSources := make(map[string][]string)
coinSource := e.config.CoinSource
// 设置自定义的 API URL(如果配置了)
if coinSource.CoinPoolAPIURL != "" {
pool.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
logger.Infof("✓ 使用策略配置的 AI500 API URL: %s", coinSource.CoinPoolAPIURL)
}
if coinSource.OITopAPIURL != "" {
pool.SetOITopAPI(coinSource.OITopAPIURL)
logger.Infof("✓ 使用策略配置的 OI Top API URL: %s", coinSource.OITopAPIURL)
}
switch coinSource.SourceType {
case "static":
// 静态币种列表
for _, symbol := range coinSource.StaticCoins {
symbol = market.Normalize(symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"static"},
})
}
return candidates, nil
case "coinpool":
// 仅使用 AI500 币种池
return e.getCoinPoolCoins(coinSource.CoinPoolLimit)
case "oi_top":
// 仅使用 OI Top
return e.getOITopCoins(coinSource.OITopLimit)
case "mixed":
// 混合模式:AI500 + OI Top
if coinSource.UseCoinPool {
poolCoins, err := e.getCoinPoolCoins(coinSource.CoinPoolLimit)
if err != nil {
logger.Infof("⚠️ 获取 AI500 币种池失败: %v", err)
} else {
for _, coin := range poolCoins {
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "ai500")
}
}
}
if coinSource.UseOITop {
oiCoins, err := e.getOITopCoins(coinSource.OITopLimit)
if err != nil {
logger.Infof("⚠️ 获取 OI Top 失败: %v", err)
} else {
for _, coin := range oiCoins {
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "oi_top")
}
}
}
// 添加静态币种(如果有)
for _, symbol := range coinSource.StaticCoins {
symbol = market.Normalize(symbol)
if _, exists := symbolSources[symbol]; !exists {
symbolSources[symbol] = []string{"static"}
} else {
symbolSources[symbol] = append(symbolSources[symbol], "static")
}
}
// 转换为候选币种列表
for symbol, sources := range symbolSources {
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: sources,
})
}
return candidates, nil
default:
return nil, fmt.Errorf("未知的币种来源类型: %s", coinSource.SourceType)
}
}
// getCoinPoolCoins 获取 AI500 币种池
func (e *StrategyEngine) getCoinPoolCoins(limit int) ([]CandidateCoin, error) {
if limit <= 0 {
limit = 30
}
symbols, err := pool.GetTopRatedCoins(limit)
if err != nil {
return nil, err
}
var candidates []CandidateCoin
for _, symbol := range symbols {
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"ai500"},
})
}
return candidates, nil
}
// getOITopCoins 获取 OI Top 币种
func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
if limit <= 0 {
limit = 20
}
positions, err := pool.GetOITopPositions()
if err != nil {
return nil, err
}
var candidates []CandidateCoin
for i, pos := range positions {
if i >= limit {
break
}
symbol := market.Normalize(pos.Symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"oi_top"},
})
}
return candidates, nil
}
// FetchMarketData 根据策略配置获取市场数据
func (e *StrategyEngine) FetchMarketData(symbol string) (*market.Data, error) {
// 目前使用现有的 market.Get,后续可以根据策略配置自定义
return market.Get(symbol)
}
// FetchExternalData 获取外部数据源
func (e *StrategyEngine) FetchExternalData() (map[string]interface{}, error) {
externalData := make(map[string]interface{})
for _, source := range e.config.Indicators.ExternalDataSources {
data, err := e.fetchSingleExternalSource(source)
if err != nil {
logger.Infof("⚠️ 获取外部数据源 [%s] 失败: %v", source.Name, err)
continue
}
externalData[source.Name] = data
}
return externalData, nil
}
// fetchSingleExternalSource 获取单个外部数据源
func (e *StrategyEngine) fetchSingleExternalSource(source store.ExternalDataSource) (interface{}, error) {
client := &http.Client{
Timeout: time.Duration(source.RefreshSecs) * time.Second,
}
if client.Timeout == 0 {
client.Timeout = 30 * time.Second
}
req, err := http.NewRequest(source.Method, source.URL, nil)
if err != nil {
return nil, err
}
// 添加请求头
for k, v := range source.Headers {
req.Header.Set(k, v)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
// 如果指定了数据路径,提取指定路径的数据
if source.DataPath != "" {
result = extractJSONPath(result, source.DataPath)
}
return result, nil
}
// extractJSONPath 提取 JSON 路径数据(简单实现)
func extractJSONPath(data interface{}, path string) interface{} {
parts := strings.Split(path, ".")
current := data
for _, part := range parts {
if m, ok := current.(map[string]interface{}); ok {
current = m[part]
} else {
return nil
}
}
return current
}
// BuildUserPrompt 根据策略配置构建 User Prompt
func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
var sb strings.Builder
// 系统状态
sb.WriteString(fmt.Sprintf("时间: %s | 周期: #%d | 运行: %d分钟\n\n",
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes))
// BTC 市场(如果配置了)
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))
}
// 账户信息
sb.WriteString(fmt.Sprintf("账户: 净值%.2f | 余额%.2f (%.1f%%) | 盈亏%+.2f%% | 保证金%.1f%% | 持仓%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))
// 持仓信息
if len(ctx.Positions) > 0 {
sb.WriteString("## 当前持仓\n")
for i, pos := range ctx.Positions {
sb.WriteString(e.formatPositionInfo(i+1, pos, ctx))
}
} else {
sb.WriteString("当前持仓: 无\n\n")
}
// 交易统计
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
sb.WriteString("## 历史交易统计\n")
sb.WriteString(fmt.Sprintf("总交易数: %d | 胜率: %.1f%% | 盈亏比: %.2f | 夏普比: %.2f\n",
ctx.TradingStats.TotalTrades,
ctx.TradingStats.WinRate,
ctx.TradingStats.ProfitFactor,
ctx.TradingStats.SharpeRatio))
sb.WriteString(fmt.Sprintf("总盈亏: %.2f USDT | 平均盈利: %.2f | 平均亏损: %.2f | 最大回撤: %.1f%%\n\n",
ctx.TradingStats.TotalPnL,
ctx.TradingStats.AvgWin,
ctx.TradingStats.AvgLoss,
ctx.TradingStats.MaxDrawdownPct))
}
// 最近完成的订单
if len(ctx.RecentOrders) > 0 {
sb.WriteString("## 最近完成的交易\n")
for i, order := range ctx.RecentOrders {
resultStr := "盈利"
if order.RealizedPnL < 0 {
resultStr = "亏损"
}
sb.WriteString(fmt.Sprintf("%d. %s %s | 入场%.4f 出场%.4f | %s: %+.2f USDT (%+.2f%%) | %s\n",
i+1, order.Symbol, order.Side,
order.EntryPrice, order.ExitPrice,
resultStr, order.RealizedPnL, order.PnLPct,
order.FilledAt))
}
sb.WriteString("\n")
}
// 候选币种
sb.WriteString(fmt.Sprintf("## 候选币种 (%d个)\n\n", len(ctx.MarketDataMap)))
displayedCount := 0
for _, coin := range ctx.CandidateCoins {
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))
sb.WriteString("\n")
}
sb.WriteString("\n")
sb.WriteString("---\n\n")
sb.WriteString("现在请分析并输出决策(思维链 + JSON)\n")
return sb.String()
}
// formatPositionInfo 格式化持仓信息
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(" | 持仓时长%d分钟", durationMin)
} else {
durationHour := durationMin / 60
durationMinRemainder := durationMin % 60
holdingDuration = fmt.Sprintf(" | 持仓时长%d小时%d分钟", durationHour, durationMinRemainder)
}
}
// 计算仓位价值
positionValue := pos.Quantity * pos.MarkPrice
if positionValue < 0 {
positionValue = -positionValue
}
sb.WriteString(fmt.Sprintf("%d. %s %s | 入场价%.4f 当前价%.4f | 数量%.4f | 仓位价值%.2f USDT | 盈亏%+.2f%% | 盈亏金额%+.2f USDT | 最高收益率%.2f%% | 杠杆%dx | 保证金%.0f | 强平价%.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))
sb.WriteString("\n")
}
return sb.String()
}
// formatCoinSourceTag 格式化币种来源标签
func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
if len(sources) > 1 {
return " (AI500+OI_Top双重信号)"
} else if len(sources) == 1 {
switch sources[0] {
case "ai500":
return " (AI500)"
case "oi_top":
return " (OI_Top持仓增长)"
case "static":
return " (手动选择)"
}
}
return ""
}
// formatMarketData 根据策略配置格式化市场数据
func (e *StrategyEngine) formatMarketData(data *market.Data) string {
var sb strings.Builder
indicators := e.config.Indicators
// 当前价格(总是显示)
sb.WriteString(fmt.Sprintf("current_price = %.4f", data.CurrentPrice))
// EMA
if indicators.EnableEMA {
sb.WriteString(fmt.Sprintf(", current_ema20 = %.3f", data.CurrentEMA20))
}
// MACD
if indicators.EnableMACD {
sb.WriteString(fmt.Sprintf(", current_macd = %.3f", data.CurrentMACD))
}
// RSI
if indicators.EnableRSI {
sb.WriteString(fmt.Sprintf(", current_rsi7 = %.3f", data.CurrentRSI7))
}
sb.WriteString("\n\n")
// OI 和 Funding Rate
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 {
// 兼容旧的数据格式
// 日内数据
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()
}
// formatTimeframeSeriesData 格式化单个时间周期的序列数据
func (e *StrategyEngine) formatTimeframeSeriesData(sb *strings.Builder, data *market.TimeframeSeriesData, indicators store.IndicatorConfig) {
if len(data.MidPrices) > 0 {
sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidPrices)))
}
if indicators.EnableEMA {
if len(data.EMA20Values) > 0 {
sb.WriteString(fmt.Sprintf("EMA indicators (20-period): %s\n\n", formatFloatSlice(data.EMA20Values)))
}
if len(data.EMA50Values) > 0 {
sb.WriteString(fmt.Sprintf("EMA indicators (50-period): %s\n\n", formatFloatSlice(data.EMA50Values)))
}
}
if indicators.EnableMACD && len(data.MACDValues) > 0 {
sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.MACDValues)))
}
if indicators.EnableRSI {
if len(data.RSI7Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (7-Period): %s\n\n", formatFloatSlice(data.RSI7Values)))
}
if len(data.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (14-Period): %s\n\n", formatFloatSlice(data.RSI14Values)))
}
}
if indicators.EnableVolume && len(data.Volume) > 0 {
sb.WriteString(fmt.Sprintf("Volume: %s\n\n", formatFloatSlice(data.Volume)))
}
if indicators.EnableATR {
sb.WriteString(fmt.Sprintf("ATR (14-period): %.3f\n\n", data.ATR14))
}
}
// formatFloatSlice 格式化浮点数切片
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, ", ") + "]"
}
// BuildSystemPrompt 根据策略配置构建 System Prompt
func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string) string {
var sb strings.Builder
riskControl := e.config.RiskControl
promptSections := e.config.PromptSections
// 1. 角色定义(可编辑)
if promptSections.RoleDefinition != "" {
sb.WriteString(promptSections.RoleDefinition)
sb.WriteString("\n\n")
} else {
sb.WriteString("# 你是专业的加密货币交易AI\n\n")
sb.WriteString("你的任务是根据提供的市场数据做出交易决策。\n\n")
}
// 2. 交易模式变体
switch strings.ToLower(strings.TrimSpace(variant)) {
case "aggressive":
sb.WriteString("## 模式:Aggressive(进攻型)\n- 优先捕捉趋势突破,可在信心度≥70时分批建仓\n- 允许更高仓位,但须严格设置止损并说明盈亏比\n\n")
case "conservative":
sb.WriteString("## 模式:Conservative(稳健型)\n- 仅在多重信号共振时开仓\n- 优先保留现金,连续亏损必须暂停多个周期\n\n")
case "scalping":
sb.WriteString("## 模式:Scalping(剥头皮)\n- 聚焦短周期动量,目标收益较小但要求迅速\n- 若价格两根bar内未按预期运行,立即减仓或止损\n\n")
}
// 3. 硬约束(风险控制)- 来自策略配置(不可编辑,自动生成)
sb.WriteString("# 硬约束(风险控制)\n\n")
sb.WriteString(fmt.Sprintf("1. 风险回报比: 必须 ≥ 1:%.1f\n", riskControl.MinRiskRewardRatio))
sb.WriteString(fmt.Sprintf("2. 最多持仓: %d个币种(质量>数量)\n", riskControl.MaxPositions))
sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U | BTC/ETH %.0f-%.0f U\n",
accountEquity*0.8, accountEquity*riskControl.MaxPositionRatio,
accountEquity*5, accountEquity*10))
sb.WriteString(fmt.Sprintf("4. 杠杆限制: **山寨币最大%dx杠杆** | **BTC/ETH最大%dx杠杆**\n",
riskControl.AltcoinMaxLeverage, riskControl.BTCETHMaxLeverage))
sb.WriteString(fmt.Sprintf("5. 保证金使用率 ≤ %.0f%%\n", riskControl.MaxMarginUsage*100))
sb.WriteString(fmt.Sprintf("6. 开仓金额: 建议 ≥%.0f USDT\n", riskControl.MinPositionSize))
sb.WriteString(fmt.Sprintf("7. 最小信心度: ≥%d\n\n", riskControl.MinConfidence))
// 4. 交易频率与信号质量(可编辑)
if promptSections.TradingFrequency != "" {
sb.WriteString(promptSections.TradingFrequency)
sb.WriteString("\n\n")
} else {
sb.WriteString("# ⏱️ 交易频率认知\n\n")
sb.WriteString("- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔\n")
sb.WriteString("- 每小时>2笔 = 过度交易\n")
sb.WriteString("- 单笔持仓时间≥30-60分钟\n")
sb.WriteString("如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。\n\n")
}
// 5. 开仓标准(可编辑)
if promptSections.EntryStandards != "" {
sb.WriteString(promptSections.EntryStandards)
sb.WriteString("\n\n你拥有以下指标数据:\n")
e.writeAvailableIndicators(&sb)
sb.WriteString(fmt.Sprintf("\n**信心度 ≥%d** 才能开仓。\n\n", riskControl.MinConfidence))
} else {
sb.WriteString("# 🎯 开仓标准(严格)\n\n")
sb.WriteString("只在多重信号共振时开仓。你拥有:\n")
e.writeAvailableIndicators(&sb)
sb.WriteString(fmt.Sprintf("\n自由运用任何有效的分析方法,但**信心度 ≥%d** 才能开仓;避免单一指标、信号矛盾、横盘震荡、刚平仓即重启等低质量行为。\n\n", riskControl.MinConfidence))
}
// 6. 决策流程提示(可编辑)
if promptSections.DecisionProcess != "" {
sb.WriteString(promptSections.DecisionProcess)
sb.WriteString("\n\n")
} else {
sb.WriteString("# 📋 决策流程\n\n")
sb.WriteString("1. 检查持仓 → 是否该止盈/止损\n")
sb.WriteString("2. 扫描候选币 + 多时间框 → 是否存在强信号\n")
sb.WriteString("3. 先写思维链,再输出结构化JSON\n\n")
}
// 7. 输出格式
sb.WriteString("# 输出格式 (严格遵守)\n\n")
sb.WriteString("**必须使用XML标签 <reasoning> 和 <decision> 标签分隔思维链和决策JSON,避免解析错误**\n\n")
sb.WriteString("## 格式要求\n\n")
sb.WriteString("<reasoning>\n")
sb.WriteString("你的思维链分析...\n")
sb.WriteString("- 简洁分析你的思考过程 \n")
sb.WriteString("</reasoning>\n\n")
sb.WriteString("<decision>\n")
sb.WriteString("第二步: JSON决策数组\n\n")
sb.WriteString("```json\n[\n")
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, accountEquity*5))
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\"}\n")
sb.WriteString("]\n```\n")
sb.WriteString("</decision>\n\n")
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\n")
// 8. 自定义 Prompt
if e.config.CustomPrompt != "" {
sb.WriteString("# 📌 个性化交易策略\n\n")
sb.WriteString(e.config.CustomPrompt)
sb.WriteString("\n\n")
sb.WriteString("注意: 以上个性化策略是对基础规则的补充,不能违背基础风险控制原则。\n")
}
return sb.String()
}
// writeAvailableIndicators 写入可用指标列表
func (e *StrategyEngine) writeAvailableIndicators(sb *strings.Builder) {
indicators := e.config.Indicators
kline := indicators.Klines
sb.WriteString(fmt.Sprintf("- %s价格序列", kline.PrimaryTimeframe))
if kline.EnableMultiTimeframe {
sb.WriteString(fmt.Sprintf(" + %s K线序列\n", kline.LongerTimeframe))
} else {
sb.WriteString("\n")
}
if indicators.EnableEMA {
sb.WriteString("- EMA 指标")
if len(indicators.EMAPeriods) > 0 {
sb.WriteString(fmt.Sprintf("(周期: %v", indicators.EMAPeriods))
}
sb.WriteString("\n")
}
if indicators.EnableMACD {
sb.WriteString("- MACD 指标\n")
}
if indicators.EnableRSI {
sb.WriteString("- RSI 指标")
if len(indicators.RSIPeriods) > 0 {
sb.WriteString(fmt.Sprintf("(周期: %v", indicators.RSIPeriods))
}
sb.WriteString("\n")
}
if indicators.EnableATR {
sb.WriteString("- ATR 指标")
if len(indicators.ATRPeriods) > 0 {
sb.WriteString(fmt.Sprintf("(周期: %v", indicators.ATRPeriods))
}
sb.WriteString("\n")
}
if indicators.EnableVolume {
sb.WriteString("- 成交量数据\n")
}
if indicators.EnableOI {
sb.WriteString("- 持仓量(OI)数据\n")
}
if indicators.EnableFundingRate {
sb.WriteString("- 资金费率\n")
}
if len(e.config.CoinSource.StaticCoins) > 0 || e.config.CoinSource.UseCoinPool || e.config.CoinSource.UseOITop {
sb.WriteString("- AI500 / OI_Top 筛选标签(若有)\n")
}
}
// GetRiskControlConfig 获取风险控制配置
func (e *StrategyEngine) GetRiskControlConfig() store.RiskControlConfig {
return e.config.RiskControl
}
// GetConfig 获取完整策略配置
func (e *StrategyEngine) GetConfig() *store.StrategyConfig {
return e.config
}