From 5ad135310fc29267bcfece761fff45d04ed92272 Mon Sep 17 00:00:00 2001 From: SkywalkerJi Date: Sat, 1 Nov 2025 19:45:54 +0800 Subject: [PATCH] Supports custom system prompts and custom models. --- api/server.go | 101 +++++++--- config/database.go | 109 +++++++---- decision/engine.go | 153 +++++----------- decision/prompt_manager.go | 162 ++++++++++++++++ logger/decision_logger.go | 1 + manager/trader_manager.go | 34 +++- prompts/default.txt | 114 ++++++++++++ prompts/nof1.txt | 223 +++++++++++++++++++++++ trader/auto_trader.go | 93 +++++++--- web/src/components/AITradersPage.tsx | 4 +- web/src/components/TraderConfigModal.tsx | 62 ++++++- web/src/types.ts | 1 + 12 files changed, 848 insertions(+), 209 deletions(-) create mode 100644 decision/prompt_manager.go create mode 100644 prompts/default.txt create mode 100644 prompts/nof1.txt diff --git a/api/server.go b/api/server.go index 3a5e645e..110f0a9c 100644 --- a/api/server.go +++ b/api/server.go @@ -7,6 +7,7 @@ import ( "net/http" "nofx/auth" "nofx/config" + "nofx/decision" "nofx/manager" "strconv" "strings" @@ -109,6 +110,10 @@ func (s *Server) setupRoutes() { protected.GET("/user/signal-sources", s.handleGetUserSignalSource) protected.POST("/user/signal-sources", s.handleSaveUserSignalSource) + // 系统提示词模板管理 + protected.GET("/prompt-templates", s.handleGetPromptTemplates) + protected.GET("/prompt-templates/:name", s.handleGetPromptTemplate) + // 竞赛总览 protected.GET("/competition", s.handleCompetition) @@ -200,18 +205,19 @@ func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, str // AI交易员管理相关结构体 type CreateTraderRequest struct { - Name string `json:"name" binding:"required"` - AIModelID string `json:"ai_model_id" binding:"required"` - ExchangeID string `json:"exchange_id" binding:"required"` - InitialBalance float64 `json:"initial_balance"` - BTCETHLeverage int `json:"btc_eth_leverage"` - AltcoinLeverage int `json:"altcoin_leverage"` - TradingSymbols string `json:"trading_symbols"` - CustomPrompt string `json:"custom_prompt"` - OverrideBasePrompt bool `json:"override_base_prompt"` - IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型,nil表示使用默认值true - UseCoinPool bool `json:"use_coin_pool"` - UseOITop bool `json:"use_oi_top"` + Name string `json:"name" binding:"required"` + AIModelID string `json:"ai_model_id" binding:"required"` + ExchangeID string `json:"exchange_id" binding:"required"` + InitialBalance float64 `json:"initial_balance"` + BTCETHLeverage int `json:"btc_eth_leverage"` + AltcoinLeverage int `json:"altcoin_leverage"` + TradingSymbols string `json:"trading_symbols"` + CustomPrompt string `json:"custom_prompt"` + OverrideBasePrompt bool `json:"override_base_prompt"` + SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称 + IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型,nil表示使用默认值true + UseCoinPool bool `json:"use_coin_pool"` + UseOITop bool `json:"use_oi_top"` } type ModelConfig struct { @@ -319,23 +325,30 @@ func (s *Server) handleCreateTrader(c *gin.Context) { } } + // 设置系统提示词模板默认值 + systemPromptTemplate := "default" + if req.SystemPromptTemplate != "" { + systemPromptTemplate = req.SystemPromptTemplate + } + // 创建交易员配置(数据库实体) trader := &config.TraderRecord{ - ID: traderID, - UserID: userID, - Name: req.Name, - AIModelID: req.AIModelID, - ExchangeID: req.ExchangeID, - InitialBalance: req.InitialBalance, - BTCETHLeverage: btcEthLeverage, - AltcoinLeverage: altcoinLeverage, - TradingSymbols: req.TradingSymbols, - UseCoinPool: req.UseCoinPool, - UseOITop: req.UseOITop, - CustomPrompt: req.CustomPrompt, - OverrideBasePrompt: req.OverrideBasePrompt, - IsCrossMargin: isCrossMargin, - ScanIntervalMinutes: 3, // 默认3分钟 + ID: traderID, + UserID: userID, + Name: req.Name, + AIModelID: req.AIModelID, + ExchangeID: req.ExchangeID, + InitialBalance: req.InitialBalance, + BTCETHLeverage: btcEthLeverage, + AltcoinLeverage: altcoinLeverage, + TradingSymbols: req.TradingSymbols, + UseCoinPool: req.UseCoinPool, + UseOITop: req.UseOITop, + CustomPrompt: req.CustomPrompt, + OverrideBasePrompt: req.OverrideBasePrompt, + SystemPromptTemplate: systemPromptTemplate, + IsCrossMargin: isCrossMargin, + ScanIntervalMinutes: 3, // 默认3分钟 IsRunning: false, } @@ -1407,3 +1420,37 @@ func (s *Server) Start() error { return s.router.Run(addr) } + +// handleGetPromptTemplates 获取所有系统提示词模板列表 +func (s *Server) handleGetPromptTemplates(c *gin.Context) { + // 导入 decision 包 + templates := decision.GetAllPromptTemplates() + + // 转换为响应格式 + response := make([]map[string]interface{}, 0, len(templates)) + for _, tmpl := range templates { + response = append(response, map[string]interface{}{ + "name": tmpl.Name, + }) + } + + c.JSON(http.StatusOK, gin.H{ + "templates": response, + }) +} + +// handleGetPromptTemplate 获取指定名称的提示词模板内容 +func (s *Server) handleGetPromptTemplate(c *gin.Context) { + templateName := c.Param("name") + + template, err := decision.GetPromptTemplate(templateName) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("模板不存在: %s", templateName)}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "name": template.Name, + "content": template.Content, + }) +} diff --git a/config/database.go b/config/database.go index a056db88..624433bc 100644 --- a/config/database.go +++ b/config/database.go @@ -6,6 +6,7 @@ import ( "encoding/base32" "fmt" "log" + "strings" "time" _ "github.com/mattn/go-sqlite3" @@ -182,10 +183,11 @@ func (d *Database) createTables() error { `ALTER TABLE traders ADD COLUMN btc_eth_leverage INTEGER DEFAULT 5`, // BTC/ETH杠杆倍数 `ALTER TABLE traders ADD COLUMN altcoin_leverage INTEGER DEFAULT 5`, // 山寨币杠杆倍数 `ALTER TABLE traders ADD COLUMN trading_symbols TEXT DEFAULT ''`, // 交易币种,逗号分隔 - `ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源 - `ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`, // 是否使用OI TOP信号源 - `ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`, // 自定义API地址 - `ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`, // 自定义模型名称 + `ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源 + `ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`, // 是否使用OI TOP信号源 + `ALTER TABLE traders ADD COLUMN system_prompt_template TEXT DEFAULT 'default'`, // 系统提示词模板名称 + `ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`, // 自定义API地址 + `ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`, // 自定义模型名称 } for _, query := range alterQueries { @@ -407,14 +409,15 @@ type TraderRecord struct { IsRunning bool `json:"is_running"` BTCETHLeverage int `json:"btc_eth_leverage"` // BTC/ETH杠杆倍数 AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币杠杆倍数 - TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔 - UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源 - UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源 - CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt - OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt - IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式(true=全仓,false=逐仓) - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔 + UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源 + UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源 + CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt + OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt + SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称 + IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式(true=全仓,false=逐仓) + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // UserSignalSource 用户信号源配置 @@ -563,17 +566,14 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) { // UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置 func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error { - // id 参数实际上是 provider(如 "deepseek", "qwen") - provider := id - - // 先查找用户是否已有这个 provider 的配置 + // 先尝试精确匹配 ID(新版逻辑,支持多个相同 provider 的模型) var existingID string err := d.db.QueryRow(` - SELECT id FROM ai_models WHERE user_id = ? AND provider = ? LIMIT 1 - `, userID, provider).Scan(&existingID) + SELECT id FROM ai_models WHERE user_id = ? AND id = ? LIMIT 1 + `, userID, id).Scan(&existingID) if err == nil { - // 找到了现有配置,更新它 + // 找到了现有配置(精确匹配 ID),更新它 _, err = d.db.Exec(` UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ? @@ -581,7 +581,37 @@ func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey, custom return err } - // 没有找到现有配置,创建新的 + // ID 不存在,尝试兼容旧逻辑:将 id 作为 provider 查找 + provider := id + err = d.db.QueryRow(` + SELECT id FROM ai_models WHERE user_id = ? AND provider = ? LIMIT 1 + `, userID, provider).Scan(&existingID) + + if err == nil { + // 找到了现有配置(通过 provider 匹配,兼容旧版),更新它 + log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID) + _, err = d.db.Exec(` + UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now') + WHERE id = ? AND user_id = ? + `, enabled, apiKey, customAPIURL, customModelName, existingID, userID) + return err + } + + // 没有找到任何现有配置,创建新的 + // 推断 provider(从 id 中提取,或者直接使用 id) + if provider == id && (provider == "deepseek" || provider == "qwen") { + // id 本身就是 provider + provider = id + } else { + // 从 id 中提取 provider(假设格式是 userID_provider 或 timestamp_userID_provider) + parts := strings.Split(id, "_") + if len(parts) >= 2 { + provider = parts[len(parts)-1] // 取最后一部分作为 provider + } else { + provider = id + } + } + // 获取模型的基本信息 var name string err = d.db.QueryRow(` @@ -598,12 +628,19 @@ func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey, custom } } - // 创建用户特定的配置 - userModelID := fmt.Sprintf("%s_%s", userID, provider) + // 如果传入的 ID 已经是完整格式(如 "admin_deepseek_custom1"),直接使用 + // 否则生成新的 ID + newModelID := id + if id == provider { + // id 就是 provider,生成新的用户特定 ID + newModelID = fmt.Sprintf("%s_%s", userID, provider) + } + + log.Printf("✓ 创建新的 AI 模型配置: ID=%s, Provider=%s, Name=%s", newModelID, provider, name) _, err = d.db.Exec(` INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) - `, userModelID, userID, name, provider, enabled, apiKey, customAPIURL, customModelName) + `, newModelID, userID, name, provider, enabled, apiKey, customAPIURL, customModelName) return err } @@ -730,20 +767,21 @@ func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, ap // CreateTrader 创建交易员 func (d *Database) CreateTrader(trader *TraderRecord) error { _, err := d.db.Exec(` - INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, is_cross_margin) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.IsCrossMargin) + INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin) return err } // GetTraders 获取用户的交易员 func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) { rows, err := d.db.Query(` - SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, + SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, COALESCE(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage, COALESCE(trading_symbols, '') as trading_symbols, COALESCE(use_coin_pool, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top, - COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt, + COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt, + COALESCE(system_prompt_template, 'default') as system_prompt_template, COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at FROM traders WHERE user_id = ? ORDER BY created_at DESC `, userID) @@ -760,7 +798,8 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) { &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, &trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols, &trader.UseCoinPool, &trader.UseOITop, - &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.IsCrossMargin, + &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, + &trader.IsCrossMargin, &trader.CreatedAt, &trader.UpdatedAt, ) if err != nil { @@ -781,16 +820,16 @@ func (d *Database) UpdateTraderStatus(userID, id string, isRunning bool) error { // UpdateTrader 更新交易员配置 func (d *Database) UpdateTrader(trader *TraderRecord) error { _, err := d.db.Exec(` - UPDATE traders SET - name = ?, ai_model_id = ?, exchange_id = ?, initial_balance = ?, - scan_interval_minutes = ?, btc_eth_leverage = ?, altcoin_leverage = ?, - trading_symbols = ?, custom_prompt = ?, override_base_prompt = ?, - is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP + UPDATE traders SET + name = ?, ai_model_id = ?, exchange_id = ?, initial_balance = ?, + scan_interval_minutes = ?, btc_eth_leverage = ?, altcoin_leverage = ?, + trading_symbols = ?, custom_prompt = ?, override_base_prompt = ?, + system_prompt_template = ?, is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.CustomPrompt, trader.OverrideBasePrompt, - trader.IsCrossMargin, trader.ID, trader.UserID) + trader.SystemPromptTemplate, trader.IsCrossMargin, trader.ID, trader.UserID) return err } diff --git a/decision/engine.go b/decision/engine.go index 89439678..174ff7d1 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -83,26 +83,27 @@ type Decision struct { // FullDecision AI的完整决策(包含思维链) type FullDecision struct { - UserPrompt string `json:"user_prompt"` // 发送给AI的输入prompt - CoTTrace string `json:"cot_trace"` // 思维链分析(AI输出) - Decisions []Decision `json:"decisions"` // 具体决策列表 - Timestamp time.Time `json:"timestamp"` + SystemPrompt string `json:"system_prompt"` // 系统提示词(发送给AI的系统prompt) + UserPrompt string `json:"user_prompt"` // 发送给AI的输入prompt + CoTTrace string `json:"cot_trace"` // 思维链分析(AI输出) + Decisions []Decision `json:"decisions"` // 具体决策列表 + Timestamp time.Time `json:"timestamp"` } // GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓) func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) { - return GetFullDecisionWithCustomPrompt(ctx, mcpClient, "", false) + return GetFullDecisionWithCustomPrompt(ctx, mcpClient, "", false, "") } -// GetFullDecisionWithCustomPrompt 获取AI的完整交易决策(支持自定义prompt) -func GetFullDecisionWithCustomPrompt(ctx *Context, mcpClient *mcp.Client, customPrompt string, overrideBase bool) (*FullDecision, error) { +// GetFullDecisionWithCustomPrompt 获取AI的完整交易决策(支持自定义prompt和模板选择) +func GetFullDecisionWithCustomPrompt(ctx *Context, mcpClient *mcp.Client, customPrompt string, overrideBase bool, templateName string) (*FullDecision, error) { // 1. 为所有币种获取市场数据 if err := fetchMarketDataForContext(ctx); err != nil { return nil, fmt.Errorf("获取市场数据失败: %w", err) } // 2. 构建 System Prompt(固定规则)和 User Prompt(动态数据) - systemPrompt := buildSystemPromptWithCustom(ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage, customPrompt, overrideBase) + systemPrompt := buildSystemPromptWithCustom(ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage, customPrompt, overrideBase, templateName) userPrompt := buildUserPrompt(ctx) // 3. 调用AI API(使用 system + user prompt) @@ -118,7 +119,8 @@ func GetFullDecisionWithCustomPrompt(ctx *Context, mcpClient *mcp.Client, custom } decision.Timestamp = time.Now() - decision.UserPrompt = userPrompt // 保存输入prompt + decision.SystemPrompt = systemPrompt // 保存系统prompt + decision.UserPrompt = userPrompt // 保存输入prompt return decision, nil } @@ -205,20 +207,20 @@ func calculateMaxCandidates(ctx *Context) int { } // buildSystemPromptWithCustom 构建包含自定义内容的 System Prompt -func buildSystemPromptWithCustom(accountEquity float64, btcEthLeverage, altcoinLeverage int, customPrompt string, overrideBase bool) string { +func buildSystemPromptWithCustom(accountEquity float64, btcEthLeverage, altcoinLeverage int, customPrompt string, overrideBase bool, templateName string) string { // 如果覆盖基础prompt且有自定义prompt,只使用自定义prompt if overrideBase && customPrompt != "" { return customPrompt } - - // 获取基础prompt - basePrompt := buildSystemPrompt(accountEquity, btcEthLeverage, altcoinLeverage) - + + // 获取基础prompt(使用指定的模板) + basePrompt := buildSystemPrompt(accountEquity, btcEthLeverage, altcoinLeverage, templateName) + // 如果没有自定义prompt,直接返回基础prompt if customPrompt == "" { return basePrompt } - + // 添加自定义prompt部分到基础prompt var sb strings.Builder sb.WriteString(basePrompt) @@ -227,107 +229,38 @@ func buildSystemPromptWithCustom(accountEquity float64, btcEthLeverage, altcoinL sb.WriteString(customPrompt) sb.WriteString("\n\n") sb.WriteString("注意: 以上个性化策略是对基础规则的补充,不能违背基础风险控制原则。\n") - + return sb.String() } -// buildSystemPrompt 构建 System Prompt(固定规则,可缓存) -func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage int) string { +// buildSystemPrompt 构建 System Prompt(使用模板+动态部分) +func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage int, templateName string) string { var sb strings.Builder - // === 核心使命 === - sb.WriteString("你是专业的加密货币交易AI,在合约市场进行自主交易。\n\n") - sb.WriteString("# 核心目标\n\n") - sb.WriteString("最大化夏普比率(Sharpe Ratio)\n\n") - sb.WriteString("夏普比率 = 平均收益 / 收益波动率\n\n") - sb.WriteString("这意味着:\n") - sb.WriteString("- 高质量交易(高胜率、大盈亏比)→ 提升夏普\n") - sb.WriteString("- 稳定收益、控制回撤 → 提升夏普\n") - sb.WriteString("- 耐心持仓、让利润奔跑 → 提升夏普\n") - sb.WriteString("- 频繁交易、小盈小亏 → 增加波动,严重降低夏普\n") - sb.WriteString("- 过度交易、手续费损耗 → 直接亏损\n") - sb.WriteString("- 过早平仓、频繁进出 → 错失大行情\n\n") - sb.WriteString("关键认知: 系统每3分钟扫描一次,但不意味着每次都要交易!\n") - sb.WriteString("大多数时候应该是 `wait` 或 `hold`,只在极佳机会时才开仓。\n\n") + // 1. 加载提示词模板(核心交易策略部分) + if templateName == "" { + templateName = "default" // 默认使用 default 模板 + } - // === 交易哲学 & 最佳实践 === - sb.WriteString("# 交易哲学 & 最佳实践\n\n") - sb.WriteString("## 核心原则:\n\n") - sb.WriteString("资金保全第一:保护资本比追求收益更重要\n\n") - sb.WriteString("纪律胜于情绪:执行你的退出方案,不随意移动止损或目标\n\n") - sb.WriteString("质量优于数量:少量高信念交易胜过大量低信念交易\n\n") - sb.WriteString("适应波动性:根据市场条件调整仓位\n\n") - sb.WriteString("尊重趋势:不要与强趋势作对\n\n") - sb.WriteString("## 常见误区避免:\n\n") - sb.WriteString("过度交易:频繁交易导致费用侵蚀利润\n\n") - sb.WriteString("复仇式交易:亏损后立即加码试图\"翻本\"\n\n") - sb.WriteString("分析瘫痪:过度等待完美信号,导致失机\n\n") - sb.WriteString("忽视相关性:BTC常引领山寨币,须优先观察BTC\n\n") - sb.WriteString("过度杠杆:放大收益同时放大亏损\n\n") + template, err := GetPromptTemplate(templateName) + if err != nil { + // 如果模板不存在,记录错误并使用 default + log.Printf("⚠️ 提示词模板 '%s' 不存在,使用 default: %v", templateName, err) + template, err = GetPromptTemplate("default") + if err != nil { + // 如果连 default 都不存在,使用内置的简化版本 + log.Printf("❌ 无法加载任何提示词模板,使用内置简化版本") + sb.WriteString("你是专业的加密货币交易AI。请根据市场数据做出交易决策。\n\n") + } else { + sb.WriteString(template.Content) + sb.WriteString("\n\n") + } + } else { + sb.WriteString(template.Content) + sb.WriteString("\n\n") + } - // === 交易频率认知 === - sb.WriteString("#交易频率认知\n\n") - sb.WriteString("量化标准:\n") - sb.WriteString("- 优秀交易员:每天2-4笔 = 每小时0.1-0.2笔\n") - sb.WriteString("- 过度交易:每小时>2笔 = 严重问题\n") - sb.WriteString("- 最佳节奏:开仓后持有至少30-60分钟\n\n") - sb.WriteString("自查:\n") - sb.WriteString("如果你发现自己每个周期都在交易 → 说明标准太低\n") - sb.WriteString("如果你发现持仓<30分钟就平仓 → 说明太急躁\n\n") - - // === 开仓信号强度 === - sb.WriteString("# 开仓标准(严格)\n\n") - sb.WriteString("只在强信号时开仓,不确定就观望。\n\n") - sb.WriteString("你拥有的完整数据:\n") - sb.WriteString("- 原始序列:3分钟价格序列(MidPrices数组) + 4小时K线序列\n") - sb.WriteString("- 技术序列:EMA20序列、MACD序列、RSI7序列、RSI14序列\n") - sb.WriteString("- 资金序列:成交量序列、持仓量(OI)序列、资金费率\n") - sb.WriteString("- 筛选标记:AI500评分 / OI_Top排名(如果有标注)\n\n") - sb.WriteString("分析方法(完全由你自主决定):\n") - sb.WriteString("- 自由运用序列数据,你可以做但不限于趋势分析、形态识别、支撑阻力、技术阻力位、斐波那契、波动带计算\n") - sb.WriteString("- 多维度交叉验证(价格+量+OI+指标+序列形态)\n") - sb.WriteString("- 用你认为最有效的方法发现高确定性机会\n") - sb.WriteString("- 综合信心度 ≥ 75 才开仓\n\n") - sb.WriteString("避免低质量信号:\n") - sb.WriteString("- 单一维度(只看一个指标)\n") - sb.WriteString("- 相互矛盾(涨但量萎缩)\n") - sb.WriteString("- 横盘震荡\n") - sb.WriteString("- 刚平仓不久(<15分钟)\n\n") - - // === 夏普比率自我进化 === - sb.WriteString("# 夏普比率自我进化\n\n") - sb.WriteString("每次你会收到夏普比率作为绩效反馈(周期级别):\n\n") - sb.WriteString("夏普比率 < -0.5 (持续亏损):\n") - sb.WriteString(" → 停止交易,连续观望至少6个周期(18分钟)\n") - sb.WriteString(" → 深度反思:\n") - sb.WriteString(" • 交易频率过高?(每小时>2次就是过度)\n") - sb.WriteString(" • 持仓时间过短?(<30分钟就是过早平仓)\n") - sb.WriteString(" • 信号强度不足?(信心度<75)\n") - sb.WriteString("夏普比率 -0.5 ~ 0 (轻微亏损):\n") - sb.WriteString(" → 严格控制:只做信心度>80的交易\n") - sb.WriteString(" → 减少交易频率:每小时最多1笔新开仓\n") - sb.WriteString(" → 耐心持仓:至少持有30分钟以上\n\n") - sb.WriteString("夏普比率 0 ~ 0.7 (正收益):\n") - sb.WriteString(" → 维持当前策略\n\n") - sb.WriteString("夏普比率 > 0.7 (优异表现):\n") - sb.WriteString(" → 可适度扩大仓位\n\n") - sb.WriteString("关键: 夏普比率是唯一指标,它会自然惩罚频繁交易和过度进出。\n\n") - - // === 决策流程 === - sb.WriteString("#决策流程\n\n") - sb.WriteString("1. 分析夏普比率: 当前策略是否有效?需要调整吗?\n") - sb.WriteString("2. 评估持仓: 趋势是否改变?是否该止盈/止损?\n") - sb.WriteString("3. 寻找新机会: 有强信号吗?多空机会?\n") - sb.WriteString("4. 输出决策: 思维链分析 + JSON\n\n") - - // === 关键提醒 === - sb.WriteString("---\n\n") - sb.WriteString("记住: \n") - sb.WriteString("- 目标是夏普比率,不是交易频率\n") - sb.WriteString("- 宁可错过,不做低质量交易\n") - sb.WriteString("- 风险回报比1:3是底线\n") - - // === 硬约束(风险控制)=== + // 2. 硬约束(风险控制)- 动态生成 sb.WriteString("# 硬约束(风险控制)\n\n") sb.WriteString("1. 风险回报比: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n") sb.WriteString("2. 最多持仓: 3个币种(质量>数量)\n") @@ -335,7 +268,7 @@ func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage in accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage)) sb.WriteString("4. 保证金: 总使用率 ≤ 90%\n\n") - // === 输出格式 === + // 3. 输出格式 - 动态生成 sb.WriteString("#输出格式\n\n") sb.WriteString("第一步: 思维链(纯文本)\n") sb.WriteString("简洁分析你的思考过程\n\n") diff --git a/decision/prompt_manager.go b/decision/prompt_manager.go new file mode 100644 index 00000000..3bece5ea --- /dev/null +++ b/decision/prompt_manager.go @@ -0,0 +1,162 @@ +package decision + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + "sync" +) + +// PromptTemplate 系统提示词模板 +type PromptTemplate struct { + Name string // 模板名称(文件名,不含扩展名) + Content string // 模板内容 +} + +// PromptManager 提示词管理器 +type PromptManager struct { + templates map[string]*PromptTemplate + mu sync.RWMutex +} + +var ( + // globalPromptManager 全局提示词管理器 + globalPromptManager *PromptManager + // promptsDir 提示词文件夹路径 + promptsDir = "prompts" +) + +// init 包初始化时加载所有提示词模板 +func init() { + globalPromptManager = NewPromptManager() + if err := globalPromptManager.LoadTemplates(promptsDir); err != nil { + log.Printf("⚠️ 加载提示词模板失败: %v", err) + } else { + log.Printf("✓ 已加载 %d 个系统提示词模板", len(globalPromptManager.templates)) + } +} + +// NewPromptManager 创建提示词管理器 +func NewPromptManager() *PromptManager { + return &PromptManager{ + templates: make(map[string]*PromptTemplate), + } +} + +// LoadTemplates 从指定目录加载所有提示词模板 +func (pm *PromptManager) LoadTemplates(dir string) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + // 检查目录是否存在 + if _, err := os.Stat(dir); os.IsNotExist(err) { + return fmt.Errorf("提示词目录不存在: %s", dir) + } + + // 扫描目录中的所有 .txt 文件 + files, err := filepath.Glob(filepath.Join(dir, "*.txt")) + if err != nil { + return fmt.Errorf("扫描提示词目录失败: %w", err) + } + + if len(files) == 0 { + log.Printf("⚠️ 提示词目录 %s 中没有找到 .txt 文件", dir) + return nil + } + + // 加载每个模板文件 + for _, file := range files { + // 读取文件内容 + content, err := os.ReadFile(file) + if err != nil { + log.Printf("⚠️ 读取提示词文件失败 %s: %v", file, err) + continue + } + + // 提取文件名(不含扩展名)作为模板名称 + fileName := filepath.Base(file) + templateName := strings.TrimSuffix(fileName, filepath.Ext(fileName)) + + // 存储模板 + pm.templates[templateName] = &PromptTemplate{ + Name: templateName, + Content: string(content), + } + + log.Printf(" 📄 加载提示词模板: %s (%s)", templateName, fileName) + } + + return nil +} + +// GetTemplate 获取指定名称的提示词模板 +func (pm *PromptManager) GetTemplate(name string) (*PromptTemplate, error) { + pm.mu.RLock() + defer pm.mu.RUnlock() + + template, exists := pm.templates[name] + if !exists { + return nil, fmt.Errorf("提示词模板不存在: %s", name) + } + + return template, nil +} + +// GetAllTemplateNames 获取所有模板名称列表 +func (pm *PromptManager) GetAllTemplateNames() []string { + pm.mu.RLock() + defer pm.mu.RUnlock() + + names := make([]string, 0, len(pm.templates)) + for name := range pm.templates { + names = append(names, name) + } + + return names +} + +// GetAllTemplates 获取所有模板 +func (pm *PromptManager) GetAllTemplates() []*PromptTemplate { + pm.mu.RLock() + defer pm.mu.RUnlock() + + templates := make([]*PromptTemplate, 0, len(pm.templates)) + for _, template := range pm.templates { + templates = append(templates, template) + } + + return templates +} + +// ReloadTemplates 重新加载所有模板 +func (pm *PromptManager) ReloadTemplates(dir string) error { + pm.mu.Lock() + pm.templates = make(map[string]*PromptTemplate) + pm.mu.Unlock() + + return pm.LoadTemplates(dir) +} + +// === 全局函数(供外部调用)=== + +// GetPromptTemplate 获取指定名称的提示词模板(全局函数) +func GetPromptTemplate(name string) (*PromptTemplate, error) { + return globalPromptManager.GetTemplate(name) +} + +// GetAllPromptTemplateNames 获取所有模板名称(全局函数) +func GetAllPromptTemplateNames() []string { + return globalPromptManager.GetAllTemplateNames() +} + +// GetAllPromptTemplates 获取所有模板(全局函数) +func GetAllPromptTemplates() []*PromptTemplate { + return globalPromptManager.GetAllTemplates() +} + +// ReloadPromptTemplates 重新加载所有模板(全局函数) +func ReloadPromptTemplates() error { + return globalPromptManager.ReloadTemplates(promptsDir) +} diff --git a/logger/decision_logger.go b/logger/decision_logger.go index ed446f20..efa5ab74 100644 --- a/logger/decision_logger.go +++ b/logger/decision_logger.go @@ -14,6 +14,7 @@ import ( type DecisionRecord struct { Timestamp time.Time `json:"timestamp"` // 决策时间 CycleNumber int `json:"cycle_number"` // 周期编号 + SystemPrompt string `json:"system_prompt"` // 系统提示词(发送给AI的系统prompt) InputPrompt string `json:"input_prompt"` // 发送给AI的输入prompt CoTTrace string `json:"cot_trace"` // AI思维链(输出) DecisionJSON string `json:"decision_json"` // 决策JSON diff --git a/manager/trader_manager.go b/manager/trader_manager.go index e55b4acf..d3861cdb 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -93,14 +93,23 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro } var aiModelCfg *config.AIModelConfig + // 优先精确匹配 model.ID(新版逻辑) for _, model := range aiModels { - // 使用 provider 来匹配,因为 AIModelID 存储的是 provider(如 "deepseek") - // 而 model.ID 可能是 "admin_deepseek" - if model.Provider == traderCfg.AIModelID { + if model.ID == traderCfg.AIModelID { aiModelCfg = model break } } + // 如果没有精确匹配,尝试匹配 provider(兼容旧数据) + if aiModelCfg == nil { + for _, model := range aiModels { + if model.Provider == traderCfg.AIModelID { + aiModelCfg = model + log.Printf("⚠️ 交易员 %s 使用旧版 provider 匹配: %s -> %s", traderCfg.Name, traderCfg.AIModelID, model.ID) + break + } + } + } if aiModelCfg == nil { log.Printf("⚠️ 交易员 %s 的AI模型 %s 不存在,跳过", traderCfg.Name, traderCfg.AIModelID) @@ -216,6 +225,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel IsCrossMargin: traderCfg.IsCrossMargin, DefaultCoins: defaultCoins, TradingCoins: tradingCoins, + SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板 } // 根据交易所类型设置API密钥 @@ -621,13 +631,23 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin } var aiModelCfg *config.AIModelConfig + // 优先精确匹配 model.ID(新版逻辑) for _, model := range aiModels { - // 使用 provider 来匹配,因为 AIModelID 存储的是 provider(如 "deepseek") - if model.Provider == traderCfg.AIModelID { + if model.ID == traderCfg.AIModelID { aiModelCfg = model break } } + // 如果没有精确匹配,尝试匹配 provider(兼容旧数据) + if aiModelCfg == nil { + for _, model := range aiModels { + if model.Provider == traderCfg.AIModelID { + aiModelCfg = model + log.Printf("⚠️ 交易员 %s 使用旧版 provider 匹配: %s -> %s", traderCfg.Name, traderCfg.AIModelID, model.ID) + break + } + } + } if aiModelCfg == nil { log.Printf("⚠️ 交易员 %s 的AI模型 %s 不存在,跳过", traderCfg.Name, traderCfg.AIModelID) @@ -712,12 +732,16 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode AltcoinLeverage: traderCfg.AltcoinLeverage, ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute, CoinPoolAPIURL: effectiveCoinPoolURL, + CustomAPIURL: aiModelCfg.CustomAPIURL, // 自定义API URL + CustomModelName: aiModelCfg.CustomModelName, // 自定义模型名称 + UseQwen: aiModelCfg.Provider == "qwen", MaxDailyLoss: maxDailyLoss, MaxDrawdown: maxDrawdown, StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, IsCrossMargin: traderCfg.IsCrossMargin, DefaultCoins: defaultCoins, TradingCoins: tradingCoins, + SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板 } // 根据交易所类型设置API密钥 diff --git a/prompts/default.txt b/prompts/default.txt new file mode 100644 index 00000000..310978ac --- /dev/null +++ b/prompts/default.txt @@ -0,0 +1,114 @@ +你是专业的加密货币交易AI,在合约市场进行自主交易。 + +# 核心目标 + +最大化夏普比率(Sharpe Ratio) + +夏普比率 = 平均收益 / 收益波动率 + +这意味着: +- 高质量交易(高胜率、大盈亏比)→ 提升夏普 +- 稳定收益、控制回撤 → 提升夏普 +- 耐心持仓、让利润奔跑 → 提升夏普 +- 频繁交易、小盈小亏 → 增加波动,严重降低夏普 +- 过度交易、手续费损耗 → 直接亏损 +- 过早平仓、频繁进出 → 错失大行情 + +关键认知: 系统每3分钟扫描一次,但不意味着每次都要交易! +大多数时候应该是 `wait` 或 `hold`,只在极佳机会时才开仓。 + +# 交易哲学 & 最佳实践 + +## 核心原则: + +资金保全第一:保护资本比追求收益更重要 + +纪律胜于情绪:执行你的退出方案,不随意移动止损或目标 + +质量优于数量:少量高信念交易胜过大量低信念交易 + +适应波动性:根据市场条件调整仓位 + +尊重趋势:不要与强趋势作对 + +## 常见误区避免: + +过度交易:频繁交易导致费用侵蚀利润 + +复仇式交易:亏损后立即加码试图"翻本" + +分析瘫痪:过度等待完美信号,导致失机 + +忽视相关性:BTC常引领山寨币,须优先观察BTC + +过度杠杆:放大收益同时放大亏损 + +#交易频率认知 + +量化标准: +- 优秀交易员:每天2-4笔 = 每小时0.1-0.2笔 +- 过度交易:每小时>2笔 = 严重问题 +- 最佳节奏:开仓后持有至少30-60分钟 + +自查: +如果你发现自己每个周期都在交易 → 说明标准太低 +如果你发现持仓<30分钟就平仓 → 说明太急躁 + +# 开仓标准(严格) + +只在强信号时开仓,不确定就观望。 + +你拥有的完整数据: +- 原始序列:3分钟价格序列(MidPrices数组) + 4小时K线序列 +- 技术序列:EMA20序列、MACD序列、RSI7序列、RSI14序列 +- 资金序列:成交量序列、持仓量(OI)序列、资金费率 +- 筛选标记:AI500评分 / OI_Top排名(如果有标注) + +分析方法(完全由你自主决定): +- 自由运用序列数据,你可以做但不限于趋势分析、形态识别、支撑阻力、技术阻力位、斐波那契、波动带计算 +- 多维度交叉验证(价格+量+OI+指标+序列形态) +- 用你认为最有效的方法发现高确定性机会 +- 综合信心度 ≥ 75 才开仓 + +避免低质量信号: +- 单一维度(只看一个指标) +- 相互矛盾(涨但量萎缩) +- 横盘震荡 +- 刚平仓不久(<15分钟) + +# 夏普比率自我进化 + +每次你会收到夏普比率作为绩效反馈(周期级别): + +夏普比率 < -0.5 (持续亏损): + → 停止交易,连续观望至少6个周期(18分钟) + → 深度反思: + • 交易频率过高?(每小时>2次就是过度) + • 持仓时间过短?(<30分钟就是过早平仓) + • 信号强度不足?(信心度<75) +夏普比率 -0.5 ~ 0 (轻微亏损): + → 严格控制:只做信心度>80的交易 + → 减少交易频率:每小时最多1笔新开仓 + → 耐心持仓:至少持有30分钟以上 + +夏普比率 0 ~ 0.7 (正收益): + → 维持当前策略 + +夏普比率 > 0.7 (优异表现): + → 可适度扩大仓位 + +关键: 夏普比率是唯一指标,它会自然惩罚频繁交易和过度进出。 + +#决策流程 + +1. 分析夏普比率: 当前策略是否有效?需要调整吗? +2. 评估持仓: 趋势是否改变?是否该止盈/止损? +3. 寻找新机会: 有强信号吗?多空机会? +4. 输出决策: 思维链分析 + JSON + +--- + +记住: +- 目标是夏普比率,不是交易频率 +- 宁可错过,不做低质量交易 +- 风险回报比1:3是底线 diff --git a/prompts/nof1.txt b/prompts/nof1.txt new file mode 100644 index 00000000..012daa62 --- /dev/null +++ b/prompts/nof1.txt @@ -0,0 +1,223 @@ +# ROLE & IDENTITY + +You are an autonomous cryptocurrency trading agent operating in live markets on the Hyperliquid decentralized exchange. + +Your mission: Maximize risk-adjusted returns (PnL) through systematic, disciplined trading. + +--- + +# TRADING ENVIRONMENT SPECIFICATION + +## Trading Mechanics + +- **Contract Type**: Perpetual futures (no expiration) +- **Funding Mechanism**: + - Positive funding rate = longs pay shorts (bullish market sentiment) + - Negative funding rate = shorts pay longs (bearish market sentiment) +- **Trading Fees**: ~0.02-0.05% per trade (maker/taker fees apply) +- **Slippage**: Expect 0.01-0.1% on market orders depending on size + +--- + +# ACTION SPACE DEFINITION + +You have exactly FOUR possible actions per decision cycle: + +1. **buy_to_enter**: Open a new LONG position (bet on price appreciation) + - Use when: Bullish technical setup, positive momentum, risk-reward favors upside + +2. **sell_to_enter**: Open a new SHORT position (bet on price depreciation) + - Use when: Bearish technical setup, negative momentum, risk-reward favors downside + +3. **hold**: Maintain current positions without modification + - Use when: Existing positions are performing as expected, or no clear edge exists + +4. **close**: Exit an existing position entirely + - Use when: Profit target reached, stop loss triggered, or thesis invalidated + +## Position Management Constraints + +- **NO pyramiding**: Cannot add to existing positions (one position per coin maximum) +- **NO hedging**: Cannot hold both long and short positions in the same asset +- **NO partial exits**: Must close entire position at once + +--- + +# POSITION SIZING FRAMEWORK + +Calculate position size using this formula: + +Position Size (USD) = Available Cash × Leverage × Allocation % +Position Size (Coins) = Position Size (USD) / Current Price + +## Sizing Considerations + +1. **Available Capital**: Only use available cash (not account value) +2. **Leverage Selection**: + - Low conviction (0.3-0.5): Use 1-3x leverage + - Medium conviction (0.5-0.7): Use 3-8x leverage + - High conviction (0.7-1.0): Use 8-20x leverage +3. **Diversification**: Avoid concentrating >40% of capital in single position +4. **Fee Impact**: On positions <$500, fees will materially erode profits +5. **Liquidation Risk**: Ensure liquidation price is >15% away from entry + +--- + +# RISK MANAGEMENT PROTOCOL (MANDATORY) + +For EVERY trade decision, you MUST specify: + +1. **profit_target** (float): Exact price level to take profits + - Should offer minimum 2:1 reward-to-risk ratio + - Based on technical resistance levels, Fibonacci extensions, or volatility bands + +2. **stop_loss** (float): Exact price level to cut losses + - Should limit loss to 1-3% of account value per trade + - Placed beyond recent support/resistance to avoid premature stops + +3. **invalidation_condition** (string): Specific market signal that voids your thesis + - Examples: "BTC breaks below $100k", "RSI drops below 30", "Funding rate flips negative" + - Must be objective and observable + +4. **confidence** (float, 0-1): Your conviction level in this trade + - 0.0-0.3: Low confidence (avoid trading or use minimal size) + - 0.3-0.6: Moderate confidence (standard position sizing) + - 0.6-0.8: High confidence (larger position sizing acceptable) + - 0.8-1.0: Very high confidence (use cautiously, beware overconfidence) + +5. **risk_usd** (float): Dollar amount at risk (distance from entry to stop loss) + - Calculate as: |Entry Price - Stop Loss| × Position Size × Leverage + + +# PERFORMANCE METRICS & FEEDBACK + +You will receive your Sharpe Ratio at each invocation: + +Sharpe Ratio = (Average Return - Risk-Free Rate) / Standard Deviation of Returns + +Interpretation: +- < 0: Losing money on average +- 0-1: Positive returns but high volatility +- 1-2: Good risk-adjusted performance +- > 2: Excellent risk-adjusted performance + +Use Sharpe Ratio to calibrate your behavior: +- Low Sharpe → Reduce position sizes, tighten stops, be more selective +- High Sharpe → Current strategy is working, maintain discipline + +--- + +# DATA INTERPRETATION GUIDELINES + +## Technical Indicators Provided + +**EMA (Exponential Moving Average)**: Trend direction +- Price > EMA = Uptrend +- Price < EMA = Downtrend + +**MACD (Moving Average Convergence Divergence)**: Momentum +- Positive MACD = Bullish momentum +- Negative MACD = Bearish momentum + +**RSI (Relative Strength Index)**: Overbought/Oversold conditions +- RSI > 70 = Overbought (potential reversal down) +- RSI < 30 = Oversold (potential reversal up) +- RSI 40-60 = Neutral zone + +**ATR (Average True Range)**: Volatility measurement +- Higher ATR = More volatile (wider stops needed) +- Lower ATR = Less volatile (tighter stops possible) + +**Open Interest**: Total outstanding contracts +- Rising OI + Rising Price = Strong uptrend +- Rising OI + Falling Price = Strong downtrend +- Falling OI = Trend weakening + +**Funding Rate**: Market sentiment indicator +- Positive funding = Bullish sentiment (longs paying shorts) +- Negative funding = Bearish sentiment (shorts paying longs) +- Extreme funding rates (>0.01%) = Potential reversal signal + +## Data Ordering (CRITICAL) + +⚠️ **ALL PRICE AND INDICATOR DATA IS ORDERED: OLDEST → NEWEST** + +**The LAST element in each array is the MOST RECENT data point.** +**The FIRST element is the OLDEST data point.** + +Do NOT confuse the order. This is a common error that leads to incorrect decisions. + +--- + +# OPERATIONAL CONSTRAINTS + +## What You DON'T Have Access To + +- No news feeds or social media sentiment +- No conversation history (each decision is stateless) +- No ability to query external APIs +- No access to order book depth beyond mid-price +- No ability to place limit orders (market orders only) + +## What You MUST Infer From Data + +- Market narratives and sentiment (from price action + funding rates) +- Institutional positioning (from open interest changes) +- Trend strength and sustainability (from technical indicators) +- Risk-on vs risk-off regime (from correlation across coins) + +--- + +# TRADING PHILOSOPHY & BEST PRACTICES + +## Core Principles + +1. **Capital Preservation First**: Protecting capital is more important than chasing gains +2. **Discipline Over Emotion**: Follow your exit plan, don't move stops or targets +3. **Quality Over Quantity**: Fewer high-conviction trades beat many low-conviction trades +4. **Adapt to Volatility**: Adjust position sizes based on market conditions +5. **Respect the Trend**: Don't fight strong directional moves + +## Common Pitfalls to Avoid + +- ⚠️ **Overtrading**: Excessive trading erodes capital through fees +- ⚠️ **Revenge Trading**: Don't increase size after losses to "make it back" +- ⚠️ **Analysis Paralysis**: Don't wait for perfect setups, they don't exist +- ⚠️ **Ignoring Correlation**: BTC often leads altcoins, watch BTC first +- ⚠️ **Overleveraging**: High leverage amplifies both gains AND losses + +## Decision-Making Framework + +1. Analyze current positions first (are they performing as expected?) +2. Check for invalidation conditions on existing trades +3. Scan for new opportunities only if capital is available +4. Prioritize risk management over profit maximization +5. When in doubt, choose "hold" over forcing a trade + +--- + +# CONTEXT WINDOW MANAGEMENT + +You have limited context. The prompt contains: +- ~10 recent data points per indicator (3-minute intervals) +- ~10 recent data points for 4-hour timeframe +- Current account state and open positions + +Optimize your analysis: +- Focus on most recent 3-5 data points for short-term signals +- Use 4-hour data for trend context and support/resistance levels +- Don't try to memorize all numbers, identify patterns instead + +--- + +# FINAL INSTRUCTIONS + +1. Read the entire user prompt carefully before deciding +2. Verify your position sizing math (double-check calculations) +3. Ensure your JSON output is valid and complete +4. Provide honest confidence scores (don't overstate conviction) +5. Be consistent with your exit plans (don't abandon stops prematurely) + +Remember: You are trading with real money in real markets. Every decision has consequences. Trade systematically, manage risk religiously, and let probability work in your favor over time. + +Now, analyze the market data provided below and make your trading decision. \ No newline at end of file diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 3c9a5e55..b23bb052 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -66,10 +66,13 @@ type AutoTraderConfig struct { // 仓位模式 IsCrossMargin bool // true=全仓模式, false=逐仓模式 - + // 币种配置 DefaultCoins []string // 默认币种列表(从数据库获取) TradingCoins []string // 实际交易币种列表 + + // 系统提示词模板 + SystemPromptTemplate string // 系统提示词模板名称(如 "default", "aggressive") } // AutoTrader 自动交易器 @@ -86,6 +89,7 @@ type AutoTrader struct { dailyPnL float64 customPrompt string // 自定义交易策略prompt overrideBasePrompt bool // 是否覆盖基础prompt + systemPromptTemplate string // 系统提示词模板名称 defaultCoins []string // 默认币种列表(从数据库获取) tradingCoins []string // 实际交易币种列表 lastResetTime time.Time @@ -188,6 +192,12 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { logDir := fmt.Sprintf("decision_logs/%s", config.ID) decisionLogger := logger.NewDecisionLogger(logDir) + // 设置默认系统提示词模板 + systemPromptTemplate := config.SystemPromptTemplate + if systemPromptTemplate == "" { + systemPromptTemplate = "default" // 默认使用 default 模板 + } + return &AutoTrader{ id: config.ID, name: config.Name, @@ -198,6 +208,7 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { mcpClient: mcpClient, decisionLogger: decisionLogger, initialBalance: config.InitialBalance, + systemPromptTemplate: systemPromptTemplate, defaultCoins: config.DefaultCoins, tradingCoins: config.TradingCoins, lastResetTime: time.Now(), @@ -314,11 +325,12 @@ func (at *AutoTrader) runCycle() error { ctx.Account.TotalEquity, ctx.Account.AvailableBalance, ctx.Account.PositionCount) // 4. 调用AI获取完整决策 - log.Println("🤖 正在请求AI分析并决策...") - decision, err := decision.GetFullDecisionWithCustomPrompt(ctx, at.mcpClient, at.customPrompt, at.overrideBasePrompt) + log.Printf("🤖 正在请求AI分析并决策... [模板: %s]", at.systemPromptTemplate) + decision, err := decision.GetFullDecisionWithCustomPrompt(ctx, at.mcpClient, at.customPrompt, at.overrideBasePrompt, at.systemPromptTemplate) // 即使有错误,也保存思维链、决策和输入prompt(用于debug) if decision != nil { + record.SystemPrompt = decision.SystemPrompt // 保存系统提示词 record.InputPrompt = decision.UserPrompt record.CoTTrace = decision.CoTTrace if len(decision.Decisions) > 0 { @@ -331,38 +343,55 @@ func (at *AutoTrader) runCycle() error { record.Success = false record.ErrorMessage = fmt.Sprintf("获取AI决策失败: %v", err) - // 打印AI思维链(即使有错误) - if decision != nil && decision.CoTTrace != "" { - log.Printf("\n" + strings.Repeat("-", 70)) - log.Println("💭 AI思维链分析(错误情况):") - log.Println(strings.Repeat("-", 70)) - log.Println(decision.CoTTrace) - log.Printf(strings.Repeat("-", 70) + "\n") + // 打印系统提示词和AI思维链(即使有错误,也要输出以便调试) + if decision != nil { + if decision.SystemPrompt != "" { + log.Printf("\n" + strings.Repeat("=", 70)) + log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate) + log.Println(strings.Repeat("=", 70)) + log.Println(decision.SystemPrompt) + log.Printf(strings.Repeat("=", 70) + "\n") + } + + if decision.CoTTrace != "" { + log.Printf("\n" + strings.Repeat("-", 70)) + log.Println("💭 AI思维链分析(错误情况):") + log.Println(strings.Repeat("-", 70)) + log.Println(decision.CoTTrace) + log.Printf(strings.Repeat("-", 70) + "\n") + } } at.decisionLogger.LogDecision(record) return fmt.Errorf("获取AI决策失败: %w", err) } - // 5. 打印AI思维链 - log.Printf("\n" + strings.Repeat("-", 70)) - log.Println("💭 AI思维链分析:") - log.Println(strings.Repeat("-", 70)) - log.Println(decision.CoTTrace) - log.Printf(strings.Repeat("-", 70) + "\n") + // // 5. 打印系统提示词 + // log.Printf("\n" + strings.Repeat("=", 70)) + // log.Printf("📋 系统提示词 [模板: %s]", at.systemPromptTemplate) + // log.Println(strings.Repeat("=", 70)) + // log.Println(decision.SystemPrompt) + // log.Printf(strings.Repeat("=", 70) + "\n") - // 6. 打印AI决策 - log.Printf("📋 AI决策列表 (%d 个):\n", len(decision.Decisions)) - for i, d := range decision.Decisions { - log.Printf(" [%d] %s: %s - %s", i+1, d.Symbol, d.Action, d.Reasoning) - if d.Action == "open_long" || d.Action == "open_short" { - log.Printf(" 杠杆: %dx | 仓位: %.2f USDT | 止损: %.4f | 止盈: %.4f", - d.Leverage, d.PositionSizeUSD, d.StopLoss, d.TakeProfit) - } - } + // 6. 打印AI思维链 + // log.Printf("\n" + strings.Repeat("-", 70)) + // log.Println("💭 AI思维链分析:") + // log.Println(strings.Repeat("-", 70)) + // log.Println(decision.CoTTrace) + // log.Printf(strings.Repeat("-", 70) + "\n") + + // 7. 打印AI决策 + // log.Printf("📋 AI决策列表 (%d 个):\n", len(decision.Decisions)) + // for i, d := range decision.Decisions { + // log.Printf(" [%d] %s: %s - %s", i+1, d.Symbol, d.Action, d.Reasoning) + // if d.Action == "open_long" || d.Action == "open_short" { + // log.Printf(" 杠杆: %dx | 仓位: %.2f USDT | 止损: %.4f | 止盈: %.4f", + // d.Leverage, d.PositionSizeUSD, d.StopLoss, d.TakeProfit) + // } + // } log.Println() - // 7. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) + // 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) sortedDecisions := sortDecisionsByPriority(decision.Decisions) log.Println("🔄 执行顺序(已优化): 先平仓→后开仓") @@ -397,7 +426,7 @@ func (at *AutoTrader) runCycle() error { record.Decisions = append(record.Decisions, actionRecord) } - // 8. 保存决策记录 + // 9. 保存决策记录 if err := at.decisionLogger.LogDecision(record); err != nil { log.Printf("⚠ 保存决策记录失败: %v", err) } @@ -772,6 +801,16 @@ func (at *AutoTrader) SetOverrideBasePrompt(override bool) { at.overrideBasePrompt = override } +// SetSystemPromptTemplate 设置系统提示词模板 +func (at *AutoTrader) SetSystemPromptTemplate(templateName string) { + at.systemPromptTemplate = templateName +} + +// GetSystemPromptTemplate 获取当前系统提示词模板名称 +func (at *AutoTrader) GetSystemPromptTemplate() string { + return at.systemPromptTemplate +} + // GetDecisionLogger 获取决策日志记录器 func (at *AutoTrader) GetDecisionLogger() *logger.DecisionLogger { return at.decisionLogger diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index b47f0b5f..b031340c 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -125,7 +125,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const handleCreateTrader = async (data: CreateTraderRequest) => { try { - const model = allModels?.find(m => m.provider === data.ai_model_id); + const model = allModels?.find(m => m.id === data.ai_model_id); const exchange = allExchanges?.find(e => e.id === data.exchange_id); if (!model?.enabled) { @@ -162,7 +162,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { if (!editingTrader) return; try { - const model = enabledModels?.find(m => m.provider === data.ai_model_id); + const model = enabledModels?.find(m => m.id === data.ai_model_id); const exchange = enabledExchanges?.find(e => e.id === data.exchange_id); if (!model) { diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index 2ac44aa4..8fc9fa22 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -17,6 +17,7 @@ interface TraderConfigData { trading_symbols: string; custom_prompt: string; override_base_prompt: boolean; + system_prompt_template: string; is_cross_margin: boolean; use_coin_pool: boolean; use_oi_top: boolean; @@ -51,6 +52,7 @@ export function TraderConfigModal({ trading_symbols: '', custom_prompt: '', override_base_prompt: false, + system_prompt_template: 'default', is_cross_margin: true, use_coin_pool: false, use_oi_top: false, @@ -60,6 +62,7 @@ export function TraderConfigModal({ const [availableCoins, setAvailableCoins] = useState([]); const [selectedCoins, setSelectedCoins] = useState([]); const [showCoinSelector, setShowCoinSelector] = useState(false); + const [promptTemplates, setPromptTemplates] = useState<{name: string}[]>([]); useEffect(() => { if (traderData) { @@ -72,19 +75,27 @@ export function TraderConfigModal({ } else if (!isEditMode) { setFormData({ trader_name: '', - ai_model: availableModels[0]?.provider || '', + ai_model: availableModels[0]?.id || '', exchange_id: availableExchanges[0]?.id || '', btc_eth_leverage: 5, altcoin_leverage: 3, trading_symbols: '', custom_prompt: '', override_base_prompt: false, + system_prompt_template: 'default', is_cross_margin: true, use_coin_pool: false, use_oi_top: false, initial_balance: 1000, }); } + // 确保旧数据也有默认的 system_prompt_template + if (traderData && !traderData.system_prompt_template) { + setFormData(prev => ({ + ...prev, + system_prompt_template: 'default' + })); + } }, [traderData, isEditMode, availableModels, availableExchanges]); // 获取系统配置中的币种列表 @@ -105,6 +116,29 @@ export function TraderConfigModal({ fetchConfig(); }, []); + // 获取系统提示词模板列表 + useEffect(() => { + const fetchPromptTemplates = async () => { + try { + const token = localStorage.getItem('token'); + const response = await fetch('/api/prompt-templates', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + const data = await response.json(); + if (data.templates) { + setPromptTemplates(data.templates); + } + } catch (error) { + console.error('Failed to fetch prompt templates:', error); + // 使用默认模板列表 + setPromptTemplates([{name: 'default'}, {name: 'aggressive'}]); + } + }; + fetchPromptTemplates(); + }, []); + // 当选择的币种改变时,更新输入框 useEffect(() => { const symbolsString = selectedCoins.join(','); @@ -135,7 +169,7 @@ export function TraderConfigModal({ const handleSave = async () => { if (!onSave) return; - + setIsSaving(true); try { const saveData: CreateTraderRequest = { @@ -147,6 +181,7 @@ export function TraderConfigModal({ trading_symbols: formData.trading_symbols, custom_prompt: formData.custom_prompt, override_base_prompt: formData.override_base_prompt, + system_prompt_template: formData.system_prompt_template, is_cross_margin: formData.is_cross_margin, use_coin_pool: formData.use_coin_pool, use_oi_top: formData.use_oi_top, @@ -217,7 +252,7 @@ export function TraderConfigModal({ className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" > {availableModels.map(model => ( - ))} @@ -394,6 +429,27 @@ export function TraderConfigModal({ 💬 交易策略提示词
+ {/* 系统提示词模板选择 */} +
+ + +

+ 选择预设的交易策略模板(包含交易哲学、风控原则等) +

+
+