mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 01:06:59 +08:00
refactor: standardize code comments
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -9,102 +9,102 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// PromptTemplate 系统提示词模板
|
||||
// PromptTemplate system prompt template
|
||||
type PromptTemplate struct {
|
||||
Name string // 模板名称(文件名,不含扩展名)
|
||||
Content string // 模板内容
|
||||
Name string // Template name (filename without extension)
|
||||
Content string // Template content
|
||||
}
|
||||
|
||||
// PromptManager 提示词管理器
|
||||
// PromptManager prompt manager
|
||||
type PromptManager struct {
|
||||
templates map[string]*PromptTemplate
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
// globalPromptManager 全局提示词管理器
|
||||
// globalPromptManager global prompt manager
|
||||
globalPromptManager *PromptManager
|
||||
// promptsDir 提示词文件夹路径
|
||||
// promptsDir prompt folder path
|
||||
promptsDir = "prompts"
|
||||
)
|
||||
|
||||
// init 包初始化时加载所有提示词模板
|
||||
// init loads all prompt templates during package initialization
|
||||
func init() {
|
||||
globalPromptManager = NewPromptManager()
|
||||
if err := globalPromptManager.LoadTemplates(promptsDir); err != nil {
|
||||
log.Printf("⚠️ 加载提示词模板失败: %v", err)
|
||||
log.Printf("⚠️ Failed to load prompt templates: %v", err)
|
||||
} else {
|
||||
log.Printf("✓ 已加载 %d 个系统提示词模板", len(globalPromptManager.templates))
|
||||
log.Printf("✓ Loaded %d system prompt templates", len(globalPromptManager.templates))
|
||||
}
|
||||
}
|
||||
|
||||
// NewPromptManager 创建提示词管理器
|
||||
// NewPromptManager creates a prompt manager
|
||||
func NewPromptManager() *PromptManager {
|
||||
return &PromptManager{
|
||||
templates: make(map[string]*PromptTemplate),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadTemplates 从指定目录加载所有提示词模板
|
||||
// LoadTemplates loads all prompt templates from specified directory
|
||||
func (pm *PromptManager) LoadTemplates(dir string) error {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
// 检查目录是否存在
|
||||
// Check if directory exists
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("提示词目录不存在: %s", dir)
|
||||
return fmt.Errorf("prompt directory does not exist: %s", dir)
|
||||
}
|
||||
|
||||
// 扫描目录中的所有 .txt 文件
|
||||
// Scan all .txt files in directory
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*.txt"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("扫描提示词目录失败: %w", err)
|
||||
return fmt.Errorf("failed to scan prompt directory: %w", err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
log.Printf("⚠️ 提示词目录 %s 中没有找到 .txt 文件", dir)
|
||||
log.Printf("⚠️ No .txt files found in prompt directory %s", dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 加载每个模板文件
|
||||
// Load each template file
|
||||
for _, file := range files {
|
||||
// 读取文件内容
|
||||
// Read file content
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 读取提示词文件失败 %s: %v", file, err)
|
||||
log.Printf("⚠️ Failed to read prompt file %s: %v", file, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 提取文件名(不含扩展名)作为模板名称
|
||||
// Extract filename (without extension) as template name
|
||||
fileName := filepath.Base(file)
|
||||
templateName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
||||
|
||||
// 存储模板
|
||||
// Store template
|
||||
pm.templates[templateName] = &PromptTemplate{
|
||||
Name: templateName,
|
||||
Content: string(content),
|
||||
}
|
||||
|
||||
log.Printf(" 📄 加载提示词模板: %s (%s)", templateName, fileName)
|
||||
log.Printf(" 📄 Loaded prompt template: %s (%s)", templateName, fileName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTemplate 获取指定名称的提示词模板
|
||||
// GetTemplate gets prompt template by name
|
||||
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 nil, fmt.Errorf("prompt template does not exist: %s", name)
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// GetAllTemplateNames 获取所有模板名称列表
|
||||
// GetAllTemplateNames gets all template names list
|
||||
func (pm *PromptManager) GetAllTemplateNames() []string {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
@@ -117,7 +117,7 @@ func (pm *PromptManager) GetAllTemplateNames() []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// GetAllTemplates 获取所有模板
|
||||
// GetAllTemplates gets all templates
|
||||
func (pm *PromptManager) GetAllTemplates() []*PromptTemplate {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
@@ -130,7 +130,7 @@ func (pm *PromptManager) GetAllTemplates() []*PromptTemplate {
|
||||
return templates
|
||||
}
|
||||
|
||||
// ReloadTemplates 重新加载所有模板
|
||||
// ReloadTemplates reloads all templates
|
||||
func (pm *PromptManager) ReloadTemplates(dir string) error {
|
||||
pm.mu.Lock()
|
||||
pm.templates = make(map[string]*PromptTemplate)
|
||||
@@ -139,24 +139,24 @@ func (pm *PromptManager) ReloadTemplates(dir string) error {
|
||||
return pm.LoadTemplates(dir)
|
||||
}
|
||||
|
||||
// === 全局函数(供外部调用)===
|
||||
// === Global functions (for external calls) ===
|
||||
|
||||
// GetPromptTemplate 获取指定名称的提示词模板(全局函数)
|
||||
// GetPromptTemplate gets prompt template by name (global function)
|
||||
func GetPromptTemplate(name string) (*PromptTemplate, error) {
|
||||
return globalPromptManager.GetTemplate(name)
|
||||
}
|
||||
|
||||
// GetAllPromptTemplateNames 获取所有模板名称(全局函数)
|
||||
// GetAllPromptTemplateNames gets all template names (global function)
|
||||
func GetAllPromptTemplateNames() []string {
|
||||
return globalPromptManager.GetAllTemplateNames()
|
||||
}
|
||||
|
||||
// GetAllPromptTemplates 获取所有模板(全局函数)
|
||||
// GetAllPromptTemplates gets all templates (global function)
|
||||
func GetAllPromptTemplates() []*PromptTemplate {
|
||||
return globalPromptManager.GetAllTemplates()
|
||||
}
|
||||
|
||||
// ReloadPromptTemplates 重新加载所有模板(全局函数)
|
||||
// ReloadPromptTemplates reloads all templates (global function)
|
||||
func ReloadPromptTemplates() error {
|
||||
return globalPromptManager.ReloadTemplates(promptsDir)
|
||||
}
|
||||
|
||||
@@ -7,49 +7,49 @@ import (
|
||||
)
|
||||
|
||||
func TestPromptManager_LoadTemplates(t *testing.T) {
|
||||
// 创建临时目录用于测试
|
||||
// Create temporary directory for testing
|
||||
tempDir := t.TempDir()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFiles map[string]string // 文件名 -> 内容
|
||||
setupFiles map[string]string // filename -> content
|
||||
expectedCount int
|
||||
expectedNames []string
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "加载单个模板文件",
|
||||
name: "Load single template file",
|
||||
setupFiles: map[string]string{
|
||||
"default.txt": "你是专业的加密货币交易AI。",
|
||||
"default.txt": "You are a professional cryptocurrency trading AI.",
|
||||
},
|
||||
expectedCount: 1,
|
||||
expectedNames: []string{"default"},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "加载多个模板文件",
|
||||
name: "Load multiple template files",
|
||||
setupFiles: map[string]string{
|
||||
"default.txt": "默认策略",
|
||||
"conservative.txt": "保守策略",
|
||||
"aggressive.txt": "激进策略",
|
||||
"default.txt": "Default strategy",
|
||||
"conservative.txt": "Conservative strategy",
|
||||
"aggressive.txt": "Aggressive strategy",
|
||||
},
|
||||
expectedCount: 3,
|
||||
expectedNames: []string{"default", "conservative", "aggressive"},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "空目录",
|
||||
name: "Empty directory",
|
||||
setupFiles: map[string]string{},
|
||||
expectedCount: 0,
|
||||
expectedNames: []string{},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "忽略非.txt文件",
|
||||
name: "Ignore non-.txt files",
|
||||
setupFiles: map[string]string{
|
||||
"default.txt": "正确的模板",
|
||||
"readme.md": "应该被忽略",
|
||||
"config.json": "应该被忽略",
|
||||
"default.txt": "Correct template",
|
||||
"readme.md": "Should be ignored",
|
||||
"config.json": "Should be ignored",
|
||||
},
|
||||
expectedCount: 1,
|
||||
expectedNames: []string{"default"},
|
||||
@@ -59,57 +59,57 @@ func TestPromptManager_LoadTemplates(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 为每个测试用例创建独立的子目录
|
||||
// Create independent subdirectory for each test case
|
||||
testDir := filepath.Join(tempDir, tt.name)
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
t.Fatalf("创建测试目录失败: %v", err)
|
||||
t.Fatalf("Failed to create test directory: %v", err)
|
||||
}
|
||||
|
||||
// 设置测试文件
|
||||
// Setup test files
|
||||
for filename, content := range tt.setupFiles {
|
||||
filePath := filepath.Join(testDir, filename)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("创建测试文件失败 %s: %v", filename, err)
|
||||
t.Fatalf("Failed to create test file %s: %v", filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的 PromptManager
|
||||
// Create new PromptManager
|
||||
pm := NewPromptManager()
|
||||
|
||||
// 执行测试
|
||||
// Execute test
|
||||
err := pm.LoadTemplates(testDir)
|
||||
|
||||
// 检查错误
|
||||
// Check error
|
||||
if (err != nil) != tt.shouldError {
|
||||
t.Errorf("LoadTemplates() error = %v, shouldError %v", err, tt.shouldError)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查加载的模板数量
|
||||
// Check loaded template count
|
||||
if len(pm.templates) != tt.expectedCount {
|
||||
t.Errorf("加载的模板数量 = %d, 期望 %d", len(pm.templates), tt.expectedCount)
|
||||
t.Errorf("Loaded template count = %d, expected %d", len(pm.templates), tt.expectedCount)
|
||||
}
|
||||
|
||||
// 检查模板名称
|
||||
// Check template names
|
||||
for _, expectedName := range tt.expectedNames {
|
||||
if _, exists := pm.templates[expectedName]; !exists {
|
||||
t.Errorf("缺少预期的模板: %s", expectedName)
|
||||
t.Errorf("Missing expected template: %s", expectedName)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证模板内容
|
||||
// Verify template content
|
||||
for filename, expectedContent := range tt.setupFiles {
|
||||
if filepath.Ext(filename) != ".txt" {
|
||||
continue
|
||||
}
|
||||
templateName := filename[:len(filename)-4] // 去掉 .txt
|
||||
templateName := filename[:len(filename)-4] // Remove .txt
|
||||
template, err := pm.GetTemplate(templateName)
|
||||
if err != nil {
|
||||
t.Errorf("获取模板 %s 失败: %v", templateName, err)
|
||||
t.Errorf("Failed to get template %s: %v", templateName, err)
|
||||
continue
|
||||
}
|
||||
if template.Content != expectedContent {
|
||||
t.Errorf("模板内容不匹配\n期望: %s\n实际: %s", expectedContent, template.Content)
|
||||
t.Errorf("Template content mismatch\nExpected: %s\nActual: %s", expectedContent, template.Content)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -121,11 +121,11 @@ func TestPromptManager_GetTemplate(t *testing.T) {
|
||||
pm.templates = map[string]*PromptTemplate{
|
||||
"default": {
|
||||
Name: "default",
|
||||
Content: "默认策略内容",
|
||||
Content: "Default strategy content",
|
||||
},
|
||||
"aggressive": {
|
||||
Name: "aggressive",
|
||||
Content: "激进策略内容",
|
||||
Content: "Aggressive strategy content",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -136,13 +136,13 @@ func TestPromptManager_GetTemplate(t *testing.T) {
|
||||
expectedContent string
|
||||
}{
|
||||
{
|
||||
name: "获取存在的模板",
|
||||
name: "Get existing template",
|
||||
templateName: "default",
|
||||
expectError: false,
|
||||
expectedContent: "默认策略内容",
|
||||
expectedContent: "Default strategy content",
|
||||
},
|
||||
{
|
||||
name: "获取不存在的模板",
|
||||
name: "Get non-existent template",
|
||||
templateName: "nonexistent",
|
||||
expectError: true,
|
||||
},
|
||||
@@ -158,7 +158,7 @@ func TestPromptManager_GetTemplate(t *testing.T) {
|
||||
}
|
||||
|
||||
if !tt.expectError && template.Content != tt.expectedContent {
|
||||
t.Errorf("模板内容 = %s, 期望 %s", template.Content, tt.expectedContent)
|
||||
t.Errorf("Template content = %s, expected %s", template.Content, tt.expectedContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -167,76 +167,76 @@ func TestPromptManager_GetTemplate(t *testing.T) {
|
||||
func TestPromptManager_ReloadTemplates(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始文件
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "default.txt"), []byte("初始内容"), 0644); err != nil {
|
||||
t.Fatalf("创建初始文件失败: %v", err)
|
||||
// Initial file
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "default.txt"), []byte("Initial content"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create initial file: %v", err)
|
||||
}
|
||||
|
||||
pm := NewPromptManager()
|
||||
if err := pm.LoadTemplates(tempDir); err != nil {
|
||||
t.Fatalf("初始加载失败: %v", err)
|
||||
t.Fatalf("Initial load failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证初始内容
|
||||
// Verify initial content
|
||||
template, _ := pm.GetTemplate("default")
|
||||
if template.Content != "初始内容" {
|
||||
t.Errorf("初始内容不正确: %s", template.Content)
|
||||
if template.Content != "Initial content" {
|
||||
t.Errorf("Initial content incorrect: %s", template.Content)
|
||||
}
|
||||
|
||||
// 修改文件内容
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "default.txt"), []byte("更新后内容"), 0644); err != nil {
|
||||
t.Fatalf("更新文件失败: %v", err)
|
||||
// Modify file content
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "default.txt"), []byte("Updated content"), 0644); err != nil {
|
||||
t.Fatalf("Failed to update file: %v", err)
|
||||
}
|
||||
|
||||
// 添加新文件
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "new.txt"), []byte("新模板内容"), 0644); err != nil {
|
||||
t.Fatalf("创建新文件失败: %v", err)
|
||||
// Add new file
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "new.txt"), []byte("New template content"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create new file: %v", err)
|
||||
}
|
||||
|
||||
// 重新加载
|
||||
// Reload
|
||||
if err := pm.ReloadTemplates(tempDir); err != nil {
|
||||
t.Fatalf("重新加载失败: %v", err)
|
||||
t.Fatalf("Reload failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证更新后的内容
|
||||
// Verify updated content
|
||||
template, err := pm.GetTemplate("default")
|
||||
if err != nil {
|
||||
t.Fatalf("获取 default 模板失败: %v", err)
|
||||
t.Fatalf("Failed to get default template: %v", err)
|
||||
}
|
||||
if template.Content != "更新后内容" {
|
||||
t.Errorf("重新加载后内容不正确: got %s, want '更新后内容'", template.Content)
|
||||
if template.Content != "Updated content" {
|
||||
t.Errorf("Content after reload incorrect: got %s, want 'Updated content'", template.Content)
|
||||
}
|
||||
|
||||
// 验证新模板
|
||||
// Verify new template
|
||||
newTemplate, err := pm.GetTemplate("new")
|
||||
if err != nil {
|
||||
t.Fatalf("获取 new 模板失败: %v", err)
|
||||
t.Fatalf("Failed to get new template: %v", err)
|
||||
}
|
||||
if newTemplate.Content != "新模板内容" {
|
||||
t.Errorf("新模板内容不正确: %s", newTemplate.Content)
|
||||
if newTemplate.Content != "New template content" {
|
||||
t.Errorf("New template content incorrect: %s", newTemplate.Content)
|
||||
}
|
||||
|
||||
// 验证模板数量
|
||||
// Verify template count
|
||||
if len(pm.templates) != 2 {
|
||||
t.Errorf("重新加载后模板数量 = %d, 期望 2", len(pm.templates))
|
||||
t.Errorf("Template count after reload = %d, expected 2", len(pm.templates))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptManager_GetAllTemplateNames(t *testing.T) {
|
||||
pm := NewPromptManager()
|
||||
pm.templates = map[string]*PromptTemplate{
|
||||
"default": {Name: "default", Content: "默认策略"},
|
||||
"conservative": {Name: "conservative", Content: "保守策略"},
|
||||
"aggressive": {Name: "aggressive", Content: "激进策略"},
|
||||
"default": {Name: "default", Content: "Default strategy"},
|
||||
"conservative": {Name: "conservative", Content: "Conservative strategy"},
|
||||
"aggressive": {Name: "aggressive", Content: "Aggressive strategy"},
|
||||
}
|
||||
|
||||
names := pm.GetAllTemplateNames()
|
||||
|
||||
if len(names) != 3 {
|
||||
t.Errorf("GetAllTemplateNames() 返回数量 = %d, 期望 3", len(names))
|
||||
t.Errorf("GetAllTemplateNames() returned count = %d, expected 3", len(names))
|
||||
}
|
||||
|
||||
// 验证所有名称都存在
|
||||
// Verify all names exist
|
||||
nameMap := make(map[string]bool)
|
||||
for _, name := range names {
|
||||
nameMap[name] = true
|
||||
@@ -245,41 +245,41 @@ func TestPromptManager_GetAllTemplateNames(t *testing.T) {
|
||||
expectedNames := []string{"default", "conservative", "aggressive"}
|
||||
for _, expectedName := range expectedNames {
|
||||
if !nameMap[expectedName] {
|
||||
t.Errorf("缺少预期的模板名称: %s", expectedName)
|
||||
t.Errorf("Missing expected template name: %s", expectedName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadPromptTemplates_GlobalFunction(t *testing.T) {
|
||||
// 保存原始的 promptsDir
|
||||
// Save original promptsDir
|
||||
originalDir := promptsDir
|
||||
defer func() {
|
||||
promptsDir = originalDir
|
||||
// 恢复原始模板
|
||||
// Restore original templates
|
||||
globalPromptManager.ReloadTemplates(originalDir)
|
||||
}()
|
||||
|
||||
// 创建临时目录
|
||||
// Create temporary directory
|
||||
tempDir := t.TempDir()
|
||||
promptsDir = tempDir
|
||||
|
||||
// 创建测试文件
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "test.txt"), []byte("测试内容"), 0644); err != nil {
|
||||
t.Fatalf("创建测试文件失败: %v", err)
|
||||
// Create test file
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "test.txt"), []byte("Test content"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
// 调用全局重新加载函数
|
||||
// Call global reload function
|
||||
if err := ReloadPromptTemplates(); err != nil {
|
||||
t.Fatalf("ReloadPromptTemplates() 失败: %v", err)
|
||||
t.Fatalf("ReloadPromptTemplates() failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证全局管理器已更新
|
||||
// Verify global manager has been updated
|
||||
template, err := GetPromptTemplate("test")
|
||||
if err != nil {
|
||||
t.Fatalf("获取模板失败: %v", err)
|
||||
t.Fatalf("Failed to get template: %v", err)
|
||||
}
|
||||
|
||||
if template.Content != "测试内容" {
|
||||
t.Errorf("模板内容不正确: got %s, want '测试内容'", template.Content)
|
||||
if template.Content != "Test content" {
|
||||
t.Errorf("Template content incorrect: got %s, want 'Test content'", template.Content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,205 +7,205 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPromptReloadEndToEnd 端到端测试:验证从文件修改到决策引擎使用的完整流程
|
||||
// TestPromptReloadEndToEnd end-to-end test: verify complete flow from file modification to decision engine usage
|
||||
func TestPromptReloadEndToEnd(t *testing.T) {
|
||||
// 保存原始的 promptsDir
|
||||
// Save original promptsDir
|
||||
originalDir := promptsDir
|
||||
defer func() {
|
||||
promptsDir = originalDir
|
||||
// 恢复原始模板
|
||||
// Restore original templates
|
||||
globalPromptManager.ReloadTemplates(originalDir)
|
||||
}()
|
||||
|
||||
// 创建临时目录模拟 prompts/ 目录
|
||||
// Create temporary directory to simulate prompts/ directory
|
||||
tempDir := t.TempDir()
|
||||
promptsDir = tempDir
|
||||
|
||||
// 步骤1: 创建初始 prompt 文件
|
||||
initialContent := "# 初始交易策略\n你是一个保守的交易AI。"
|
||||
// Step 1: Create initial prompt file
|
||||
initialContent := "# Initial Trading Strategy\nYou are a conservative trading AI."
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "test_strategy.txt"), []byte(initialContent), 0644); err != nil {
|
||||
t.Fatalf("创建初始文件失败: %v", err)
|
||||
t.Fatalf("Failed to create initial file: %v", err)
|
||||
}
|
||||
|
||||
// 步骤2: 首次加载(模拟系统启动)
|
||||
// Step 2: First load (simulate system startup)
|
||||
if err := ReloadPromptTemplates(); err != nil {
|
||||
t.Fatalf("首次加载失败: %v", err)
|
||||
t.Fatalf("First load failed: %v", err)
|
||||
}
|
||||
|
||||
// 步骤3: 验证初始内容
|
||||
// Step 3: Verify initial content
|
||||
template, err := GetPromptTemplate("test_strategy")
|
||||
if err != nil {
|
||||
t.Fatalf("获取初始模板失败: %v", err)
|
||||
t.Fatalf("Failed to get initial template: %v", err)
|
||||
}
|
||||
if template.Content != initialContent {
|
||||
t.Errorf("初始内容不匹配\n期望: %s\n实际: %s", initialContent, template.Content)
|
||||
t.Errorf("Initial content mismatch\nExpected: %s\nActual: %s", initialContent, template.Content)
|
||||
}
|
||||
|
||||
// 步骤4: 使用 buildSystemPrompt 验证模板被正确使用
|
||||
// Step 4: Use buildSystemPrompt to verify template is correctly used
|
||||
systemPrompt := buildSystemPrompt(10000.0, 10, 5, "test_strategy", "")
|
||||
if !strings.Contains(systemPrompt, initialContent) {
|
||||
t.Errorf("buildSystemPrompt 未包含模板内容\n生成的 prompt:\n%s", systemPrompt)
|
||||
t.Errorf("buildSystemPrompt doesn't contain template content\nGenerated prompt:\n%s", systemPrompt)
|
||||
}
|
||||
|
||||
// 步骤5: 模拟用户修改文件(这是用户在硬盘上修改 prompt)
|
||||
updatedContent := "# 更新的交易策略\n你是一个激进的交易AI,追求高风险高收益。"
|
||||
// Step 5: Simulate user modifying file (user modifies prompt on disk)
|
||||
updatedContent := "# Updated Trading Strategy\nYou are an aggressive trading AI seeking high risk and high reward."
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "test_strategy.txt"), []byte(updatedContent), 0644); err != nil {
|
||||
t.Fatalf("更新文件失败: %v", err)
|
||||
t.Fatalf("Failed to update file: %v", err)
|
||||
}
|
||||
|
||||
// 步骤6: 模拟交易员启动时调用 ReloadPromptTemplates()
|
||||
t.Log("模拟交易员启动,调用 ReloadPromptTemplates()...")
|
||||
// Step 6: Simulate trader startup calling ReloadPromptTemplates()
|
||||
t.Log("Simulating trader startup, calling ReloadPromptTemplates()...")
|
||||
if err := ReloadPromptTemplates(); err != nil {
|
||||
t.Fatalf("重新加载失败: %v", err)
|
||||
t.Fatalf("Reload failed: %v", err)
|
||||
}
|
||||
|
||||
// 步骤7: 验证新内容已生效
|
||||
// Step 7: Verify new content has taken effect
|
||||
reloadedTemplate, err := GetPromptTemplate("test_strategy")
|
||||
if err != nil {
|
||||
t.Fatalf("获取重新加载的模板失败: %v", err)
|
||||
t.Fatalf("Failed to get reloaded template: %v", err)
|
||||
}
|
||||
if reloadedTemplate.Content != updatedContent {
|
||||
t.Errorf("重新加载后内容不匹配\n期望: %s\n实际: %s", updatedContent, reloadedTemplate.Content)
|
||||
t.Errorf("Content mismatch after reload\nExpected: %s\nActual: %s", updatedContent, reloadedTemplate.Content)
|
||||
}
|
||||
|
||||
// 步骤8: 验证 buildSystemPrompt 使用了新内容
|
||||
// Step 8: Verify buildSystemPrompt uses new content
|
||||
newSystemPrompt := buildSystemPrompt(10000.0, 10, 5, "test_strategy", "")
|
||||
if !strings.Contains(newSystemPrompt, updatedContent) {
|
||||
t.Errorf("buildSystemPrompt 未包含更新后的模板内容\n生成的 prompt:\n%s", newSystemPrompt)
|
||||
t.Errorf("buildSystemPrompt doesn't contain updated template content\nGenerated prompt:\n%s", newSystemPrompt)
|
||||
}
|
||||
|
||||
// 步骤9: 验证旧内容不再存在
|
||||
if strings.Contains(newSystemPrompt, "保守的交易AI") {
|
||||
t.Errorf("buildSystemPrompt 仍包含旧的模板内容")
|
||||
// Step 9: Verify old content no longer exists
|
||||
if strings.Contains(newSystemPrompt, "conservative trading AI") {
|
||||
t.Errorf("buildSystemPrompt still contains old template content")
|
||||
}
|
||||
|
||||
t.Log("✅ 端到端测试通过:文件修改 -> 重新加载 -> 决策引擎使用新内容")
|
||||
t.Log("✅ End-to-end test passed: file modification -> reload -> decision engine uses new content")
|
||||
}
|
||||
|
||||
// TestPromptReloadWithCustomPrompt 测试自定义 prompt 与模板重新加载的交互
|
||||
// TestPromptReloadWithCustomPrompt tests interaction between custom prompt and template reload
|
||||
func TestPromptReloadWithCustomPrompt(t *testing.T) {
|
||||
// 保存原始的 promptsDir
|
||||
// Save original promptsDir
|
||||
originalDir := promptsDir
|
||||
defer func() {
|
||||
promptsDir = originalDir
|
||||
globalPromptManager.ReloadTemplates(originalDir)
|
||||
}()
|
||||
|
||||
// 创建临时目录
|
||||
// Create temporary directory
|
||||
tempDir := t.TempDir()
|
||||
promptsDir = tempDir
|
||||
|
||||
// 创建基础模板
|
||||
baseContent := "基础策略:稳健交易"
|
||||
// Create base template
|
||||
baseContent := "Base strategy: Stable trading"
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "base.txt"), []byte(baseContent), 0644); err != nil {
|
||||
t.Fatalf("创建文件失败: %v", err)
|
||||
t.Fatalf("Failed to create file: %v", err)
|
||||
}
|
||||
|
||||
// 加载模板
|
||||
// Load templates
|
||||
if err := ReloadPromptTemplates(); err != nil {
|
||||
t.Fatalf("加载失败: %v", err)
|
||||
t.Fatalf("Load failed: %v", err)
|
||||
}
|
||||
|
||||
// 测试1: 基础模板 + 自定义 prompt(不覆盖)
|
||||
customPrompt := "个性化规则:只交易 BTC"
|
||||
// Test 1: Base template + custom prompt (no override)
|
||||
customPrompt := "Personalized rule: Only trade BTC"
|
||||
result := buildSystemPromptWithCustom(10000.0, 10, 5, customPrompt, false, "base", "")
|
||||
if !strings.Contains(result, baseContent) {
|
||||
t.Errorf("未包含基础模板内容")
|
||||
t.Errorf("Doesn't contain base template content")
|
||||
}
|
||||
if !strings.Contains(result, customPrompt) {
|
||||
t.Errorf("未包含自定义 prompt")
|
||||
t.Errorf("Doesn't contain custom prompt")
|
||||
}
|
||||
|
||||
// 测试2: 覆盖基础 prompt
|
||||
// Test 2: Override base prompt
|
||||
result = buildSystemPromptWithCustom(10000.0, 10, 5, customPrompt, true, "base", "")
|
||||
if strings.Contains(result, baseContent) {
|
||||
t.Errorf("覆盖模式下仍包含基础模板内容")
|
||||
t.Errorf("Override mode still contains base template content")
|
||||
}
|
||||
if !strings.Contains(result, customPrompt) {
|
||||
t.Errorf("覆盖模式下未包含自定义 prompt")
|
||||
t.Errorf("Override mode doesn't contain custom prompt")
|
||||
}
|
||||
|
||||
// 测试3: 重新加载后效果
|
||||
updatedBase := "更新的基础策略:激进交易"
|
||||
// Test 3: Effect after reload
|
||||
updatedBase := "Updated base strategy: Aggressive trading"
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "base.txt"), []byte(updatedBase), 0644); err != nil {
|
||||
t.Fatalf("更新文件失败: %v", err)
|
||||
t.Fatalf("Failed to update file: %v", err)
|
||||
}
|
||||
|
||||
if err := ReloadPromptTemplates(); err != nil {
|
||||
t.Fatalf("重新加载失败: %v", err)
|
||||
t.Fatalf("Reload failed: %v", err)
|
||||
}
|
||||
|
||||
result = buildSystemPromptWithCustom(10000.0, 10, 5, customPrompt, false, "base", "")
|
||||
if !strings.Contains(result, updatedBase) {
|
||||
t.Errorf("重新加载后未包含更新的基础模板内容")
|
||||
t.Errorf("After reload doesn't contain updated base template content")
|
||||
}
|
||||
if strings.Contains(result, baseContent) {
|
||||
t.Errorf("重新加载后仍包含旧的基础模板内容")
|
||||
t.Errorf("After reload still contains old base template content")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPromptReloadFallback 测试模板不存在时的降级机制
|
||||
// TestPromptReloadFallback tests fallback mechanism when template doesn't exist
|
||||
func TestPromptReloadFallback(t *testing.T) {
|
||||
// 保存原始的 promptsDir
|
||||
// Save original promptsDir
|
||||
originalDir := promptsDir
|
||||
defer func() {
|
||||
promptsDir = originalDir
|
||||
globalPromptManager.ReloadTemplates(originalDir)
|
||||
}()
|
||||
|
||||
// 创建临时目录
|
||||
// Create temporary directory
|
||||
tempDir := t.TempDir()
|
||||
promptsDir = tempDir
|
||||
|
||||
// 只创建 default 模板
|
||||
defaultContent := "默认策略"
|
||||
// Only create default template
|
||||
defaultContent := "Default strategy"
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "default.txt"), []byte(defaultContent), 0644); err != nil {
|
||||
t.Fatalf("创建文件失败: %v", err)
|
||||
t.Fatalf("Failed to create file: %v", err)
|
||||
}
|
||||
|
||||
if err := ReloadPromptTemplates(); err != nil {
|
||||
t.Fatalf("加载失败: %v", err)
|
||||
t.Fatalf("Load failed: %v", err)
|
||||
}
|
||||
|
||||
// 测试1: 请求不存在的模板,应该降级到 default
|
||||
// Test 1: Request non-existent template, should fall back to default
|
||||
result := buildSystemPrompt(10000.0, 10, 5, "nonexistent", "")
|
||||
if !strings.Contains(result, defaultContent) {
|
||||
t.Errorf("请求不存在的模板时,未降级到 default")
|
||||
t.Errorf("When requesting non-existent template, didn't fall back to default")
|
||||
}
|
||||
|
||||
// 测试2: 空模板名,应该使用 default
|
||||
// Test 2: Empty template name, should use default
|
||||
result = buildSystemPrompt(10000.0, 10, 5, "", "")
|
||||
if !strings.Contains(result, defaultContent) {
|
||||
t.Errorf("空模板名时,未使用 default")
|
||||
t.Errorf("With empty template name, didn't use default")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentPromptReload 测试并发场景下的 prompt 重新加载
|
||||
// TestConcurrentPromptReload tests prompt reload in concurrent scenarios
|
||||
func TestConcurrentPromptReload(t *testing.T) {
|
||||
// 保存原始的 promptsDir
|
||||
// Save original promptsDir
|
||||
originalDir := promptsDir
|
||||
defer func() {
|
||||
promptsDir = originalDir
|
||||
globalPromptManager.ReloadTemplates(originalDir)
|
||||
}()
|
||||
|
||||
// 创建临时目录
|
||||
// Create temporary directory
|
||||
tempDir := t.TempDir()
|
||||
promptsDir = tempDir
|
||||
|
||||
// 创建测试文件
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "test.txt"), []byte("测试内容"), 0644); err != nil {
|
||||
t.Fatalf("创建文件失败: %v", err)
|
||||
// Create test file
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "test.txt"), []byte("Test content"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create file: %v", err)
|
||||
}
|
||||
|
||||
if err := ReloadPromptTemplates(); err != nil {
|
||||
t.Fatalf("初始加载失败: %v", err)
|
||||
t.Fatalf("Initial load failed: %v", err)
|
||||
}
|
||||
|
||||
// 并发测试:同时读取和重新加载
|
||||
// Concurrent test: read and reload simultaneously
|
||||
done := make(chan bool)
|
||||
|
||||
// 启动多个读取 goroutine
|
||||
// Start multiple read goroutines
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
for j := 0; j < 100; j++ {
|
||||
@@ -215,7 +215,7 @@ func TestConcurrentPromptReload(t *testing.T) {
|
||||
}()
|
||||
}
|
||||
|
||||
// 启动多个重新加载 goroutine
|
||||
// Start multiple reload goroutines
|
||||
for i := 0; i < 3; i++ {
|
||||
go func() {
|
||||
for j := 0; j < 10; j++ {
|
||||
@@ -225,19 +225,19 @@ func TestConcurrentPromptReload(t *testing.T) {
|
||||
}()
|
||||
}
|
||||
|
||||
// 等待所有 goroutine 完成
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 13; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// 验证最终状态正确
|
||||
// Verify final state is correct
|
||||
template, err := GetPromptTemplate("test")
|
||||
if err != nil {
|
||||
t.Errorf("并发测试后获取模板失败: %v", err)
|
||||
t.Errorf("Failed to get template after concurrent test: %v", err)
|
||||
}
|
||||
if template.Content != "测试内容" {
|
||||
t.Errorf("并发测试后模板内容错误: %s", template.Content)
|
||||
if template.Content != "Test content" {
|
||||
t.Errorf("Template content error after concurrent test: %s", template.Content)
|
||||
}
|
||||
|
||||
t.Log("✅ 并发测试通过:多个 goroutine 同时读取和重新加载模板,无数据竞争")
|
||||
t.Log("✅ Concurrent test passed: multiple goroutines reading and reloading templates simultaneously, no data race")
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestBuildSystemPrompt_ContainsAllValidActions 测试 prompt 是否包含所有有效的 action
|
||||
// TestBuildSystemPrompt_ContainsAllValidActions tests whether prompt contains all valid actions
|
||||
func TestBuildSystemPrompt_ContainsAllValidActions(t *testing.T) {
|
||||
// 这是系统中定义的所有有效 action(来自 validateDecision)
|
||||
// These are all valid actions defined in the system (from validateDecision)
|
||||
validActions := []string{
|
||||
"open_long",
|
||||
"open_short",
|
||||
@@ -17,13 +17,13 @@ func TestBuildSystemPrompt_ContainsAllValidActions(t *testing.T) {
|
||||
"wait",
|
||||
}
|
||||
|
||||
// 构建 prompt
|
||||
// Build prompt
|
||||
prompt := buildSystemPrompt(1000.0, 10, 5, "default", "")
|
||||
|
||||
// 验证每个有效 action 都在 prompt 中出现
|
||||
// Verify each valid action appears in prompt
|
||||
for _, action := range validActions {
|
||||
if !strings.Contains(prompt, action) {
|
||||
t.Errorf("Prompt 缺少有效的 action: %s", action)
|
||||
t.Errorf("Prompt missing valid action: %s", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,37 +13,37 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// StrategyEngine 策略执行引擎
|
||||
// 负责基于策略配置动态获取数据和组装 Prompt
|
||||
// StrategyEngine strategy execution engine
|
||||
// Responsible for dynamically fetching data and assembling prompts based on strategy configuration
|
||||
type StrategyEngine struct {
|
||||
config *store.StrategyConfig
|
||||
}
|
||||
|
||||
// NewStrategyEngine 创建策略执行引擎
|
||||
// NewStrategyEngine creates strategy execution engine
|
||||
func NewStrategyEngine(config *store.StrategyConfig) *StrategyEngine {
|
||||
return &StrategyEngine{config: config}
|
||||
}
|
||||
|
||||
// GetCandidateCoins 根据策略配置获取候选币种
|
||||
// GetCandidateCoins gets candidate coins based on strategy configuration
|
||||
func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
var candidates []CandidateCoin
|
||||
symbolSources := make(map[string][]string)
|
||||
|
||||
coinSource := e.config.CoinSource
|
||||
|
||||
// 设置自定义的 API URL(如果配置了)
|
||||
// Set custom API URL (if configured)
|
||||
if coinSource.CoinPoolAPIURL != "" {
|
||||
pool.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
logger.Infof("✓ 使用策略配置的 AI500 API URL: %s", coinSource.CoinPoolAPIURL)
|
||||
logger.Infof("✓ Using strategy-configured AI500 API URL: %s", coinSource.CoinPoolAPIURL)
|
||||
}
|
||||
if coinSource.OITopAPIURL != "" {
|
||||
pool.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
logger.Infof("✓ 使用策略配置的 OI Top API URL: %s", coinSource.OITopAPIURL)
|
||||
logger.Infof("✓ Using strategy-configured OI Top API URL: %s", coinSource.OITopAPIURL)
|
||||
}
|
||||
|
||||
switch coinSource.SourceType {
|
||||
case "static":
|
||||
// 静态币种列表
|
||||
// Static coin list
|
||||
for _, symbol := range coinSource.StaticCoins {
|
||||
symbol = market.Normalize(symbol)
|
||||
candidates = append(candidates, CandidateCoin{
|
||||
@@ -54,19 +54,19 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
return candidates, nil
|
||||
|
||||
case "coinpool":
|
||||
// 仅使用 AI500 币种池
|
||||
// Use AI500 coin pool only
|
||||
return e.getCoinPoolCoins(coinSource.CoinPoolLimit)
|
||||
|
||||
case "oi_top":
|
||||
// 仅使用 OI Top
|
||||
// Use OI Top only
|
||||
return e.getOITopCoins(coinSource.OITopLimit)
|
||||
|
||||
case "mixed":
|
||||
// 混合模式:AI500 + OI Top
|
||||
// Mixed mode: AI500 + OI Top
|
||||
if coinSource.UseCoinPool {
|
||||
poolCoins, err := e.getCoinPoolCoins(coinSource.CoinPoolLimit)
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取 AI500 币种池失败: %v", err)
|
||||
logger.Infof("⚠️ Failed to get AI500 coin pool: %v", err)
|
||||
} else {
|
||||
for _, coin := range poolCoins {
|
||||
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "ai500")
|
||||
@@ -77,7 +77,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
if coinSource.UseOITop {
|
||||
oiCoins, err := e.getOITopCoins(coinSource.OITopLimit)
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取 OI Top 失败: %v", err)
|
||||
logger.Infof("⚠️ Failed to get OI Top: %v", err)
|
||||
} else {
|
||||
for _, coin := range oiCoins {
|
||||
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "oi_top")
|
||||
@@ -85,7 +85,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加静态币种(如果有)
|
||||
// Add static coins (if any)
|
||||
for _, symbol := range coinSource.StaticCoins {
|
||||
symbol = market.Normalize(symbol)
|
||||
if _, exists := symbolSources[symbol]; !exists {
|
||||
@@ -95,7 +95,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为候选币种列表
|
||||
// Convert to candidate coin list
|
||||
for symbol, sources := range symbolSources {
|
||||
candidates = append(candidates, CandidateCoin{
|
||||
Symbol: symbol,
|
||||
@@ -105,11 +105,11 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
return candidates, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("未知的币种来源类型: %s", coinSource.SourceType)
|
||||
return nil, fmt.Errorf("unknown coin source type: %s", coinSource.SourceType)
|
||||
}
|
||||
}
|
||||
|
||||
// getCoinPoolCoins 获取 AI500 币种池
|
||||
// getCoinPoolCoins gets AI500 coin pool
|
||||
func (e *StrategyEngine) getCoinPoolCoins(limit int) ([]CandidateCoin, error) {
|
||||
if limit <= 0 {
|
||||
limit = 30
|
||||
@@ -130,7 +130,7 @@ func (e *StrategyEngine) getCoinPoolCoins(limit int) ([]CandidateCoin, error) {
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
// getOITopCoins 获取 OI Top 币种
|
||||
// getOITopCoins gets OI Top coins
|
||||
func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
@@ -155,20 +155,20 @@ func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
// FetchMarketData 根据策略配置获取市场数据
|
||||
// FetchMarketData fetches market data based on strategy configuration
|
||||
func (e *StrategyEngine) FetchMarketData(symbol string) (*market.Data, error) {
|
||||
// 目前使用现有的 market.Get,后续可以根据策略配置自定义
|
||||
// Currently using existing market.Get, can be customized based on strategy configuration later
|
||||
return market.Get(symbol)
|
||||
}
|
||||
|
||||
// FetchExternalData 获取外部数据源
|
||||
// FetchExternalData fetches external data sources
|
||||
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)
|
||||
logger.Infof("⚠️ Failed to fetch external data source [%s]: %v", source.Name, err)
|
||||
continue
|
||||
}
|
||||
externalData[source.Name] = data
|
||||
@@ -177,7 +177,7 @@ func (e *StrategyEngine) FetchExternalData() (map[string]interface{}, error) {
|
||||
return externalData, nil
|
||||
}
|
||||
|
||||
// QuantData 量化数据结构(资金流向、持仓变化、价格变化)
|
||||
// QuantData quantitative data structure (fund flow, position changes, price changes)
|
||||
type QuantData struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Price float64 `json:"price"`
|
||||
@@ -209,49 +209,49 @@ type OIDeltaData struct {
|
||||
OIDeltaPercent float64 `json:"oi_delta_percent"`
|
||||
}
|
||||
|
||||
// FetchQuantData 获取单个币种的量化数据
|
||||
// FetchQuantData fetches quantitative data for a single coin
|
||||
func (e *StrategyEngine) FetchQuantData(symbol string) (*QuantData, error) {
|
||||
if !e.config.Indicators.EnableQuantData || e.config.Indicators.QuantDataAPIURL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 替换 {symbol} 占位符
|
||||
// Replace {symbol} placeholder
|
||||
url := strings.Replace(e.config.Indicators.QuantDataAPIURL, "{symbol}", symbol, -1)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %w", err)
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP状态码: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("HTTP status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
// Parse response
|
||||
var apiResp struct {
|
||||
Code int `json:"code"`
|
||||
Data *QuantData `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("解析JSON失败: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
||||
}
|
||||
|
||||
if apiResp.Code != 0 {
|
||||
return nil, fmt.Errorf("API返回错误码: %d", apiResp.Code)
|
||||
return nil, fmt.Errorf("API returned error code: %d", apiResp.Code)
|
||||
}
|
||||
|
||||
return apiResp.Data, nil
|
||||
}
|
||||
|
||||
// FetchQuantDataBatch 批量获取量化数据
|
||||
// FetchQuantDataBatch batch fetches quantitative data
|
||||
func (e *StrategyEngine) FetchQuantDataBatch(symbols []string) map[string]*QuantData {
|
||||
result := make(map[string]*QuantData)
|
||||
|
||||
@@ -262,7 +262,7 @@ func (e *StrategyEngine) FetchQuantDataBatch(symbols []string) map[string]*Quant
|
||||
for _, symbol := range symbols {
|
||||
data, err := e.FetchQuantData(symbol)
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取 %s 量化数据失败: %v", symbol, err)
|
||||
logger.Infof("⚠️ Failed to fetch quantitative data for %s: %v", symbol, err)
|
||||
continue
|
||||
}
|
||||
if data != nil {
|
||||
@@ -273,18 +273,18 @@ func (e *StrategyEngine) FetchQuantDataBatch(symbols []string) map[string]*Quant
|
||||
return result
|
||||
}
|
||||
|
||||
// formatQuantData 格式化量化数据
|
||||
// formatQuantData formats quantitative data
|
||||
func (e *StrategyEngine) formatQuantData(data *QuantData) string {
|
||||
if data == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("📊 量化数据:\n")
|
||||
sb.WriteString("📊 Quantitative Data:\n")
|
||||
|
||||
// 价格变化
|
||||
// Price changes
|
||||
if len(data.PriceChange) > 0 {
|
||||
sb.WriteString("价格变化: ")
|
||||
sb.WriteString("Price Change: ")
|
||||
timeframes := []string{"5m", "15m", "1h", "4h", "24h"}
|
||||
parts := []string{}
|
||||
for _, tf := range timeframes {
|
||||
@@ -296,14 +296,14 @@ func (e *StrategyEngine) formatQuantData(data *QuantData) string {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// 资金流向
|
||||
// Fund flow
|
||||
if data.Netflow != nil {
|
||||
sb.WriteString("资金流向(USDT):\n")
|
||||
sb.WriteString("Fund Flow (USDT):\n")
|
||||
|
||||
// 机构资金
|
||||
// Institutional funds
|
||||
if data.Netflow.Institution != nil {
|
||||
if data.Netflow.Institution.Future != nil {
|
||||
sb.WriteString(" 机构合约: ")
|
||||
sb.WriteString(" Institutional Futures: ")
|
||||
parts := []string{}
|
||||
for _, tf := range []string{"1h", "4h", "24h"} {
|
||||
if v, ok := data.Netflow.Institution.Future[tf]; ok {
|
||||
@@ -314,7 +314,7 @@ func (e *StrategyEngine) formatQuantData(data *QuantData) string {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
if data.Netflow.Institution.Spot != nil {
|
||||
sb.WriteString(" 机构现货: ")
|
||||
sb.WriteString(" Institutional Spot: ")
|
||||
parts := []string{}
|
||||
for _, tf := range []string{"1h", "4h", "24h"} {
|
||||
if v, ok := data.Netflow.Institution.Spot[tf]; ok {
|
||||
@@ -326,10 +326,10 @@ func (e *StrategyEngine) formatQuantData(data *QuantData) string {
|
||||
}
|
||||
}
|
||||
|
||||
// 散户资金
|
||||
// Retail funds
|
||||
if data.Netflow.Personal != nil {
|
||||
if data.Netflow.Personal.Future != nil {
|
||||
sb.WriteString(" 散户合约: ")
|
||||
sb.WriteString(" Retail Futures: ")
|
||||
parts := []string{}
|
||||
for _, tf := range []string{"1h", "4h", "24h"} {
|
||||
if v, ok := data.Netflow.Personal.Future[tf]; ok {
|
||||
@@ -342,13 +342,13 @@ func (e *StrategyEngine) formatQuantData(data *QuantData) string {
|
||||
}
|
||||
}
|
||||
|
||||
// 持仓数据
|
||||
// Position data
|
||||
if len(data.OI) > 0 {
|
||||
for exchange, oiData := range data.OI {
|
||||
sb.WriteString(fmt.Sprintf("持仓(%s): 当前%.2f | 多%.2f 空%.2f\n",
|
||||
sb.WriteString(fmt.Sprintf("Open Interest (%s): Current %.2f | Long %.2f Short %.2f\n",
|
||||
exchange, oiData.CurrentOI, oiData.NetLong, oiData.NetShort))
|
||||
if len(oiData.Delta) > 0 {
|
||||
sb.WriteString(" 持仓变化: ")
|
||||
sb.WriteString(" OI Change: ")
|
||||
parts := []string{}
|
||||
for _, tf := range []string{"1h", "4h", "24h"} {
|
||||
if d, ok := oiData.Delta[tf]; ok {
|
||||
@@ -364,7 +364,7 @@ func (e *StrategyEngine) formatQuantData(data *QuantData) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// fetchSingleExternalSource 获取单个外部数据源
|
||||
// fetchSingleExternalSource fetches a single external data source
|
||||
func (e *StrategyEngine) fetchSingleExternalSource(source store.ExternalDataSource) (interface{}, error) {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(source.RefreshSecs) * time.Second,
|
||||
@@ -379,7 +379,7 @@ func (e *StrategyEngine) fetchSingleExternalSource(source store.ExternalDataSour
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 添加请求头
|
||||
// Add request headers
|
||||
for k, v := range source.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
@@ -400,7 +400,7 @@ func (e *StrategyEngine) fetchSingleExternalSource(source store.ExternalDataSour
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果指定了数据路径,提取指定路径的数据
|
||||
// If data path is specified, extract data at specified path
|
||||
if source.DataPath != "" {
|
||||
result = extractJSONPath(result, source.DataPath)
|
||||
}
|
||||
@@ -408,7 +408,7 @@ func (e *StrategyEngine) fetchSingleExternalSource(source store.ExternalDataSour
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractJSONPath 提取 JSON 路径数据(简单实现)
|
||||
// extractJSONPath extracts JSON path data (simple implementation)
|
||||
func extractJSONPath(data interface{}, path string) interface{} {
|
||||
parts := strings.Split(path, ".")
|
||||
current := data
|
||||
@@ -424,23 +424,23 @@ func extractJSONPath(data interface{}, path string) interface{} {
|
||||
return current
|
||||
}
|
||||
|
||||
// BuildUserPrompt 根据策略配置构建 User Prompt
|
||||
// BuildUserPrompt builds User Prompt based on strategy configuration
|
||||
func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// 系统状态
|
||||
sb.WriteString(fmt.Sprintf("时间: %s | 周期: #%d | 运行: %d分钟\n\n",
|
||||
// System status
|
||||
sb.WriteString(fmt.Sprintf("Time: %s | Period: #%d | Runtime: %d minutes\n\n",
|
||||
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes))
|
||||
|
||||
// BTC 市场(如果配置了)
|
||||
// BTC market (if configured)
|
||||
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",
|
||||
// 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,
|
||||
@@ -448,40 +448,40 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
||||
ctx.Account.MarginUsedPct,
|
||||
ctx.Account.PositionCount))
|
||||
|
||||
// 持仓信息
|
||||
// Position information
|
||||
if len(ctx.Positions) > 0 {
|
||||
sb.WriteString("## 当前持仓\n")
|
||||
sb.WriteString("## Current Positions\n")
|
||||
for i, pos := range ctx.Positions {
|
||||
sb.WriteString(e.formatPositionInfo(i+1, pos, ctx))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString("当前持仓: 无\n\n")
|
||||
sb.WriteString("Current Positions: None\n\n")
|
||||
}
|
||||
|
||||
// 交易统计
|
||||
// Trading statistics
|
||||
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
|
||||
sb.WriteString("## 历史交易统计\n")
|
||||
sb.WriteString(fmt.Sprintf("总交易数: %d | 胜率: %.1f%% | 盈亏比: %.2f | 夏普比: %.2f\n",
|
||||
sb.WriteString("## Historical Trading Statistics\n")
|
||||
sb.WriteString(fmt.Sprintf("Total Trades: %d | Win Rate: %.1f%% | Profit Factor: %.2f | Sharpe Ratio: %.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",
|
||||
sb.WriteString(fmt.Sprintf("Total P&L: %.2f USDT | Avg Win: %.2f | Avg Loss: %.2f | Max Drawdown: %.1f%%\n\n",
|
||||
ctx.TradingStats.TotalPnL,
|
||||
ctx.TradingStats.AvgWin,
|
||||
ctx.TradingStats.AvgLoss,
|
||||
ctx.TradingStats.MaxDrawdownPct))
|
||||
}
|
||||
|
||||
// 最近完成的订单
|
||||
// Recently completed orders
|
||||
if len(ctx.RecentOrders) > 0 {
|
||||
sb.WriteString("## 最近完成的交易\n")
|
||||
sb.WriteString("## Recent Completed Trades\n")
|
||||
for i, order := range ctx.RecentOrders {
|
||||
resultStr := "盈利"
|
||||
resultStr := "Profit"
|
||||
if order.RealizedPnL < 0 {
|
||||
resultStr = "亏损"
|
||||
resultStr = "Loss"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%d. %s %s | 入场%.4f 出场%.4f | %s: %+.2f USDT (%+.2f%%) | %s\n",
|
||||
sb.WriteString(fmt.Sprintf("%d. %s %s | Entry %.4f Exit %.4f | %s: %+.2f USDT (%+.2f%%) | %s\n",
|
||||
i+1, order.Symbol, order.Side,
|
||||
order.EntryPrice, order.ExitPrice,
|
||||
resultStr, order.RealizedPnL, order.PnLPct,
|
||||
@@ -490,8 +490,8 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// 候选币种
|
||||
sb.WriteString(fmt.Sprintf("## 候选币种 (%d个)\n\n", len(ctx.MarketDataMap)))
|
||||
// Candidate coins
|
||||
sb.WriteString(fmt.Sprintf("## Candidate Coins (%d coins)\n\n", len(ctx.MarketDataMap)))
|
||||
displayedCount := 0
|
||||
for _, coin := range ctx.CandidateCoins {
|
||||
marketData, hasData := ctx.MarketDataMap[coin.Symbol]
|
||||
@@ -504,7 +504,7 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
||||
sb.WriteString(fmt.Sprintf("### %d. %s%s\n\n", displayedCount, coin.Symbol, sourceTags))
|
||||
sb.WriteString(e.formatMarketData(marketData))
|
||||
|
||||
// 添加量化数据(如果有)
|
||||
// Add quantitative data if available
|
||||
if ctx.QuantDataMap != nil {
|
||||
if quantData, hasQuant := ctx.QuantDataMap[coin.Symbol]; hasQuant {
|
||||
sb.WriteString(e.formatQuantData(quantData))
|
||||
@@ -515,45 +515,45 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString("---\n\n")
|
||||
sb.WriteString("现在请分析并输出决策(思维链 + JSON)\n")
|
||||
sb.WriteString("Now please analyze and output your decision (Chain of Thought + JSON)\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatPositionInfo 格式化持仓信息
|
||||
// formatPositionInfo formats position information
|
||||
func (e *StrategyEngine) formatPositionInfo(index int, pos PositionInfo, ctx *Context) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// 计算持仓时长
|
||||
// Calculate holding duration
|
||||
holdingDuration := ""
|
||||
if pos.UpdateTime > 0 {
|
||||
durationMs := time.Now().UnixMilli() - pos.UpdateTime
|
||||
durationMin := durationMs / (1000 * 60)
|
||||
if durationMin < 60 {
|
||||
holdingDuration = fmt.Sprintf(" | 持仓时长%d分钟", durationMin)
|
||||
holdingDuration = fmt.Sprintf(" | Holding Duration %d min", durationMin)
|
||||
} else {
|
||||
durationHour := durationMin / 60
|
||||
durationMinRemainder := durationMin % 60
|
||||
holdingDuration = fmt.Sprintf(" | 持仓时长%d小时%d分钟", durationHour, durationMinRemainder)
|
||||
holdingDuration = fmt.Sprintf(" | Holding Duration %dh %dm", durationHour, durationMinRemainder)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算仓位价值
|
||||
// Calculate position value
|
||||
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",
|
||||
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))
|
||||
|
||||
// 使用策略配置的指标输出市场数据
|
||||
// Output market data using strategy configured indicators
|
||||
if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok {
|
||||
sb.WriteString(e.formatMarketData(marketData))
|
||||
|
||||
// 添加量化数据(如果有)
|
||||
// Add quantitative data if available
|
||||
if ctx.QuantDataMap != nil {
|
||||
if quantData, hasQuant := ctx.QuantDataMap[pos.Symbol]; hasQuant {
|
||||
sb.WriteString(e.formatQuantData(quantData))
|
||||
@@ -565,29 +565,29 @@ func (e *StrategyEngine) formatPositionInfo(index int, pos PositionInfo, ctx *Co
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatCoinSourceTag 格式化币种来源标签
|
||||
// formatCoinSourceTag formats coin source tag
|
||||
func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
|
||||
if len(sources) > 1 {
|
||||
return " (AI500+OI_Top双重信号)"
|
||||
return " (AI500+OI_Top dual signal)"
|
||||
} else if len(sources) == 1 {
|
||||
switch sources[0] {
|
||||
case "ai500":
|
||||
return " (AI500)"
|
||||
case "oi_top":
|
||||
return " (OI_Top持仓增长)"
|
||||
return " (OI_Top position growth)"
|
||||
case "static":
|
||||
return " (手动选择)"
|
||||
return " (Manual selection)"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// formatMarketData 根据策略配置格式化市场数据
|
||||
// formatMarketData formats market data according to strategy configuration
|
||||
func (e *StrategyEngine) formatMarketData(data *market.Data) string {
|
||||
var sb strings.Builder
|
||||
indicators := e.config.Indicators
|
||||
|
||||
// 当前价格(总是显示)
|
||||
// Current price (always display)
|
||||
sb.WriteString(fmt.Sprintf("current_price = %.4f", data.CurrentPrice))
|
||||
|
||||
// EMA
|
||||
@@ -607,7 +607,7 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string {
|
||||
|
||||
sb.WriteString("\n\n")
|
||||
|
||||
// OI 和 Funding Rate
|
||||
// OI and Funding Rate
|
||||
if indicators.EnableOI || indicators.EnableFundingRate {
|
||||
sb.WriteString(fmt.Sprintf("Additional data for %s:\n\n", data.Symbol))
|
||||
|
||||
@@ -621,9 +621,9 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string {
|
||||
}
|
||||
}
|
||||
|
||||
// 优先使用多时间周期数据(新增)
|
||||
// Prefer using multi-timeframe data (new addition)
|
||||
if len(data.TimeframeData) > 0 {
|
||||
// 按时间周期排序输出
|
||||
// Output in timeframe order
|
||||
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 {
|
||||
@@ -632,8 +632,8 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 兼容旧的数据格式
|
||||
// 日内数据
|
||||
// Compatible with old data format
|
||||
// Intraday data
|
||||
if data.IntradaySeries != nil {
|
||||
klineConfig := indicators.Klines
|
||||
sb.WriteString(fmt.Sprintf("Intraday series (%s intervals, oldest → latest):\n\n", klineConfig.PrimaryTimeframe))
|
||||
@@ -668,7 +668,7 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string {
|
||||
}
|
||||
}
|
||||
|
||||
// 长周期数据
|
||||
// Longer-term data
|
||||
if data.LongerTermContext != nil && indicators.Klines.EnableMultiTimeframe {
|
||||
sb.WriteString(fmt.Sprintf("Longer-term context (%s timeframe):\n\n", indicators.Klines.LongerTimeframe))
|
||||
|
||||
@@ -700,7 +700,7 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatTimeframeSeriesData 格式化单个时间周期的序列数据
|
||||
// formatTimeframeSeriesData formats series data for a single timeframe
|
||||
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)))
|
||||
@@ -737,7 +737,7 @@ func (e *StrategyEngine) formatTimeframeSeriesData(sb *strings.Builder, data *ma
|
||||
}
|
||||
}
|
||||
|
||||
// formatFloatSlice 格式化浮点数切片
|
||||
// formatFloatSlice formats float slice
|
||||
func formatFloatSlice(values []float64) string {
|
||||
strValues := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
@@ -746,179 +746,179 @@ func formatFloatSlice(values []float64) string {
|
||||
return "[" + strings.Join(strValues, ", ") + "]"
|
||||
}
|
||||
|
||||
// BuildSystemPrompt 根据策略配置构建 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
|
||||
|
||||
// 1. 角色定义(可编辑)
|
||||
// 1. Role definition (editable)
|
||||
if promptSections.RoleDefinition != "" {
|
||||
sb.WriteString(promptSections.RoleDefinition)
|
||||
sb.WriteString("\n\n")
|
||||
} else {
|
||||
sb.WriteString("# 你是专业的加密货币交易AI\n\n")
|
||||
sb.WriteString("你的任务是根据提供的市场数据做出交易决策。\n\n")
|
||||
sb.WriteString("# You are a professional cryptocurrency trading AI\n\n")
|
||||
sb.WriteString("Your task is to make trading decisions based on provided market data.\n\n")
|
||||
}
|
||||
|
||||
// 2. 交易模式变体
|
||||
// 2. Trading mode variant
|
||||
switch strings.ToLower(strings.TrimSpace(variant)) {
|
||||
case "aggressive":
|
||||
sb.WriteString("## 模式:Aggressive(进攻型)\n- 优先捕捉趋势突破,可在信心度≥70时分批建仓\n- 允许更高仓位,但须严格设置止损并说明盈亏比\n\n")
|
||||
sb.WriteString("## Mode: Aggressive\n- Prioritize capturing trend breakouts, can build positions in batches when confidence ≥ 70\n- Allow higher positions, but must strictly set stop-loss and explain risk-reward ratio\n\n")
|
||||
case "conservative":
|
||||
sb.WriteString("## 模式:Conservative(稳健型)\n- 仅在多重信号共振时开仓\n- 优先保留现金,连续亏损必须暂停多个周期\n\n")
|
||||
sb.WriteString("## Mode: Conservative\n- Only open positions when multiple signals resonate\n- Prioritize cash preservation, must pause for multiple periods after consecutive losses\n\n")
|
||||
case "scalping":
|
||||
sb.WriteString("## 模式:Scalping(剥头皮)\n- 聚焦短周期动量,目标收益较小但要求迅速\n- 若价格两根bar内未按预期运行,立即减仓或止损\n\n")
|
||||
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")
|
||||
}
|
||||
|
||||
// 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",
|
||||
// 3. Hard constraints (risk control) - from strategy config (non-editable, auto-generated)
|
||||
sb.WriteString("# Hard Constraints (Risk Control)\n\n")
|
||||
sb.WriteString(fmt.Sprintf("1. Risk-Reward Ratio: Must be ≥ 1:%.1f\n", riskControl.MinRiskRewardRatio))
|
||||
sb.WriteString(fmt.Sprintf("2. Max Positions: %d coins (quality > quantity)\n", riskControl.MaxPositions))
|
||||
sb.WriteString(fmt.Sprintf("3. Single Coin Position: Altcoins %.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",
|
||||
sb.WriteString(fmt.Sprintf("4. Leverage Limits: **Altcoins max %dx leverage** | **BTC/ETH max %dx leverage**\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))
|
||||
sb.WriteString(fmt.Sprintf("5. Margin Usage ≤ %.0f%%\n", riskControl.MaxMarginUsage*100))
|
||||
sb.WriteString(fmt.Sprintf("6. Opening Amount: Recommended ≥%.0f USDT\n", riskControl.MinPositionSize))
|
||||
sb.WriteString(fmt.Sprintf("7. Minimum Confidence: ≥%d\n\n", riskControl.MinConfidence))
|
||||
|
||||
// 4. 交易频率与信号质量(可编辑)
|
||||
// 4. Trading frequency and signal quality (editable)
|
||||
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")
|
||||
sb.WriteString("# ⏱️ Trading Frequency Awareness\n\n")
|
||||
sb.WriteString("- Excellent traders: 2-4 trades/day ≈ 0.1-0.2 trades/hour\n")
|
||||
sb.WriteString("- >2 trades/hour = Overtrading\n")
|
||||
sb.WriteString("- Single position hold time ≥ 30-60 minutes\n")
|
||||
sb.WriteString("If you find yourself trading every period → standards too low; if closing positions < 30 minutes → too impatient.\n\n")
|
||||
}
|
||||
|
||||
// 5. 开仓标准(可编辑)
|
||||
// 5. Entry standards (editable)
|
||||
if promptSections.EntryStandards != "" {
|
||||
sb.WriteString(promptSections.EntryStandards)
|
||||
sb.WriteString("\n\n你拥有以下指标数据:\n")
|
||||
sb.WriteString("\n\nYou have the following indicator data:\n")
|
||||
e.writeAvailableIndicators(&sb)
|
||||
sb.WriteString(fmt.Sprintf("\n**信心度 ≥%d** 才能开仓。\n\n", riskControl.MinConfidence))
|
||||
sb.WriteString(fmt.Sprintf("\n**Confidence ≥ %d** required to open positions.\n\n", riskControl.MinConfidence))
|
||||
} else {
|
||||
sb.WriteString("# 🎯 开仓标准(严格)\n\n")
|
||||
sb.WriteString("只在多重信号共振时开仓。你拥有:\n")
|
||||
sb.WriteString("# 🎯 Entry Standards (Strict)\n\n")
|
||||
sb.WriteString("Only open positions when multiple signals resonate. You have:\n")
|
||||
e.writeAvailableIndicators(&sb)
|
||||
sb.WriteString(fmt.Sprintf("\n自由运用任何有效的分析方法,但**信心度 ≥%d** 才能开仓;避免单一指标、信号矛盾、横盘震荡、刚平仓即重启等低质量行为。\n\n", riskControl.MinConfidence))
|
||||
sb.WriteString(fmt.Sprintf("\nFeel free to use any effective analysis method, but **confidence ≥ %d** required to open positions; avoid low-quality behaviors such as single indicators, contradictory signals, sideways consolidation, reopening immediately after closing, etc.\n\n", riskControl.MinConfidence))
|
||||
}
|
||||
|
||||
// 6. 决策流程提示(可编辑)
|
||||
// 6. Decision process tips (editable)
|
||||
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")
|
||||
sb.WriteString("# 📋 Decision Process\n\n")
|
||||
sb.WriteString("1. Check positions → Should we take profit/stop-loss\n")
|
||||
sb.WriteString("2. Scan candidate coins + multi-timeframe → Are there strong signals\n")
|
||||
sb.WriteString("3. Write chain of thought first, then output structured JSON\n\n")
|
||||
}
|
||||
|
||||
// 7. 输出格式
|
||||
sb.WriteString("# 输出格式 (严格遵守)\n\n")
|
||||
sb.WriteString("**必须使用XML标签 <reasoning> 和 <decision> 标签分隔思维链和决策JSON,避免解析错误**\n\n")
|
||||
sb.WriteString("## 格式要求\n\n")
|
||||
// 7. Output format
|
||||
sb.WriteString("# Output Format (Strictly Follow)\n\n")
|
||||
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")
|
||||
sb.WriteString("你的思维链分析...\n")
|
||||
sb.WriteString("- 简洁分析你的思考过程 \n")
|
||||
sb.WriteString("Your chain of thought analysis...\n")
|
||||
sb.WriteString("- Briefly analyze your thinking process \n")
|
||||
sb.WriteString("</reasoning>\n\n")
|
||||
sb.WriteString("<decision>\n")
|
||||
sb.WriteString("第二步: JSON决策数组\n\n")
|
||||
sb.WriteString("Step 2: JSON decision array\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("## 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(开仓建议≥%d)\n", riskControl.MinConfidence))
|
||||
sb.WriteString("- 开仓时必填: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n\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\n")
|
||||
|
||||
// 8. 自定义 Prompt
|
||||
// 8. Custom Prompt
|
||||
if e.config.CustomPrompt != "" {
|
||||
sb.WriteString("# 📌 个性化交易策略\n\n")
|
||||
sb.WriteString("# 📌 Personalized Trading Strategy\n\n")
|
||||
sb.WriteString(e.config.CustomPrompt)
|
||||
sb.WriteString("\n\n")
|
||||
sb.WriteString("注意: 以上个性化策略是对基础规则的补充,不能违背基础风险控制原则。\n")
|
||||
sb.WriteString("Note: The above personalized strategy is a supplement to the basic rules and cannot violate the basic risk control principles.\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// writeAvailableIndicators 写入可用指标列表
|
||||
// writeAvailableIndicators writes list of available indicators
|
||||
func (e *StrategyEngine) writeAvailableIndicators(sb *strings.Builder) {
|
||||
indicators := e.config.Indicators
|
||||
kline := indicators.Klines
|
||||
|
||||
sb.WriteString(fmt.Sprintf("- %s价格序列", kline.PrimaryTimeframe))
|
||||
sb.WriteString(fmt.Sprintf("- %s price series", kline.PrimaryTimeframe))
|
||||
if kline.EnableMultiTimeframe {
|
||||
sb.WriteString(fmt.Sprintf(" + %s K线序列\n", kline.LongerTimeframe))
|
||||
sb.WriteString(fmt.Sprintf(" + %s K-line series\n", kline.LongerTimeframe))
|
||||
} else {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if indicators.EnableEMA {
|
||||
sb.WriteString("- EMA 指标")
|
||||
sb.WriteString("- EMA indicators")
|
||||
if len(indicators.EMAPeriods) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("(周期: %v)", indicators.EMAPeriods))
|
||||
sb.WriteString(fmt.Sprintf(" (periods: %v)", indicators.EMAPeriods))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if indicators.EnableMACD {
|
||||
sb.WriteString("- MACD 指标\n")
|
||||
sb.WriteString("- MACD indicators\n")
|
||||
}
|
||||
|
||||
if indicators.EnableRSI {
|
||||
sb.WriteString("- RSI 指标")
|
||||
sb.WriteString("- RSI indicators")
|
||||
if len(indicators.RSIPeriods) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("(周期: %v)", indicators.RSIPeriods))
|
||||
sb.WriteString(fmt.Sprintf(" (periods: %v)", indicators.RSIPeriods))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if indicators.EnableATR {
|
||||
sb.WriteString("- ATR 指标")
|
||||
sb.WriteString("- ATR indicators")
|
||||
if len(indicators.ATRPeriods) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("(周期: %v)", indicators.ATRPeriods))
|
||||
sb.WriteString(fmt.Sprintf(" (periods: %v)", indicators.ATRPeriods))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if indicators.EnableVolume {
|
||||
sb.WriteString("- 成交量数据\n")
|
||||
sb.WriteString("- Volume data\n")
|
||||
}
|
||||
|
||||
if indicators.EnableOI {
|
||||
sb.WriteString("- 持仓量(OI)数据\n")
|
||||
sb.WriteString("- Open Interest (OI) data\n")
|
||||
}
|
||||
|
||||
if indicators.EnableFundingRate {
|
||||
sb.WriteString("- 资金费率\n")
|
||||
sb.WriteString("- Funding rate\n")
|
||||
}
|
||||
|
||||
if len(e.config.CoinSource.StaticCoins) > 0 || e.config.CoinSource.UseCoinPool || e.config.CoinSource.UseOITop {
|
||||
sb.WriteString("- AI500 / OI_Top 筛选标签(若有)\n")
|
||||
sb.WriteString("- AI500 / OI_Top filter tags (if available)\n")
|
||||
}
|
||||
|
||||
if indicators.EnableQuantData {
|
||||
sb.WriteString("- 量化数据(机构/散户资金流向、持仓变化、多周期价格变化)\n")
|
||||
sb.WriteString("- Quantitative data (institutional/retail fund flow, position changes, multi-period price changes)\n")
|
||||
}
|
||||
}
|
||||
|
||||
// GetRiskControlConfig 获取风险控制配置
|
||||
// GetRiskControlConfig gets risk control configuration
|
||||
func (e *StrategyEngine) GetRiskControlConfig() store.RiskControlConfig {
|
||||
return e.config.RiskControl
|
||||
}
|
||||
|
||||
// GetConfig 获取完整策略配置
|
||||
// GetConfig gets complete strategy configuration
|
||||
func (e *StrategyEngine) GetConfig() *store.StrategyConfig {
|
||||
return e.config
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLeverageFallback 测试杠杆超限时的自动修正功能
|
||||
// TestLeverageFallback tests automatic correction when leverage exceeds limit
|
||||
func TestLeverageFallback(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -12,47 +12,47 @@ func TestLeverageFallback(t *testing.T) {
|
||||
accountEquity float64
|
||||
btcEthLeverage int
|
||||
altcoinLeverage int
|
||||
wantLeverage int // 期望修正后的杠杆值
|
||||
wantLeverage int // Expected leverage after correction
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "山寨币杠杆超限_自动修正为上限",
|
||||
name: "Altcoin leverage exceeded - auto-correct to limit",
|
||||
decision: Decision{
|
||||
Symbol: "SOLUSDT",
|
||||
Action: "open_long",
|
||||
Leverage: 20, // 超过上限
|
||||
Leverage: 20, // Exceeds limit
|
||||
PositionSizeUSD: 100,
|
||||
StopLoss: 50,
|
||||
TakeProfit: 200,
|
||||
},
|
||||
accountEquity: 100,
|
||||
btcEthLeverage: 10,
|
||||
altcoinLeverage: 5, // 上限 5x
|
||||
wantLeverage: 5, // 应该修正为 5
|
||||
altcoinLeverage: 5, // Limit 5x
|
||||
wantLeverage: 5, // Should be corrected to 5
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "BTC杠杆超限_自动修正为上限",
|
||||
name: "BTC leverage exceeded - auto-correct to limit",
|
||||
decision: Decision{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "open_long",
|
||||
Leverage: 20, // 超过上限
|
||||
Leverage: 20, // Exceeds limit
|
||||
PositionSizeUSD: 1000,
|
||||
StopLoss: 90000,
|
||||
TakeProfit: 110000,
|
||||
},
|
||||
accountEquity: 100,
|
||||
btcEthLeverage: 10, // 上限 10x
|
||||
btcEthLeverage: 10, // Limit 10x
|
||||
altcoinLeverage: 5,
|
||||
wantLeverage: 10, // 应该修正为 10
|
||||
wantLeverage: 10, // Should be corrected to 10
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "杠杆在上限内_不修正",
|
||||
name: "Leverage within limit - no correction",
|
||||
decision: Decision{
|
||||
Symbol: "ETHUSDT",
|
||||
Action: "open_short",
|
||||
Leverage: 5, // 未超限
|
||||
Leverage: 5, // Not exceeded
|
||||
PositionSizeUSD: 500,
|
||||
StopLoss: 4000,
|
||||
TakeProfit: 3000,
|
||||
@@ -60,15 +60,15 @@ func TestLeverageFallback(t *testing.T) {
|
||||
accountEquity: 100,
|
||||
btcEthLeverage: 10,
|
||||
altcoinLeverage: 5,
|
||||
wantLeverage: 5, // 保持不变
|
||||
wantLeverage: 5, // Stays unchanged
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "杠杆为0_应该报错",
|
||||
name: "Leverage is 0 - should error",
|
||||
decision: Decision{
|
||||
Symbol: "SOLUSDT",
|
||||
Action: "open_long",
|
||||
Leverage: 0, // 无效
|
||||
Leverage: 0, // Invalid
|
||||
PositionSizeUSD: 100,
|
||||
StopLoss: 50,
|
||||
TakeProfit: 200,
|
||||
@@ -85,13 +85,13 @@ func TestLeverageFallback(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateDecision(&tt.decision, tt.accountEquity, tt.btcEthLeverage, tt.altcoinLeverage)
|
||||
|
||||
// 检查错误状态
|
||||
// Check error status
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("validateDecision() error = %v, wantError %v", err, tt.wantError)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果不应该报错,检查杠杆是否被正确修正
|
||||
// If shouldn't error, check if leverage was correctly corrected
|
||||
if !tt.wantError && tt.decision.Leverage != tt.wantLeverage {
|
||||
t.Errorf("Leverage not corrected: got %d, want %d", tt.decision.Leverage, tt.wantLeverage)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func TestLeverageFallback(t *testing.T) {
|
||||
}
|
||||
|
||||
|
||||
// contains 检查字符串是否包含子串(辅助函数)
|
||||
// contains checks if string contains substring (helper function)
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
(len(s) > 0 && len(substr) > 0 && stringContains(s, substr)))
|
||||
|
||||
Reference in New Issue
Block a user