refactor: standardize code comments

This commit is contained in:
tinkle-community
2025-12-08 01:40:48 +08:00
parent 0636ced476
commit a12c0ae8c9
103 changed files with 5466 additions and 5468 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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)))