From a12c0ae8c9a5aad6e6f28edbf1a292045959da89 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Mon, 8 Dec 2025 01:40:48 +0800 Subject: [PATCH] refactor: standardize code comments --- api/backtest.go | 22 +- api/crypto_handler.go | 28 +- api/register_otp_test.go | 106 +-- api/server.go | 974 ++++++++++----------- api/server_test.go | 52 +- api/strategy.go | 204 ++--- api/traderid_test.go | 44 +- api/utils.go | 18 +- api/utils_test.go | 24 +- auth/auth.go | 38 +- backtest/account.go | 4 +- backtest/ai_client.go | 10 +- backtest/aicache.go | 2 +- backtest/config.go | 14 +- backtest/datafeed.go | 8 +- backtest/equity.go | 8 +- backtest/lock.go | 2 +- backtest/manager.go | 6 +- backtest/metrics.go | 2 +- backtest/runner.go | 26 +- backtest/storage.go | 8 +- backtest/types.go | 22 +- config/config.go | 14 +- crypto/crypto.go | 100 +-- decision/engine.go | 658 +++++++------- decision/prompt_manager.go | 64 +- decision/prompt_manager_test.go | 154 ++-- decision/prompt_reload_integration_test.go | 154 ++-- decision/prompt_test.go | 10 +- decision/strategy_engine.go | 318 +++---- decision/validate_test.go | 36 +- hook/http_client_hook.go | 2 +- hook/ip_hook.go | 2 +- hook/trader_hook.go | 4 +- logger/config.go | 6 +- logger/logger.go | 44 +- main.go | 78 +- manager/trader_manager.go | 232 ++--- manager/trader_manager_test.go | 44 +- market/api_client.go | 8 +- market/combined_streams.go | 36 +- market/data.go | 220 ++--- market/data_test.go | 80 +- market/historical.go | 4 +- market/monitor.go | 98 +-- market/timeframe.go | 10 +- market/types.go | 46 +- market/websocket_client.go | 22 +- mcp/client.go | 198 ++--- mcp/client_test.go | 42 +- mcp/config.go | 22 +- mcp/config_usage_test.go | 76 +- mcp/deepseek_client.go | 30 +- mcp/deepseek_client_test.go | 44 +- mcp/examples_test.go | 88 +- mcp/interface.go | 10 +- mcp/logger.go | 10 +- mcp/mock_test.go | 56 +- mcp/options.go | 58 +- mcp/options_test.go | 30 +- mcp/qwen_client.go | 30 +- mcp/qwen_client_test.go | 44 +- mcp/request.go | 56 +- mcp/request_builder.go | 98 +-- mcp/request_builder_test.go | 34 +- pool/coin_pool.go | 288 +++--- scripts/migrate_encryption.go | 72 +- store/ai_model.go | 34 +- store/backtest.go | 76 +- store/decision.go | 86 +- store/equity.go | 60 +- store/exchange.go | 28 +- store/order.go | 146 +-- store/position.go | 106 +-- store/store.go | 114 +-- store/strategy.go | 208 ++--- store/trader.go | 48 +- store/user.go | 22 +- trader/aster_trader.go | 390 ++++----- trader/aster_trader_test.go | 60 +- trader/auto_trader.go | 716 +++++++-------- trader/auto_trader_test.go | 258 +++--- trader/binance_futures.go | 404 ++++----- trader/binance_futures_test.go | 84 +- trader/bybit_trader.go | 260 +++--- trader/bybit_trader_test.go | 104 +-- trader/helpers.go | 8 +- trader/hyperliquid_trader.go | 478 +++++----- trader/hyperliquid_trader_test.go | 156 ++-- trader/interface.go | 40 +- trader/lighter_account.go | 94 +- trader/lighter_orders.go | 118 +-- trader/lighter_trader.go | 96 +- trader/lighter_trader_test.go | 40 +- trader/lighter_trader_v2.go | 142 +-- trader/lighter_trader_v2_account.go | 40 +- trader/lighter_trader_v2_orders.go | 168 ++-- trader/lighter_trader_v2_trading.go | 212 ++--- trader/lighter_trading.go | 92 +- trader/okx_trader.go | 364 ++++---- trader/order_sync.go | 100 +-- trader/position_sync.go | 102 +-- trader/trader_test_suite.go | 128 +-- 103 files changed, 5466 insertions(+), 5468 deletions(-) diff --git a/api/backtest.go b/api/backtest.go index 9e000dda..015c75b3 100644 --- a/api/backtest.go +++ b/api/backtest.go @@ -69,7 +69,7 @@ func (s *Server) handleBacktestStart(c *gin.Context) { cfg.PromptTemplate = "default" } if _, err := decision.GetPromptTemplate(cfg.PromptTemplate); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("提示词模板不存在: %s", cfg.PromptTemplate)}) + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Prompt template does not exist: %s", cfg.PromptTemplate)}) return } cfg.CustomPrompt = strings.TrimSpace(cfg.CustomPrompt) @@ -498,9 +498,9 @@ func writeBacktestAccessError(c *gin.Context, err error) bool { } switch { case errors.Is(err, errBacktestForbidden): - c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该回测任务"}) + c.JSON(http.StatusForbidden, gin.H{"error": "No permission to access this backtest task"}) case errors.Is(err, os.ErrNotExist), errors.Is(err, sql.ErrNoRows): - c.JSON(http.StatusNotFound, gin.H{"error": "回测任务不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Backtest task does not exist"}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } @@ -512,7 +512,7 @@ func (s *Server) resolveBacktestAIConfig(cfg *backtest.BacktestConfig, userID st return fmt.Errorf("config is nil") } if s.store == nil { - return fmt.Errorf("系统数据库未就绪,无法加载AI模型配置") + return fmt.Errorf("System database not ready, cannot load AI model configuration") } cfg.UserID = normalizeUserID(userID) @@ -525,7 +525,7 @@ func (s *Server) hydrateBacktestAIConfig(cfg *backtest.BacktestConfig) error { return fmt.Errorf("config is nil") } if s.store == nil { - return fmt.Errorf("系统数据库未就绪,无法加载AI模型配置") + return fmt.Errorf("System database not ready, cannot load AI model configuration") } cfg.UserID = normalizeUserID(cfg.UserID) @@ -539,23 +539,23 @@ func (s *Server) hydrateBacktestAIConfig(cfg *backtest.BacktestConfig) error { if modelID != "" { model, err = s.store.AIModel().Get(cfg.UserID, modelID) if err != nil { - return fmt.Errorf("加载AI模型失败: %w", err) + return fmt.Errorf("Failed to load AI model: %w", err) } } else { model, err = s.store.AIModel().GetDefault(cfg.UserID) if err != nil { - return fmt.Errorf("未找到可用的AI模型: %w", err) + return fmt.Errorf("No available AI model found: %w", err) } cfg.AIModelID = model.ID } if !model.Enabled { - return fmt.Errorf("AI模型 %s 尚未启用", model.Name) + return fmt.Errorf("AI model %s is not enabled yet", model.Name) } apiKey := strings.TrimSpace(model.APIKey) if apiKey == "" { - return fmt.Errorf("AI模型 %s 缺少API Key,请先在系统中配置", model.Name) + return fmt.Errorf("AI model %s is missing API Key, please configure it in the system first", model.Name) } cfg.AICfg.Provider = strings.ToLower(model.Provider) @@ -569,10 +569,10 @@ func (s *Server) hydrateBacktestAIConfig(cfg *backtest.BacktestConfig) error { if cfg.AICfg.Provider == "custom" { if cfg.AICfg.BaseURL == "" { - return fmt.Errorf("自定义AI模型需要配置 API 地址") + return fmt.Errorf("Custom AI model requires API URL configuration") } if cfg.AICfg.Model == "" { - return fmt.Errorf("自定义AI模型需要配置模型名称") + return fmt.Errorf("Custom AI model requires model name configuration") } } diff --git a/api/crypto_handler.go b/api/crypto_handler.go index f5a5890b..fbbf0fa6 100644 --- a/api/crypto_handler.go +++ b/api/crypto_handler.go @@ -8,21 +8,21 @@ import ( "github.com/gin-gonic/gin" ) -// CryptoHandler 加密 API 處理器 +// CryptoHandler Encryption API handler type CryptoHandler struct { cryptoService *crypto.CryptoService } -// NewCryptoHandler 創建加密處理器 +// NewCryptoHandler Creates encryption handler func NewCryptoHandler(cryptoService *crypto.CryptoService) *CryptoHandler { return &CryptoHandler{ cryptoService: cryptoService, } } -// ==================== 公鑰端點 ==================== +// ==================== Public Key Endpoint ==================== -// HandleGetPublicKey 獲取伺服器公鑰 +// HandleGetPublicKey Get server public key func (h *CryptoHandler) HandleGetPublicKey(c *gin.Context) { publicKey := h.cryptoService.GetPublicKeyPEM() @@ -32,9 +32,9 @@ func (h *CryptoHandler) HandleGetPublicKey(c *gin.Context) { }) } -// ==================== 加密數據解密端點 ==================== +// ==================== Encrypted Data Decryption Endpoint ==================== -// HandleDecryptSensitiveData 解密客戶端傳送的加密数据 +// HandleDecryptSensitiveData Decrypt encrypted data sent from client func (h *CryptoHandler) HandleDecryptSensitiveData(c *gin.Context) { var payload crypto.EncryptedPayload if err := c.ShouldBindJSON(&payload); err != nil { @@ -42,10 +42,10 @@ func (h *CryptoHandler) HandleDecryptSensitiveData(c *gin.Context) { return } - // 解密 + // Decrypt decrypted, err := h.cryptoService.DecryptSensitiveData(&payload) if err != nil { - log.Printf("❌ 解密失敗: %v", err) + log.Printf("❌ Decryption failed: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Decryption failed"}) return } @@ -55,18 +55,18 @@ func (h *CryptoHandler) HandleDecryptSensitiveData(c *gin.Context) { }) } -// ==================== 審計日誌查詢端點 ==================== +// ==================== Audit Log Query Endpoint ==================== -// 删除审计日志相关功能,在当前简化的实现中不需要 +// Audit log functionality removed, not needed in current simplified implementation -// ==================== 工具函數 ==================== +// ==================== Utility Functions ==================== -// isValidPrivateKey 驗證私鑰格式 +// isValidPrivateKey Validate private key format func isValidPrivateKey(key string) bool { - // EVM 私鑰: 64 位十六進制 (可選 0x 前綴) + // EVM private key: 64 hex characters (optional 0x prefix) if len(key) == 64 || (len(key) == 66 && key[:2] == "0x") { return true } - // TODO: 添加其他鏈的驗證 + // TODO: Add validation for other chains return false } diff --git a/api/register_otp_test.go b/api/register_otp_test.go index dcd13288..f03a9bfb 100644 --- a/api/register_otp_test.go +++ b/api/register_otp_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -// MockUser 模擬用戶結構 +// MockUser Mock user structure type MockUser struct { ID int Email string @@ -12,7 +12,7 @@ type MockUser struct { OTPVerified bool } -// TestOTPRefetchLogic 測試 OTP 重新獲取邏輯 +// TestOTPRefetchLogic Test OTP refetch logic func TestOTPRefetchLogic(t *testing.T) { tests := []struct { name string @@ -22,14 +22,14 @@ func TestOTPRefetchLogic(t *testing.T) { expectedMessage string }{ { - name: "新用戶註冊_郵箱不存在", + name: "New user registration - email does not exist", existingUser: nil, userExists: false, expectedAction: "create_new", - expectedMessage: "創建新用戶", + expectedMessage: "Create new user", }, { - name: "未完成OTP驗證_允許重新獲取", + name: "Incomplete OTP verification - allow refetch", existingUser: &MockUser{ ID: 1, Email: "test@example.com", @@ -38,10 +38,10 @@ func TestOTPRefetchLogic(t *testing.T) { }, userExists: true, expectedAction: "allow_refetch", - expectedMessage: "检测到未完成的注册,请继续完成OTP设置", + expectedMessage: "Incomplete registration detected, please continue OTP setup", }, { - name: "已完成OTP驗證_拒絕重複註冊", + name: "Completed OTP verification - reject duplicate registration", existingUser: &MockUser{ ID: 2, Email: "verified@example.com", @@ -50,45 +50,45 @@ func TestOTPRefetchLogic(t *testing.T) { }, userExists: true, expectedAction: "reject_duplicate", - expectedMessage: "邮箱已被注册", + expectedMessage: "Email already registered", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // 模擬邏輯處理流程 + // Simulate logic processing flow var actualAction string var actualMessage string if !tt.userExists { - // 用戶不存在,創建新用戶 + // User does not exist, create new user actualAction = "create_new" - actualMessage = "創建新用戶" + actualMessage = "Create new user" } else { - // 用戶已存在,檢查 OTP 驗證狀態 + // User exists, check OTP verification status if !tt.existingUser.OTPVerified { - // 未完成 OTP 驗證,允許重新獲取 + // OTP verification incomplete, allow refetch actualAction = "allow_refetch" - actualMessage = "检测到未完成的注册,请继续完成OTP设置" + actualMessage = "Incomplete registration detected, please continue OTP setup" } else { - // 已完成驗證,拒絕重複註冊 + // Verification completed, reject duplicate registration actualAction = "reject_duplicate" - actualMessage = "邮箱已被注册" + actualMessage = "Email already registered" } } - // 驗證結果 + // Verify results if actualAction != tt.expectedAction { - t.Errorf("Action 不符: got %s, want %s", actualAction, tt.expectedAction) + t.Errorf("Action mismatch: got %s, want %s", actualAction, tt.expectedAction) } if actualMessage != tt.expectedMessage { - t.Errorf("Message 不符: got %s, want %s", actualMessage, tt.expectedMessage) + t.Errorf("Message mismatch: got %s, want %s", actualMessage, tt.expectedMessage) } }) } } -// TestOTPVerificationStates 測試 OTP 驗證狀態判斷 +// TestOTPVerificationStates Test OTP verification state determination func TestOTPVerificationStates(t *testing.T) { tests := []struct { name string @@ -96,12 +96,12 @@ func TestOTPVerificationStates(t *testing.T) { shouldAllowRefetch bool }{ { - name: "OTP已驗證_不允許重新獲取", + name: "OTP verified - disallow refetch", otpVerified: true, shouldAllowRefetch: false, }, { - name: "OTP未驗證_允許重新獲取", + name: "OTP not verified - allow refetch", otpVerified: false, shouldAllowRefetch: true, }, @@ -109,7 +109,7 @@ func TestOTPVerificationStates(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // 模擬驗證邏輯 + // Simulate verification logic allowRefetch := !tt.otpVerified if allowRefetch != tt.shouldAllowRefetch { @@ -120,72 +120,72 @@ func TestOTPVerificationStates(t *testing.T) { } } -// TestRegistrationFlow 測試完整註冊流程的邏輯分支 +// TestRegistrationFlow Test complete registration flow logic branches func TestRegistrationFlow(t *testing.T) { tests := []struct { name string scenario string userExists bool otpVerified bool - expectHTTPCode int // 模擬的 HTTP 狀態碼 + expectHTTPCode int // Simulated HTTP status code expectResponse string }{ { - name: "場景1_新用戶首次註冊", - scenario: "新用戶首次訪問註冊接口", + name: "Scenario 1: New user first registration", + scenario: "New user first accesses registration endpoint", userExists: false, otpVerified: false, expectHTTPCode: 200, - expectResponse: "創建用戶並返回 OTP 設置信息", + expectResponse: "Create user and return OTP setup information", }, { - name: "場景2_用戶中斷註冊後重新訪問", - scenario: "用戶之前註冊但未完成 OTP 設置,現在重新訪問", + name: "Scenario 2: User re-accesses after interrupting registration", + scenario: "User registered previously but did not complete OTP setup, now re-accessing", userExists: true, otpVerified: false, expectHTTPCode: 200, - expectResponse: "返回現有用戶的 OTP 信息,允許繼續完成", + expectResponse: "Return existing user's OTP information, allow continuation", }, { - name: "場景3_已註冊用戶嘗試重複註冊", - scenario: "用戶已完成註冊,嘗試用同一郵箱再次註冊", + name: "Scenario 3: Registered user attempts duplicate registration", + scenario: "User already completed registration, attempts to register again with same email", userExists: true, otpVerified: true, expectHTTPCode: 409, // Conflict - expectResponse: "邮箱已被注册", + expectResponse: "Email already registered", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // 模擬註冊流程邏輯 + // Simulate registration flow logic var actualHTTPCode int var actualResponse string if !tt.userExists { - // 新用戶,創建並返回 OTP 信息 + // New user, create and return OTP information actualHTTPCode = 200 - actualResponse = "創建用戶並返回 OTP 設置信息" + actualResponse = "Create user and return OTP setup information" } else { - // 用戶已存在 + // User exists if !tt.otpVerified { - // 未完成 OTP 驗證,允許重新獲取 + // OTP verification incomplete, allow refetch actualHTTPCode = 200 - actualResponse = "返回現有用戶的 OTP 信息,允許繼續完成" + actualResponse = "Return existing user's OTP information, allow continuation" } else { - // 已完成驗證,拒絕重複註冊 + // Verification completed, reject duplicate registration actualHTTPCode = 409 - actualResponse = "邮箱已被注册" + actualResponse = "Email already registered" } } - // 驗證 + // Verify if actualHTTPCode != tt.expectHTTPCode { - t.Errorf("HTTP code 不符: got %d, want %d (scenario: %s)", + t.Errorf("HTTP code mismatch: got %d, want %d (scenario: %s)", actualHTTPCode, tt.expectHTTPCode, tt.scenario) } if actualResponse != tt.expectResponse { - t.Errorf("Response 不符: got %s, want %s (scenario: %s)", + t.Errorf("Response mismatch: got %s, want %s (scenario: %s)", actualResponse, tt.expectResponse, tt.scenario) } @@ -194,7 +194,7 @@ func TestRegistrationFlow(t *testing.T) { } } -// TestEdgeCases 測試邊界情況 +// TestEdgeCases Test edge cases func TestEdgeCases(t *testing.T) { tests := []struct { name string @@ -203,17 +203,17 @@ func TestEdgeCases(t *testing.T) { description string }{ { - name: "用戶ID為0_視為新用戶", + name: "User ID is 0 - treated as new user", user: &MockUser{ ID: 0, Email: "new@example.com", OTPVerified: false, }, expectAllow: true, - description: "ID為0通常表示用戶還未創建", + description: "ID of 0 usually indicates user has not been created yet", }, { - name: "OTPSecret為空_仍可重新獲取", + name: "OTPSecret is empty - still can refetch", user: &MockUser{ ID: 1, Email: "test@example.com", @@ -221,10 +221,10 @@ func TestEdgeCases(t *testing.T) { OTPVerified: false, }, expectAllow: true, - description: "即使 OTPSecret 為空,只要未驗證就允許重新獲取", + description: "Even if OTPSecret is empty, as long as not verified, refetch is allowed", }, { - name: "OTPSecret存在但已驗證_不允許", + name: "OTPSecret exists but already verified - not allowed", user: &MockUser{ ID: 2, Email: "verified@example.com", @@ -232,13 +232,13 @@ func TestEdgeCases(t *testing.T) { OTPVerified: true, }, expectAllow: false, - description: "OTP 已驗證的用戶不能重新獲取", + description: "Users with verified OTP cannot refetch", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // 核心邏輯:只要 OTPVerified 為 false,就允許重新獲取 + // Core logic: as long as OTPVerified is false, refetch is allowed allowRefetch := !tt.user.OTPVerified if allowRefetch != tt.expectAllow { diff --git a/api/server.go b/api/server.go index d9b426df..c05670eb 100644 --- a/api/server.go +++ b/api/server.go @@ -22,7 +22,7 @@ import ( "github.com/google/uuid" ) -// Server HTTP API服务器 +// Server HTTP API server type Server struct { router *gin.Engine traderManager *manager.TraderManager @@ -33,17 +33,17 @@ type Server struct { port int } -// NewServer 创建API服务器 +// NewServer Creates API server func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoService *crypto.CryptoService, backtestManager *backtest.Manager, port int) *Server { - // 设置为Release模式(减少日志输出) + // Set to Release mode (reduce log output) gin.SetMode(gin.ReleaseMode) router := gin.Default() - // 启用CORS + // Enable CORS router.Use(corsMiddleware()) - // 创建加密处理器 + // Create crypto handler cryptoHandler := NewCryptoHandler(cryptoService) s := &Server{ @@ -55,13 +55,13 @@ func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoServ port: port, } - // 设置路由 + // Setup routes s.setupRoutes() return s } -// corsMiddleware CORS中间件 +// corsMiddleware CORS middleware func corsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") @@ -77,32 +77,32 @@ func corsMiddleware() gin.HandlerFunc { } } -// setupRoutes 设置路由 +// setupRoutes Setup routes func (s *Server) setupRoutes() { - // API路由组 + // API route group api := s.router.Group("/api") { - // 健康检查 + // Health check api.Any("/health", s.handleHealth) - // 管理员登录(管理员模式下使用,公共) + // Admin login (used in admin mode, public) - // 系统支持的模型和交易所(无需认证) + // System supported models and exchanges (no authentication required) api.GET("/supported-models", s.handleGetSupportedModels) api.GET("/supported-exchanges", s.handleGetSupportedExchanges) - // 系统配置(无需认证,用于前端判断是否管理员模式/注册是否开启) + // System config (no authentication required, for frontend to determine admin mode/registration status) api.GET("/config", s.handleGetSystemConfig) - // 加密相关接口(无需认证) + // Crypto related endpoints (no authentication required) api.GET("/crypto/public-key", s.cryptoHandler.HandleGetPublicKey) api.POST("/crypto/decrypt", s.cryptoHandler.HandleDecryptSensitiveData) - // 系统提示词模板管理(无需认证) + // System prompt template management (no authentication required) api.GET("/prompt-templates", s.handleGetPromptTemplates) api.GET("/prompt-templates/:name", s.handleGetPromptTemplate) - // 公开的竞赛数据(无需认证) + // Public competition data (no authentication required) api.GET("/traders", s.handlePublicTraderList) api.GET("/competition", s.handlePublicCompetition) api.GET("/top-traders", s.handleTopTraders) @@ -110,22 +110,22 @@ func (s *Server) setupRoutes() { api.POST("/equity-history-batch", s.handleEquityHistoryBatch) api.GET("/traders/:id/public-config", s.handleGetPublicTraderConfig) - // 认证相关路由(无需认证) + // Authentication related routes (no authentication required) api.POST("/register", s.handleRegister) api.POST("/login", s.handleLogin) api.POST("/verify-otp", s.handleVerifyOTP) api.POST("/complete-registration", s.handleCompleteRegistration) - // 需要认证的路由 + // Routes requiring authentication protected := api.Group("/", s.authMiddleware()) { - // 注销(加入黑名单) + // Logout (add to blacklist) protected.POST("/logout", s.handleLogout) - // 服务器IP查询(需要认证,用于白名单配置) + // Server IP query (requires authentication, for whitelist configuration) protected.GET("/server-ip", s.handleGetServerIP) - // AI交易员管理 + // AI trader management protected.GET("/my-traders", s.handleTraderList) protected.GET("/traders/:id/config", s.handleGetTraderConfig) protected.POST("/traders", s.handleCreateTrader) @@ -137,15 +137,15 @@ func (s *Server) setupRoutes() { protected.POST("/traders/:id/sync-balance", s.handleSyncBalance) protected.POST("/traders/:id/close-position", s.handleClosePosition) - // AI模型配置 + // AI model configuration protected.GET("/models", s.handleGetModelConfigs) protected.PUT("/models", s.handleUpdateModelConfigs) - // 交易所配置 + // Exchange configuration protected.GET("/exchanges", s.handleGetExchangeConfigs) protected.PUT("/exchanges", s.handleUpdateExchangeConfigs) - // 策略管理 + // Strategy management protected.GET("/strategies", s.handleGetStrategies) protected.GET("/strategies/active", s.handleGetActiveStrategy) protected.GET("/strategies/default-config", s.handleGetDefaultStrategyConfig) @@ -159,7 +159,7 @@ func (s *Server) setupRoutes() { protected.POST("/strategies/:id/activate", s.handleActivateStrategy) protected.POST("/strategies/:id/duplicate", s.handleDuplicateStrategy) - // 指定trader的数据(使用query参数 ?trader_id=xxx) + // Data for specified trader (using query parameter ?trader_id=xxx) protected.GET("/status", s.handleStatus) protected.GET("/account", s.handleAccount) protected.GET("/positions", s.handlePositions) @@ -170,7 +170,7 @@ func (s *Server) setupRoutes() { } } -// handleHealth 健康检查 +// handleHealth Health check func (s *Server) handleHealth(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "ok", @@ -178,42 +178,42 @@ func (s *Server) handleHealth(c *gin.Context) { }) } -// handleGetSystemConfig 获取系统配置(客户端需要知道的配置) +// handleGetSystemConfig Get system configuration (configuration that client needs to know) func (s *Server) handleGetSystemConfig(c *gin.Context) { cfg := config.Get() c.JSON(http.StatusOK, gin.H{ "registration_enabled": cfg.RegistrationEnabled, - "btc_eth_leverage": 10, // 默认值 - "altcoin_leverage": 5, // 默认值 + "btc_eth_leverage": 10, // Default value + "altcoin_leverage": 5, // Default value }) } -// handleGetServerIP 获取服务器IP地址(用于白名单配置) +// handleGetServerIP Get server IP address (for whitelist configuration) func (s *Server) handleGetServerIP(c *gin.Context) { - // 尝试通过第三方API获取公网IP + // Try to get public IP via third-party API publicIP := getPublicIPFromAPI() - // 如果第三方API失败,从网络接口获取第一个公网IP + // If third-party API fails, get first public IP from network interface if publicIP == "" { publicIP = getPublicIPFromInterface() } - // 如果还是没有获取到,返回错误 + // If still cannot get it, return error if publicIP == "" { - c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取公网IP地址"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to get public IP address"}) return } c.JSON(http.StatusOK, gin.H{ "public_ip": publicIP, - "message": "请将此IP地址添加到白名单中", + "message": "Please add this IP address to the whitelist", }) } -// getPublicIPFromAPI 通过第三方API获取公网IP +// getPublicIPFromAPI Get public IP via third-party API func getPublicIPFromAPI() string { - // 尝试多个公网IP查询服务 + // Try multiple public IP query services services := []string{ "https://api.ipify.org?format=text", "https://icanhazip.com", @@ -239,7 +239,7 @@ func getPublicIPFromAPI() string { } ip := strings.TrimSpace(string(body[:n])) - // 验证是否为有效的IP地址 + // Verify if it's a valid IP address if net.ParseIP(ip) != nil { return ip } @@ -249,7 +249,7 @@ func getPublicIPFromAPI() string { return "" } -// getPublicIPFromInterface 从网络接口获取第一个公网IP +// getPublicIPFromInterface Get first public IP from network interface func getPublicIPFromInterface() string { interfaces, err := net.Interfaces() if err != nil { @@ -257,7 +257,7 @@ func getPublicIPFromInterface() string { } for _, iface := range interfaces { - // 跳过未启用的接口和回环接口 + // Skip disabled interfaces and loopback interfaces if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { continue } @@ -280,10 +280,10 @@ func getPublicIPFromInterface() string { continue } - // 只考虑IPv4地址 + // Only consider IPv4 addresses if ip.To4() != nil { ipStr := ip.String() - // 排除私有IP地址范围 + // Exclude private IP address ranges if !isPrivateIP(ip) { return ipStr } @@ -294,9 +294,9 @@ func getPublicIPFromInterface() string { return "" } -// isPrivateIP 判断是否为私有IP地址 +// isPrivateIP Determine if it's a private IP address func isPrivateIP(ip net.IP) bool { - // 私有IP地址范围: + // Private IP address ranges: // 10.0.0.0/8 // 172.16.0.0/12 // 192.168.0.0/16 @@ -316,25 +316,25 @@ func isPrivateIP(ip net.IP) bool { return false } -// getTraderFromQuery 从query参数获取trader +// getTraderFromQuery Get trader from query parameter func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) { userID := c.GetString("user_id") traderID := c.Query("trader_id") - // 确保用户的交易员已加载到内存中 + // Ensure user's traders are loaded into memory err := s.traderManager.LoadUserTradersFromStore(s.store, userID) if err != nil { - logger.Infof("⚠️ 加载用户 %s 的交易员失败: %v", userID, err) + logger.Infof("⚠️ Failed to load traders for user %s: %v", userID, err) } if traderID == "" { - // 如果没有指定trader_id,返回该用户的第一个trader + // If no trader_id specified, return first trader for this user ids := s.traderManager.GetTraderIDs() if len(ids) == 0 { - return nil, "", fmt.Errorf("没有可用的trader") + return nil, "", fmt.Errorf("No available traders") } - // 获取用户的交易员列表,优先返回用户自己的交易员 + // Get user's trader list, prioritize returning user's own traders userTraders, err := s.store.Trader().List(userID) if err == nil && len(userTraders) > 0 { traderID = userTraders[0].ID @@ -346,22 +346,22 @@ func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, str return s.traderManager, traderID, nil } -// AI交易员管理相关结构体 +// AI trader management related structures type CreateTraderRequest struct { Name string `json:"name" binding:"required"` AIModelID string `json:"ai_model_id" binding:"required"` ExchangeID string `json:"exchange_id" binding:"required"` - StrategyID string `json:"strategy_id"` // 策略ID(新版) + StrategyID string `json:"strategy_id"` // Strategy ID (new version) InitialBalance float64 `json:"initial_balance"` ScanIntervalMinutes int `json:"scan_interval_minutes"` - IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型,nil表示使用默认值true - // 以下字段为向后兼容保留,新版使用策略配置 + IsCrossMargin *bool `json:"is_cross_margin"` // Pointer type, nil means use default value true + // The following fields are kept for backward compatibility, new version uses strategy config BTCETHLeverage int `json:"btc_eth_leverage"` AltcoinLeverage int `json:"altcoin_leverage"` TradingSymbols string `json:"trading_symbols"` CustomPrompt string `json:"custom_prompt"` OverrideBasePrompt bool `json:"override_base_prompt"` - SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称 + SystemPromptTemplate string `json:"system_prompt_template"` // System prompt template name UseCoinPool bool `json:"use_coin_pool"` UseOITop bool `json:"use_oi_top"` } @@ -375,14 +375,14 @@ type ModelConfig struct { CustomAPIURL string `json:"customApiUrl,omitempty"` } -// SafeModelConfig 安全的模型配置结构(不包含敏感信息) +// SafeModelConfig Safe model configuration structure (does not contain sensitive information) type SafeModelConfig struct { ID string `json:"id"` Name string `json:"name"` Provider string `json:"provider"` Enabled bool `json:"enabled"` - CustomAPIURL string `json:"customApiUrl"` // 自定义API URL(通常不敏感) - CustomModelName string `json:"customModelName"` // 自定义模型名(不敏感) + CustomAPIURL string `json:"customApiUrl"` // Custom API URL (usually not sensitive) + CustomModelName string `json:"customModelName"` // Custom model name (not sensitive) } type ExchangeConfig struct { @@ -395,16 +395,16 @@ type ExchangeConfig struct { Testnet bool `json:"testnet,omitempty"` } -// SafeExchangeConfig 安全的交易所配置结构(不包含敏感信息) +// SafeExchangeConfig Safe exchange configuration structure (does not contain sensitive information) type SafeExchangeConfig struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` // "cex" or "dex" Enabled bool `json:"enabled"` Testnet bool `json:"testnet,omitempty"` - HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Hyperliquid钱包地址(不敏感) - AsterUser string `json:"asterUser"` // Aster用户名(不敏感) - AsterSigner string `json:"asterSigner"` // Aster签名者(不敏感) + HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Hyperliquid wallet address (not sensitive) + AsterUser string `json:"asterUser"` // Aster username (not sensitive) + AsterSigner string `json:"asterSigner"` // Aster signer (not sensitive) } type UpdateModelConfigRequest struct { @@ -421,7 +421,7 @@ type UpdateExchangeConfigRequest struct { Enabled bool `json:"enabled"` APIKey string `json:"api_key"` SecretKey string `json:"secret_key"` - Passphrase string `json:"passphrase"` // OKX专用 + Passphrase string `json:"passphrase"` // OKX specific Testnet bool `json:"testnet"` HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"` AsterUser string `json:"aster_user"` @@ -433,7 +433,7 @@ type UpdateExchangeConfigRequest struct { } `json:"exchanges"` } -// handleCreateTrader 创建新的AI交易员 +// handleCreateTrader Create new AI trader func (s *Server) handleCreateTrader(c *gin.Context) { userID := c.GetString("user_id") var req CreateTraderRequest @@ -442,40 +442,40 @@ func (s *Server) handleCreateTrader(c *gin.Context) { return } - // 校验杠杆值 + // Validate leverage values if req.BTCETHLeverage < 0 || req.BTCETHLeverage > 50 { - c.JSON(http.StatusBadRequest, gin.H{"error": "BTC/ETH杠杆必须在1-50倍之间"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "BTC/ETH leverage must be between 1-50x"}) return } if req.AltcoinLeverage < 0 || req.AltcoinLeverage > 20 { - c.JSON(http.StatusBadRequest, gin.H{"error": "山寨币杠杆必须在1-20倍之间"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Altcoin leverage must be between 1-20x"}) return } - // 校验交易币种格式 + // Validate trading symbol format if req.TradingSymbols != "" { symbols := strings.Split(req.TradingSymbols, ",") for _, symbol := range symbols { symbol = strings.TrimSpace(symbol) if symbol != "" && !strings.HasSuffix(strings.ToUpper(symbol), "USDT") { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("无效的币种格式: %s,必须以USDT结尾", symbol)}) + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid symbol format: %s, must end with USDT", symbol)}) return } } } - // 生成交易员ID + // Generate trader ID traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix()) - // 设置默认值 - isCrossMargin := true // 默认为全仓模式 + // Set default values + isCrossMargin := true // Default to cross margin mode if req.IsCrossMargin != nil { isCrossMargin = *req.IsCrossMargin } - // 设置杠杆默认值 - btcEthLeverage := 10 // 默认值 - altcoinLeverage := 5 // 默认值 + // Set leverage default values + btcEthLeverage := 10 // Default value + altcoinLeverage := 5 // Default value if req.BTCETHLeverage > 0 { btcEthLeverage = req.BTCETHLeverage } @@ -483,26 +483,26 @@ func (s *Server) handleCreateTrader(c *gin.Context) { altcoinLeverage = req.AltcoinLeverage } - // 设置系统提示词模板默认值 + // Set system prompt template default value systemPromptTemplate := "default" if req.SystemPromptTemplate != "" { systemPromptTemplate = req.SystemPromptTemplate } - // 设置扫描间隔默认值 + // Set scan interval default value scanIntervalMinutes := req.ScanIntervalMinutes if scanIntervalMinutes < 3 { - scanIntervalMinutes = 3 // 默认3分钟,且不允许小于3 + scanIntervalMinutes = 3 // Default 3 minutes, not allowed to be less than 3 } - // ✨ 查询交易所实际余额,覆盖用户输入 - actualBalance := req.InitialBalance // 默认使用用户输入 + // Query exchange actual balance, override user input + actualBalance := req.InitialBalance // Default to use user input exchanges, err := s.store.Exchange().List(userID) if err != nil { - logger.Infof("⚠️ 获取交易所配置失败,使用用户输入的初始资金: %v", err) + logger.Infof("⚠️ Failed to get exchange config, using user input for initial balance: %v", err) } - // 查找匹配的交易所配置 + // Find matching exchange configuration var exchangeCfg *store.Exchange for _, ex := range exchanges { if ex.ID == req.ExchangeID { @@ -512,11 +512,11 @@ func (s *Server) handleCreateTrader(c *gin.Context) { } if exchangeCfg == nil { - logger.Infof("⚠️ 未找到交易所 %s 的配置,使用用户输入的初始资金", req.ExchangeID) + logger.Infof("⚠️ Exchange %s configuration not found, using user input for initial balance", req.ExchangeID) } else if !exchangeCfg.Enabled { - logger.Infof("⚠️ 交易所 %s 未启用,使用用户输入的初始资金", req.ExchangeID) + logger.Infof("⚠️ Exchange %s not enabled, using user input for initial balance", req.ExchangeID) } else { - // 根据交易所类型创建临时 trader 查询余额 + // Create temporary trader based on exchange type to query balance var tempTrader trader.Trader var createErr error @@ -541,51 +541,51 @@ func (s *Server) handleCreateTrader(c *gin.Context) { exchangeCfg.SecretKey, ) default: - logger.Infof("⚠️ 不支持的交易所类型: %s,使用用户输入的初始资金", req.ExchangeID) + logger.Infof("⚠️ Unsupported exchange type: %s, using user input for initial balance", req.ExchangeID) } if createErr != nil { - logger.Infof("⚠️ 创建临时 trader 失败,使用用户输入的初始资金: %v", createErr) + logger.Infof("⚠️ Failed to create temporary trader, using user input for initial balance: %v", createErr) } else if tempTrader != nil { - // 查询实际余额 + // Query actual balance balanceInfo, balanceErr := tempTrader.GetBalance() if balanceErr != nil { - logger.Infof("⚠️ 查询交易所余额失败,使用用户输入的初始资金: %v", balanceErr) + logger.Infof("⚠️ Failed to query exchange balance, using user input for initial balance: %v", balanceErr) } else { - // 提取可用余额 - 支持多种字段名格式 + // Extract available balance - supports multiple field name formats if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 { - // Binance 格式: availableBalance (camelCase) + // Binance format: availableBalance (camelCase) actualBalance = availableBalance - logger.Infof("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) + logger.Infof("✓ Queried exchange actual balance: %.2f USDT (user input: %.2f USDT)", actualBalance, req.InitialBalance) } else if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 { - // 其他格式: available_balance (snake_case) + // Other format: available_balance (snake_case) actualBalance = availableBalance - logger.Infof("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) + logger.Infof("✓ Queried exchange actual balance: %.2f USDT (user input: %.2f USDT)", actualBalance, req.InitialBalance) } else if totalBalance, ok := balanceInfo["totalWalletBalance"].(float64); ok && totalBalance > 0 { - // Binance 格式: totalWalletBalance (camelCase) + // Binance format: totalWalletBalance (camelCase) actualBalance = totalBalance - logger.Infof("✓ 查询到交易所总余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) + logger.Infof("✓ Queried exchange total balance: %.2f USDT (user input: %.2f USDT)", actualBalance, req.InitialBalance) } else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 { - // 其他格式: balance + // Other format: balance actualBalance = totalBalance - logger.Infof("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) + logger.Infof("✓ Queried exchange actual balance: %.2f USDT (user input: %.2f USDT)", actualBalance, req.InitialBalance) } else { - logger.Infof("⚠️ 无法从余额信息中提取可用余额,balanceInfo=%v,使用用户输入的初始资金", balanceInfo) + logger.Infof("⚠️ Unable to extract available balance from balance info, balanceInfo=%v, using user input for initial balance", balanceInfo) } } } } - // 创建交易员配置(数据库实体) - logger.Infof("🔧 DEBUG: 开始创建交易员配置, ID=%s, Name=%s, AIModel=%s, Exchange=%s, StrategyID=%s", traderID, req.Name, req.AIModelID, req.ExchangeID, req.StrategyID) + // Create trader configuration (database entity) + logger.Infof("🔧 DEBUG: Starting to create trader config, ID=%s, Name=%s, AIModel=%s, Exchange=%s, StrategyID=%s", traderID, req.Name, req.AIModelID, req.ExchangeID, req.StrategyID) traderRecord := &store.Trader{ ID: traderID, UserID: userID, Name: req.Name, AIModelID: req.AIModelID, ExchangeID: req.ExchangeID, - StrategyID: req.StrategyID, // 关联策略ID(新版) - InitialBalance: actualBalance, // 使用实际查询的余额 + StrategyID: req.StrategyID, // Associated strategy ID (new version) + InitialBalance: actualBalance, // Use actual queried balance BTCETHLeverage: btcEthLeverage, AltcoinLeverage: altcoinLeverage, TradingSymbols: req.TradingSymbols, @@ -599,26 +599,26 @@ func (s *Server) handleCreateTrader(c *gin.Context) { IsRunning: false, } - // 保存到数据库 - logger.Infof("🔧 DEBUG: 准备调用 CreateTrader") + // Save to database + logger.Infof("🔧 DEBUG: Preparing to call CreateTrader") err = s.store.Trader().Create(traderRecord) if err != nil { - logger.Infof("❌ 创建交易员失败: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)}) + logger.Infof("❌ Failed to create trader: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create trader: %v", err)}) return } - logger.Infof("🔧 DEBUG: CreateTrader 成功") + logger.Infof("🔧 DEBUG: CreateTrader succeeded") - // 立即将新交易员加载到TraderManager中 - logger.Infof("🔧 DEBUG: 准备调用 LoadUserTraders") + // Immediately load new trader into TraderManager + logger.Infof("🔧 DEBUG: Preparing to call LoadUserTraders") err = s.traderManager.LoadUserTradersFromStore(s.store, userID) if err != nil { - logger.Infof("⚠️ 加载用户交易员到内存失败: %v", err) - // 这里不返回错误,因为交易员已经成功创建到数据库 + logger.Infof("⚠️ Failed to load user traders into memory: %v", err) + // Don't return error here since trader was successfully created in database } - logger.Infof("🔧 DEBUG: LoadUserTraders 完成") + logger.Infof("🔧 DEBUG: LoadUserTraders completed") - logger.Infof("✓ 创建交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID) + logger.Infof("✓ Trader created successfully: %s (model: %s, exchange: %s)", req.Name, req.AIModelID, req.ExchangeID) c.JSON(http.StatusCreated, gin.H{ "trader_id": traderID, @@ -628,16 +628,16 @@ func (s *Server) handleCreateTrader(c *gin.Context) { }) } -// UpdateTraderRequest 更新交易员请求 +// UpdateTraderRequest Update trader request type UpdateTraderRequest struct { Name string `json:"name" binding:"required"` AIModelID string `json:"ai_model_id" binding:"required"` ExchangeID string `json:"exchange_id" binding:"required"` - StrategyID string `json:"strategy_id"` // 策略ID(新版) + StrategyID string `json:"strategy_id"` // Strategy ID (new version) InitialBalance float64 `json:"initial_balance"` ScanIntervalMinutes int `json:"scan_interval_minutes"` IsCrossMargin *bool `json:"is_cross_margin"` - // 以下字段为向后兼容保留,新版使用策略配置 + // The following fields are kept for backward compatibility, new version uses strategy config BTCETHLeverage int `json:"btc_eth_leverage"` AltcoinLeverage int `json:"altcoin_leverage"` TradingSymbols string `json:"trading_symbols"` @@ -646,7 +646,7 @@ type UpdateTraderRequest struct { SystemPromptTemplate string `json:"system_prompt_template"` } -// handleUpdateTrader 更新交易员配置 +// handleUpdateTrader Update trader configuration func (s *Server) handleUpdateTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") @@ -657,10 +657,10 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { return } - // 检查交易员是否存在且属于当前用户 + // Check if trader exists and belongs to current user traders, err := s.store.Trader().List(userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "获取交易员列表失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get trader list"}) return } @@ -673,54 +673,54 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { } if existingTrader == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Trader does not exist"}) return } - // 设置默认值 - isCrossMargin := existingTrader.IsCrossMargin // 保持原值 + // Set default values + isCrossMargin := existingTrader.IsCrossMargin // Keep original value if req.IsCrossMargin != nil { isCrossMargin = *req.IsCrossMargin } - // 设置杠杆默认值 + // Set leverage default values btcEthLeverage := req.BTCETHLeverage altcoinLeverage := req.AltcoinLeverage if btcEthLeverage <= 0 { - btcEthLeverage = existingTrader.BTCETHLeverage // 保持原值 + btcEthLeverage = existingTrader.BTCETHLeverage // Keep original value } if altcoinLeverage <= 0 { - altcoinLeverage = existingTrader.AltcoinLeverage // 保持原值 + altcoinLeverage = existingTrader.AltcoinLeverage // Keep original value } - // 设置扫描间隔,允许更新 + // Set scan interval, allow updates scanIntervalMinutes := req.ScanIntervalMinutes if scanIntervalMinutes <= 0 { - scanIntervalMinutes = existingTrader.ScanIntervalMinutes // 保持原值 + scanIntervalMinutes = existingTrader.ScanIntervalMinutes // Keep original value } else if scanIntervalMinutes < 3 { scanIntervalMinutes = 3 } - // 设置系统提示词模板 + // Set system prompt template systemPromptTemplate := req.SystemPromptTemplate if systemPromptTemplate == "" { - systemPromptTemplate = existingTrader.SystemPromptTemplate // 保持原值 + systemPromptTemplate = existingTrader.SystemPromptTemplate // Keep original value } - // 处理策略ID(如果没有提供,保持原值) + // Handle strategy ID (if not provided, keep original value) strategyID := req.StrategyID if strategyID == "" { strategyID = existingTrader.StrategyID } - // 更新交易员配置 + // Update trader configuration traderRecord := &store.Trader{ ID: traderID, UserID: userID, Name: req.Name, AIModelID: req.AIModelID, ExchangeID: req.ExchangeID, - StrategyID: strategyID, // 关联策略ID + StrategyID: strategyID, // Associated strategy ID InitialBalance: req.InitialBalance, BTCETHLeverage: btcEthLeverage, AltcoinLeverage: altcoinLeverage, @@ -730,177 +730,177 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { SystemPromptTemplate: systemPromptTemplate, IsCrossMargin: isCrossMargin, ScanIntervalMinutes: scanIntervalMinutes, - IsRunning: existingTrader.IsRunning, // 保持原值 + IsRunning: existingTrader.IsRunning, // Keep original value } - // 更新数据库 + // Update database err = s.store.Trader().Update(traderRecord) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易员失败: %v", err)}) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update trader: %v", err)}) return } - // 重新加载交易员到内存 + // Reload traders into memory err = s.traderManager.LoadUserTradersFromStore(s.store, userID) if err != nil { - logger.Infof("⚠️ 重新加载用户交易员到内存失败: %v", err) + logger.Infof("⚠️ Failed to reload user traders into memory: %v", err) } - logger.Infof("✓ 更新交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID) + logger.Infof("✓ Trader updated successfully: %s (model: %s, exchange: %s)", req.Name, req.AIModelID, req.ExchangeID) c.JSON(http.StatusOK, gin.H{ "trader_id": traderID, "trader_name": req.Name, "ai_model": req.AIModelID, - "message": "交易员更新成功", + "message": "Trader updated successfully", }) } -// handleDeleteTrader 删除交易员 +// handleDeleteTrader Delete trader func (s *Server) handleDeleteTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - // 从数据库删除 + // Delete from database err := s.store.Trader().Delete(userID, traderID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)}) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete trader: %v", err)}) return } - // 如果交易员正在运行,先停止它 + // If trader is running, stop it first if trader, err := s.traderManager.GetTrader(traderID); err == nil { status := trader.GetStatus() if isRunning, ok := status["is_running"].(bool); ok && isRunning { trader.Stop() - logger.Infof("⏹ 已停止运行中的交易员: %s", traderID) + logger.Infof("⏹ Stopped running trader: %s", traderID) } } - logger.Infof("✓ 交易员已删除: %s", traderID) - c.JSON(http.StatusOK, gin.H{"message": "交易员已删除"}) + logger.Infof("✓ Trader deleted: %s", traderID) + c.JSON(http.StatusOK, gin.H{"message": "Trader deleted"}) } -// handleStartTrader 启动交易员 +// handleStartTrader Start trader func (s *Server) handleStartTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - // 校验交易员是否属于当前用户 + // Verify trader belongs to current user _, err := s.store.Trader().GetFullConfig(userID, traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Trader does not exist or no access permission"}) return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - // 交易员不在内存中,尝试从数据库加载 - logger.Infof("🔄 交易员 %s 不在内存中,尝试加载...", traderID) + // Trader not in memory, try loading from database + logger.Infof("🔄 Trader %s not in memory, trying to load...", traderID) if loadErr := s.traderManager.LoadUserTradersFromStore(s.store, userID); loadErr != nil { - logger.Infof("❌ 加载用户交易员失败: %v", loadErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": "加载交易员失败: " + loadErr.Error()}) + logger.Infof("❌ Failed to load user traders: %v", loadErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load trader: " + loadErr.Error()}) return } - // 再次尝试获取 + // Try to get trader again trader, err = s.traderManager.GetTrader(traderID) if err != nil { - // 检查详细原因 + // Check detailed reason fullCfg, _ := s.store.Trader().GetFullConfig(userID, traderID) if fullCfg != nil && fullCfg.Trader != nil { - // 检查策略 + // Check strategy if fullCfg.Strategy == nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易员未配置策略,请先在策略工作室创建策略并关联到交易员"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader has no strategy configured, please create a strategy in Strategy Studio and associate it with the trader"}) return } - // 检查AI模型 + // Check AI model if fullCfg.AIModel == nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易员的AI模型不存在,请检查AI模型配置"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's AI model does not exist, please check AI model configuration"}) return } if !fullCfg.AIModel.Enabled { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易员的AI模型未启用,请先启用AI模型"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's AI model is not enabled, please enable the AI model first"}) return } - // 检查交易所 + // Check exchange if fullCfg.Exchange == nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易员的交易所不存在,请检查交易所配置"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's exchange does not exist, please check exchange configuration"}) return } if !fullCfg.Exchange.Enabled { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易员的交易所未启用,请先启用交易所"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader's exchange is not enabled, please enable the exchange first"}) return } } - c.JSON(http.StatusNotFound, gin.H{"error": "交易员加载失败,请检查AI模型、交易所和策略配置"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Failed to load trader, please check AI model, exchange and strategy configuration"}) return } } - // 检查交易员是否已经在运行 + // Check if trader is already running status := trader.GetStatus() if isRunning, ok := status["is_running"].(bool); ok && isRunning { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已在运行中"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader is already running"}) return } - // 启动交易员 + // Start trader go func() { - logger.Infof("▶️ 启动交易员 %s (%s)", traderID, trader.GetName()) + logger.Infof("▶️ Starting trader %s (%s)", traderID, trader.GetName()) if err := trader.Run(); err != nil { - logger.Infof("❌ 交易员 %s 运行错误: %v", trader.GetName(), err) + logger.Infof("❌ Trader %s runtime error: %v", trader.GetName(), err) } }() - // 更新数据库中的运行状态 + // Update running status in database err = s.store.Trader().UpdateStatus(userID, traderID, true) if err != nil { - logger.Infof("⚠️ 更新交易员状态失败: %v", err) + logger.Infof("⚠️ Failed to update trader status: %v", err) } - logger.Infof("✓ 交易员 %s 已启动", trader.GetName()) - c.JSON(http.StatusOK, gin.H{"message": "交易员已启动"}) + logger.Infof("✓ Trader %s started", trader.GetName()) + c.JSON(http.StatusOK, gin.H{"message": "Trader started"}) } -// handleStopTrader 停止交易员 +// handleStopTrader Stop trader func (s *Server) handleStopTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - // 校验交易员是否属于当前用户 + // Verify trader belongs to current user _, err := s.store.Trader().GetFullConfig(userID, traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Trader does not exist or no access permission"}) return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Trader does not exist"}) return } - // 检查交易员是否正在运行 + // Check if trader is running status := trader.GetStatus() if isRunning, ok := status["is_running"].(bool); ok && !isRunning { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已停止"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader is already stopped"}) return } - // 停止交易员 + // Stop trader trader.Stop() - // 更新数据库中的运行状态 + // Update running status in database err = s.store.Trader().UpdateStatus(userID, traderID, false) if err != nil { - logger.Infof("⚠️ 更新交易员状态失败: %v", err) + logger.Infof("⚠️ Failed to update trader status: %v", err) } - logger.Infof("⏹ 交易员 %s 已停止", trader.GetName()) - c.JSON(http.StatusOK, gin.H{"message": "交易员已停止"}) + logger.Infof("⏹ Trader %s stopped", trader.GetName()) + c.JSON(http.StatusOK, gin.H{"message": "Trader stopped"}) } -// handleUpdateTraderPrompt 更新交易员自定义Prompt +// handleUpdateTraderPrompt Update trader custom prompt func (s *Server) handleUpdateTraderPrompt(c *gin.Context) { traderID := c.Param("id") userID := c.GetString("user_id") @@ -915,35 +915,35 @@ func (s *Server) handleUpdateTraderPrompt(c *gin.Context) { return } - // 更新数据库 + // Update database err := s.store.Trader().UpdateCustomPrompt(userID, traderID, req.CustomPrompt, req.OverrideBasePrompt) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新自定义prompt失败: %v", err)}) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update custom prompt: %v", err)}) return } - // 如果trader在内存中,更新其custom prompt和override设置 + // If trader is in memory, update its custom prompt and override settings trader, err := s.traderManager.GetTrader(traderID) if err == nil { trader.SetCustomPrompt(req.CustomPrompt) trader.SetOverrideBasePrompt(req.OverrideBasePrompt) - logger.Infof("✓ 已更新交易员 %s 的自定义prompt (覆盖基础=%v)", trader.GetName(), req.OverrideBasePrompt) + logger.Infof("✓ Updated trader %s custom prompt (override base=%v)", trader.GetName(), req.OverrideBasePrompt) } - c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"}) + c.JSON(http.StatusOK, gin.H{"message": "Custom prompt updated"}) } -// handleSyncBalance 同步交易所余额到initial_balance(选项B:手动同步 + 选项C:智能检测) +// handleSyncBalance Sync exchange balance to initial_balance (Option B: Manual Sync + Option C: Smart Detection) func (s *Server) handleSyncBalance(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - logger.Infof("🔄 用户 %s 请求同步交易员 %s 的余额", userID, traderID) + logger.Infof("🔄 User %s requested balance sync for trader %s", userID, traderID) - // 从数据库获取交易员配置(包含交易所信息) + // Get trader configuration from database (including exchange info) fullConfig, err := s.store.Trader().GetFullConfig(userID, traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Trader does not exist"}) return } @@ -951,11 +951,11 @@ func (s *Server) handleSyncBalance(c *gin.Context) { exchangeCfg := fullConfig.Exchange if exchangeCfg == nil || !exchangeCfg.Enabled { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易所未配置或未启用"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Exchange not configured or not enabled"}) return } - // 创建临时 trader 查询余额 + // Create temporary trader to query balance var tempTrader trader.Trader var createErr error @@ -980,25 +980,25 @@ func (s *Server) handleSyncBalance(c *gin.Context) { exchangeCfg.SecretKey, ) default: - c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的交易所类型"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported exchange type"}) return } if createErr != nil { - logger.Infof("⚠️ 创建临时 trader 失败: %v", createErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("连接交易所失败: %v", createErr)}) + logger.Infof("⚠️ Failed to create temporary trader: %v", createErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to connect to exchange: %v", createErr)}) return } - // 查询实际余额 + // Query actual balance balanceInfo, balanceErr := tempTrader.GetBalance() if balanceErr != nil { - logger.Infof("⚠️ 查询交易所余额失败: %v", balanceErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("查询余额失败: %v", balanceErr)}) + logger.Infof("⚠️ Failed to query exchange balance: %v", balanceErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to query balance: %v", balanceErr)}) return } - // 提取可用余额 + // Extract available balance var actualBalance float64 if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 { actualBalance = availableBalance @@ -1007,40 +1007,40 @@ func (s *Server) handleSyncBalance(c *gin.Context) { } else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 { actualBalance = totalBalance } else { - c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取可用余额"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to get available balance"}) return } oldBalance := traderConfig.InitialBalance - // ✅ 选项C:智能检测余额变化 + // ✅ Option C: Smart balance change detection changePercent := ((actualBalance - oldBalance) / oldBalance) * 100 - changeType := "增加" + changeType := "increase" if changePercent < 0 { - changeType = "减少" + changeType = "decrease" } - logger.Infof("✓ 查询到交易所实际余额: %.2f USDT (当前配置: %.2f USDT, 变化: %.2f%%)", + logger.Infof("✓ Queried actual exchange balance: %.2f USDT (current config: %.2f USDT, change: %.2f%%)", actualBalance, oldBalance, changePercent) - // 更新数据库中的 initial_balance + // Update initial_balance in database err = s.store.Trader().UpdateInitialBalance(userID, traderID, actualBalance) if err != nil { - logger.Infof("❌ 更新initial_balance失败: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "更新余额失败"}) + logger.Infof("❌ Failed to update initial_balance: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update balance"}) return } - // 重新加载交易员到内存 + // Reload traders into memory err = s.traderManager.LoadUserTradersFromStore(s.store, userID) if err != nil { - logger.Infof("⚠️ 重新加载用户交易员到内存失败: %v", err) + logger.Infof("⚠️ Failed to reload user traders into memory: %v", err) } - logger.Infof("✅ 已同步余额: %.2f → %.2f USDT (%s %.2f%%)", oldBalance, actualBalance, changeType, changePercent) + logger.Infof("✅ Synced balance: %.2f → %.2f USDT (%s %.2f%%)", oldBalance, actualBalance, changeType, changePercent) c.JSON(http.StatusOK, gin.H{ - "message": "余额同步成功", + "message": "Balance synced successfully", "old_balance": oldBalance, "new_balance": actualBalance, "change_percent": changePercent, @@ -1048,7 +1048,7 @@ func (s *Server) handleSyncBalance(c *gin.Context) { }) } -// handleClosePosition 一键平仓 +// handleClosePosition One-click close position func (s *Server) handleClosePosition(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") @@ -1059,16 +1059,16 @@ func (s *Server) handleClosePosition(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: symbol和side必填"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Parameter error: symbol and side are required"}) return } - logger.Infof("🔻 用户 %s 请求平仓: trader=%s, symbol=%s, side=%s", userID, traderID, req.Symbol, req.Side) + logger.Infof("🔻 User %s requested position close: trader=%s, symbol=%s, side=%s", userID, traderID, req.Symbol, req.Side) - // 从数据库获取交易员配置(包含交易所信息) + // Get trader configuration from database (including exchange info) fullConfig, err := s.store.Trader().GetFullConfig(userID, traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Trader does not exist"}) return } @@ -1076,11 +1076,11 @@ func (s *Server) handleClosePosition(c *gin.Context) { exchangeCfg := fullConfig.Exchange if exchangeCfg == nil || !exchangeCfg.Enabled { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易所未配置或未启用"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Exchange not configured or not enabled"}) return } - // 创建临时 trader 执行平仓 + // Create temporary trader to execute close position var tempTrader trader.Trader var createErr error @@ -1126,57 +1126,57 @@ func (s *Server) handleClosePosition(c *gin.Context) { ) } default: - c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的交易所类型"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported exchange type"}) return } if createErr != nil { - logger.Infof("⚠️ 创建临时 trader 失败: %v", createErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("连接交易所失败: %v", createErr)}) + logger.Infof("⚠️ Failed to create temporary trader: %v", createErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to connect to exchange: %v", createErr)}) return } - // 执行平仓操作 + // Execute close position operation var result map[string]interface{} var closeErr error if req.Side == "LONG" { - result, closeErr = tempTrader.CloseLong(req.Symbol, 0) // 0 表示全部平仓 + result, closeErr = tempTrader.CloseLong(req.Symbol, 0) // 0 means close all } else if req.Side == "SHORT" { - result, closeErr = tempTrader.CloseShort(req.Symbol, 0) // 0 表示全部平仓 + result, closeErr = tempTrader.CloseShort(req.Symbol, 0) // 0 means close all } else { - c.JSON(http.StatusBadRequest, gin.H{"error": "side必须是LONG或SHORT"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "side must be LONG or SHORT"}) return } if closeErr != nil { - logger.Infof("❌ 平仓失败: symbol=%s, side=%s, error=%v", req.Symbol, req.Side, closeErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("平仓失败: %v", closeErr)}) + logger.Infof("❌ Close position failed: symbol=%s, side=%s, error=%v", req.Symbol, req.Side, closeErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to close position: %v", closeErr)}) return } - logger.Infof("✅ 平仓成功: symbol=%s, side=%s, result=%v", req.Symbol, req.Side, result) + logger.Infof("✅ Position closed successfully: symbol=%s, side=%s, result=%v", req.Symbol, req.Side, result) c.JSON(http.StatusOK, gin.H{ - "message": "平仓成功", + "message": "Position closed successfully", "symbol": req.Symbol, "side": req.Side, "result": result, }) } -// handleGetModelConfigs 获取AI模型配置 +// handleGetModelConfigs Get AI model configurations func (s *Server) handleGetModelConfigs(c *gin.Context) { userID := c.GetString("user_id") - logger.Infof("🔍 查询用户 %s 的AI模型配置", userID) + logger.Infof("🔍 Querying AI model configs for user %s", userID) models, err := s.store.AIModel().List(userID) if err != nil { - logger.Infof("❌ 获取AI模型配置失败: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取AI模型配置失败: %v", err)}) + logger.Infof("❌ Failed to get AI model configs: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get AI model configs: %v", err)}) return } - logger.Infof("✅ 找到 %d 个AI模型配置", len(models)) + logger.Infof("✅ Found %d AI model configs", len(models)) - // 转换为安全的响应结构,移除敏感信息 + // Convert to safe response structure, remove sensitive information safeModels := make([]SafeModelConfig, len(models)) for i, model := range models { safeModels[i] = SafeModelConfig{ @@ -1192,86 +1192,86 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) { c.JSON(http.StatusOK, safeModels) } -// handleUpdateModelConfigs 更新AI模型配置(仅支持加密数据) +// handleUpdateModelConfigs Update AI model configurations (encrypted data only) func (s *Server) handleUpdateModelConfigs(c *gin.Context) { userID := c.GetString("user_id") - // 读取原始请求体 + // Read raw request body bodyBytes, err := c.GetRawData() if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) return } - // 解析加密的 payload + // Parse encrypted payload var encryptedPayload crypto.EncryptedPayload if err := json.Unmarshal(bodyBytes, &encryptedPayload); err != nil { - logger.Infof("❌ 解析加密载荷失败: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误,必须使用加密传输"}) + logger.Infof("❌ Failed to parse encrypted payload: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format, encrypted transmission required"}) return } - // 验证是否为加密数据 + // Verify encrypted data if encryptedPayload.WrappedKey == "" { - logger.Infof("❌ 检测到非加密请求 (UserID: %s)", userID) + logger.Infof("❌ Detected unencrypted request (UserID: %s)", userID) c.JSON(http.StatusBadRequest, gin.H{ - "error": "此接口仅支持加密传输,请使用加密客户端", + "error": "This endpoint only supports encrypted transmission, please use encrypted client", "code": "ENCRYPTION_REQUIRED", "message": "Encrypted transmission is required for security reasons", }) return } - // 解密数据 + // Decrypt data decrypted, err := s.cryptoHandler.cryptoService.DecryptSensitiveData(&encryptedPayload) if err != nil { - logger.Infof("❌ 解密模型配置失败 (UserID: %s): %v", userID, err) - c.JSON(http.StatusBadRequest, gin.H{"error": "解密数据失败"}) + logger.Infof("❌ Failed to decrypt model config (UserID: %s): %v", userID, err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to decrypt data"}) return } - // 解析解密后的数据 + // Parse decrypted data var req UpdateModelConfigRequest if err := json.Unmarshal([]byte(decrypted), &req); err != nil { - logger.Infof("❌ 解析解密数据失败: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "解析解密数据失败"}) + logger.Infof("❌ Failed to parse decrypted data: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse decrypted data"}) return } - logger.Infof("🔓 已解密模型配置数据 (UserID: %s)", userID) + logger.Infof("🔓 Decrypted model config data (UserID: %s)", userID) - // 更新每个模型的配置 + // Update each model's configuration for modelID, modelData := range req.Models { err := s.store.AIModel().Update(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL, modelData.CustomModelName) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)}) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update model %s: %v", modelID, err)}) return } } - // 重新加载该用户的所有交易员,使新配置立即生效 + // Reload all traders for this user to make new config take effect immediately err = s.traderManager.LoadUserTradersFromStore(s.store, userID) if err != nil { - logger.Infof("⚠️ 重新加载用户交易员到内存失败: %v", err) - // 这里不返回错误,因为模型配置已经成功更新到数据库 + logger.Infof("⚠️ Failed to reload user traders into memory: %v", err) + // Don't return error here since model config was successfully updated to database } - logger.Infof("✓ AI模型配置已更新: %+v", req.Models) - c.JSON(http.StatusOK, gin.H{"message": "模型配置已更新"}) + logger.Infof("✓ AI model config updated: %+v", req.Models) + c.JSON(http.StatusOK, gin.H{"message": "Model configuration updated"}) } -// handleGetExchangeConfigs 获取交易所配置 +// handleGetExchangeConfigs Get exchange configurations func (s *Server) handleGetExchangeConfigs(c *gin.Context) { userID := c.GetString("user_id") - logger.Infof("🔍 查询用户 %s 的交易所配置", userID) + logger.Infof("🔍 Querying exchange configs for user %s", userID) exchanges, err := s.store.Exchange().List(userID) if err != nil { - logger.Infof("❌ 获取交易所配置失败: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易所配置失败: %v", err)}) + logger.Infof("❌ Failed to get exchange configs: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get exchange configs: %v", err)}) return } - logger.Infof("✅ 找到 %d 个交易所配置", len(exchanges)) - - // 调试:输出配置详情(脱敏) + logger.Infof("✅ Found %d exchange configs", len(exchanges)) + + // Debug: Output config details (masked) for _, ex := range exchanges { apiKeyMasked := "" if len(ex.APIKey) > 8 { @@ -1281,14 +1281,14 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) { if len(ex.SecretKey) > 8 { secretKeyMasked = ex.SecretKey[:8] + "..." } - logger.Infof(" └─ 交易所: %s, APIKey: %s, SecretKey: %s", ex.ID, apiKeyMasked, secretKeyMasked) + logger.Infof(" └─ Exchange: %s, APIKey: %s, SecretKey: %s", ex.ID, apiKeyMasked, secretKeyMasked) } - // 打印完整JSON响应用于调试 + // Print complete JSON response for debugging jsonData, _ := json.Marshal(exchanges) - logger.Infof("📤 完整JSON响应: %s", string(jsonData)) + logger.Infof("📤 Complete JSON response: %s", string(jsonData)) - // 转换为安全的响应结构,移除敏感信息 + // Convert to safe response structure, remove sensitive information safeExchanges := make([]SafeExchangeConfig, len(exchanges)) for i, exchange := range exchanges { safeExchanges[i] = SafeExchangeConfig{ @@ -1306,85 +1306,85 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) { c.JSON(http.StatusOK, safeExchanges) } -// handleUpdateExchangeConfigs 更新交易所配置(仅支持加密数据) +// handleUpdateExchangeConfigs Update exchange configurations (encrypted data only) func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { userID := c.GetString("user_id") - // 读取原始请求体 + // Read raw request body bodyBytes, err := c.GetRawData() if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) return } - // 解析加密的 payload + // Parse encrypted payload var encryptedPayload crypto.EncryptedPayload if err := json.Unmarshal(bodyBytes, &encryptedPayload); err != nil { - logger.Infof("❌ 解析加密载荷失败: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误,必须使用加密传输"}) + logger.Infof("❌ Failed to parse encrypted payload: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format, encrypted transmission required"}) return } - // 验证是否为加密数据 + // Verify encrypted data if encryptedPayload.WrappedKey == "" { - logger.Infof("❌ 检测到非加密请求 (UserID: %s)", userID) + logger.Infof("❌ Detected unencrypted request (UserID: %s)", userID) c.JSON(http.StatusBadRequest, gin.H{ - "error": "此接口仅支持加密传输,请使用加密客户端", + "error": "This endpoint only supports encrypted transmission, please use encrypted client", "code": "ENCRYPTION_REQUIRED", "message": "Encrypted transmission is required for security reasons", }) return } - // 解密数据 + // Decrypt data decrypted, err := s.cryptoHandler.cryptoService.DecryptSensitiveData(&encryptedPayload) if err != nil { - logger.Infof("❌ 解密交易所配置失败 (UserID: %s): %v", userID, err) - c.JSON(http.StatusBadRequest, gin.H{"error": "解密数据失败"}) + logger.Infof("❌ Failed to decrypt exchange config (UserID: %s): %v", userID, err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to decrypt data"}) return } - // 解析解密后的数据 + // Parse decrypted data var req UpdateExchangeConfigRequest if err := json.Unmarshal([]byte(decrypted), &req); err != nil { - logger.Infof("❌ 解析解密数据失败: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "解析解密数据失败"}) + logger.Infof("❌ Failed to parse decrypted data: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse decrypted data"}) return } - logger.Infof("🔓 已解密交易所配置数据 (UserID: %s)", userID) + logger.Infof("🔓 Decrypted exchange config data (UserID: %s)", userID) - // 更新每个交易所的配置 + // Update each exchange's configuration for exchangeID, exchangeData := range req.Exchanges { err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Passphrase, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.LighterAPIKeyPrivateKey) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易所 %s 失败: %v", exchangeID, err)}) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update exchange %s: %v", exchangeID, err)}) return } } - // 重新加载该用户的所有交易员,使新配置立即生效 + // Reload all traders for this user to make new config take effect immediately err = s.traderManager.LoadUserTradersFromStore(s.store, userID) if err != nil { - logger.Infof("⚠️ 重新加载用户交易员到内存失败: %v", err) - // 这里不返回错误,因为交易所配置已经成功更新到数据库 + logger.Infof("⚠️ Failed to reload user traders into memory: %v", err) + // Don't return error here since exchange config was successfully updated to database } - logger.Infof("✓ 交易所配置已更新: %+v", req.Exchanges) - c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"}) + logger.Infof("✓ Exchange config updated: %+v", req.Exchanges) + c.JSON(http.StatusOK, gin.H{"message": "Exchange configuration updated"}) } -// handleTraderList trader列表 +// handleTraderList Trader list func (s *Server) handleTraderList(c *gin.Context) { userID := c.GetString("user_id") traders, err := s.store.Trader().List(userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易员列表失败: %v", err)}) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get trader list: %v", err)}) return } result := make([]map[string]interface{}, 0, len(traders)) for _, trader := range traders { - // 获取实时运行状态 + // Get real-time running status isRunning := trader.IsRunning if at, err := s.traderManager.GetTrader(trader.ID); err == nil { status := at.GetStatus() @@ -1393,12 +1393,12 @@ func (s *Server) handleTraderList(c *gin.Context) { } } - // 返回完整的 AIModelID(如 "admin_deepseek"),不要截断 - // 前端需要完整 ID 来验证模型是否存在(与 handleGetTraderConfig 保持一致) + // Return complete AIModelID (e.g. "admin_deepseek"), don't truncate + // Frontend needs complete ID to verify model exists (consistent with handleGetTraderConfig) result = append(result, map[string]interface{}{ "trader_id": trader.ID, "trader_name": trader.Name, - "ai_model": trader.AIModelID, // 使用完整 ID + "ai_model": trader.AIModelID, // Use complete ID "exchange_id": trader.ExchangeID, "is_running": isRunning, "initial_balance": trader.InitialBalance, @@ -1408,24 +1408,24 @@ func (s *Server) handleTraderList(c *gin.Context) { c.JSON(http.StatusOK, result) } -// handleGetTraderConfig 获取交易员详细配置 +// handleGetTraderConfig Get trader detailed configuration func (s *Server) handleGetTraderConfig(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") if traderID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易员ID不能为空"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader ID cannot be empty"}) return } fullCfg, err := s.store.Trader().GetFullConfig(userID, traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("获取交易员配置失败: %v", err)}) + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("Failed to get trader config: %v", err)}) return } traderConfig := fullCfg.Trader - // 获取实时运行状态 + // Get real-time running status isRunning := traderConfig.IsRunning if at, err := s.traderManager.GetTrader(traderID); err == nil { status := at.GetStatus() @@ -1434,7 +1434,7 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) { } } - // 返回完整的模型ID,不做转换,保持与前端模型列表一致 + // Return complete model ID without conversion, consistent with frontend model list aiModelID := traderConfig.AIModelID result := map[string]interface{}{ @@ -1458,7 +1458,7 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) { c.JSON(http.StatusOK, result) } -// handleStatus 系统状态 +// handleStatus System status func (s *Server) handleStatus(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { @@ -1476,7 +1476,7 @@ func (s *Server) handleStatus(c *gin.Context) { c.JSON(http.StatusOK, status) } -// handleAccount 账户信息 +// handleAccount Account information func (s *Server) handleAccount(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { @@ -1490,17 +1490,17 @@ func (s *Server) handleAccount(c *gin.Context) { return } - logger.Infof("📊 收到账户信息请求 [%s]", trader.GetName()) + logger.Infof("📊 Received account info request [%s]", trader.GetName()) account, err := trader.GetAccountInfo() if err != nil { - logger.Infof("❌ 获取账户信息失败 [%s]: %v", trader.GetName(), err) + logger.Infof("❌ Failed to get account info [%s]: %v", trader.GetName(), err) c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取账户信息失败: %v", err), + "error": fmt.Sprintf("Failed to get account info: %v", err), }) return } - logger.Infof("✓ 返回账户信息 [%s]: 净值=%.2f, 可用=%.2f, 盈亏=%.2f (%.2f%%)", + logger.Infof("✓ Returning account info [%s]: equity=%.2f, available=%.2f, pnl=%.2f (%.2f%%)", trader.GetName(), account["total_equity"], account["available_balance"], @@ -1509,7 +1509,7 @@ func (s *Server) handleAccount(c *gin.Context) { c.JSON(http.StatusOK, account) } -// handlePositions 持仓列表 +// handlePositions Position list func (s *Server) handlePositions(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { @@ -1526,7 +1526,7 @@ func (s *Server) handlePositions(c *gin.Context) { positions, err := trader.GetPositions() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取持仓列表失败: %v", err), + "error": fmt.Sprintf("Failed to get position list: %v", err), }) return } @@ -1534,7 +1534,7 @@ func (s *Server) handlePositions(c *gin.Context) { c.JSON(http.StatusOK, positions) } -// handleDecisions 决策日志列表 +// handleDecisions Decision log list func (s *Server) handleDecisions(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { @@ -1548,11 +1548,11 @@ func (s *Server) handleDecisions(c *gin.Context) { return } - // 获取所有历史决策记录(无限制) + // Get all historical decision records (unlimited) records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), 10000) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取决策日志失败: %v", err), + "error": fmt.Sprintf("Failed to get decision log: %v", err), }) return } @@ -1560,7 +1560,7 @@ func (s *Server) handleDecisions(c *gin.Context) { c.JSON(http.StatusOK, records) } -// handleLatestDecisions 最新决策日志(最近5条,最新的在前) +// handleLatestDecisions Latest decision logs (most recent 5, newest first) func (s *Server) handleLatestDecisions(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { @@ -1577,13 +1577,13 @@ func (s *Server) handleLatestDecisions(c *gin.Context) { records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), 5) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取决策日志失败: %v", err), + "error": fmt.Sprintf("Failed to get decision log: %v", err), }) return } - // 反转数组,让最新的在前面(用于列表显示) - // GetLatestRecords返回的是从旧到新(用于图表),这里需要从新到旧 + // Reverse array to put newest first (for list display) + // GetLatestRecords returns oldest to newest (for charts), here we need newest to oldest for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 { records[i], records[j] = records[j], records[i] } @@ -1591,7 +1591,7 @@ func (s *Server) handleLatestDecisions(c *gin.Context) { c.JSON(http.StatusOK, records) } -// handleStatistics 统计信息 +// handleStatistics Statistics information func (s *Server) handleStatistics(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { @@ -1608,7 +1608,7 @@ func (s *Server) handleStatistics(c *gin.Context) { stats, err := trader.GetStore().Decision().GetStatistics(trader.GetID()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取统计信息失败: %v", err), + "error": fmt.Sprintf("Failed to get statistics: %v", err), }) return } @@ -1616,20 +1616,20 @@ func (s *Server) handleStatistics(c *gin.Context) { c.JSON(http.StatusOK, stats) } -// handleCompetition 竞赛总览(对比所有trader) +// handleCompetition Competition overview (compare all traders) func (s *Server) handleCompetition(c *gin.Context) { userID := c.GetString("user_id") - // 确保用户的交易员已加载到内存中 + // Ensure user's traders are loaded into memory err := s.traderManager.LoadUserTradersFromStore(s.store, userID) if err != nil { - logger.Infof("⚠️ 加载用户 %s 的交易员失败: %v", userID, err) + logger.Infof("⚠️ Failed to load traders for user %s: %v", userID, err) } competition, err := s.traderManager.GetCompetitionData() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取竞赛数据失败: %v", err), + "error": fmt.Sprintf("Failed to get competition data: %v", err), }) return } @@ -1637,8 +1637,8 @@ func (s *Server) handleCompetition(c *gin.Context) { c.JSON(http.StatusOK, competition) } -// handleEquityHistory 收益率历史数据 -// 直接从数据库查询,不依赖内存中的 trader(这样重启后也能获取历史数据) +// handleEquityHistory Return rate historical data +// Query directly from database, not dependent on trader in memory (so historical data can be retrieved after restart) func (s *Server) handleEquityHistory(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { @@ -1646,12 +1646,12 @@ func (s *Server) handleEquityHistory(c *gin.Context) { return } - // 从新的 equity 表获取净值历史数据 - // 每3分钟一个周期:10000条 = 约20天的数据 + // Get equity historical data from new equity table + // Every 3 minutes per cycle: 10000 records = about 20 days of data snapshots, err := s.store.Equity().GetLatest(traderID, 10000) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取历史数据失败: %v", err), + "error": fmt.Sprintf("Failed to get historical data: %v", err), }) return } @@ -1661,26 +1661,26 @@ func (s *Server) handleEquityHistory(c *gin.Context) { return } - // 构建收益率历史数据点 + // Build return rate historical data points type EquityPoint struct { Timestamp string `json:"timestamp"` - TotalEquity float64 `json:"total_equity"` // 账户净值(wallet + unrealized) - AvailableBalance float64 `json:"available_balance"` // 可用余额 - TotalPnL float64 `json:"total_pnl"` // 总盈亏(未实现盈亏) - TotalPnLPct float64 `json:"total_pnl_pct"` // 总盈亏百分比 - PositionCount int `json:"position_count"` // 持仓数量 - MarginUsedPct float64 `json:"margin_used_pct"` // 保证金使用率 + TotalEquity float64 `json:"total_equity"` // Account equity (wallet + unrealized) + AvailableBalance float64 `json:"available_balance"` // Available balance + TotalPnL float64 `json:"total_pnl"` // Total PnL (unrealized PnL) + TotalPnLPct float64 `json:"total_pnl_pct"` // Total PnL percentage + PositionCount int `json:"position_count"` // Position count + MarginUsedPct float64 `json:"margin_used_pct"` // Margin used percentage } - // 使用第一条记录的余额作为初始余额来计算收益率 + // Use the balance of the first record as initial balance to calculate return rate initialBalance := snapshots[0].Balance if initialBalance == 0 { - initialBalance = 1 // 避免除零 + initialBalance = 1 // Avoid division by zero } var history []EquityPoint for _, snap := range snapshots { - // 计算盈亏百分比 + // Calculate PnL percentage totalPnLPct := 0.0 if initialBalance > 0 { totalPnLPct = (snap.UnrealizedPnL / initialBalance) * 100 @@ -1700,42 +1700,42 @@ func (s *Server) handleEquityHistory(c *gin.Context) { c.JSON(http.StatusOK, history) } -// authMiddleware JWT认证中间件 +// authMiddleware JWT authentication middleware func (s *Server) authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"}) c.Abort() return } - // 检查Bearer token格式 + // Check Bearer token format tokenParts := strings.Split(authHeader, " ") if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的Authorization格式"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"}) c.Abort() return } tokenString := tokenParts[1] - // 黑名单检查 + // Blacklist check if auth.IsTokenBlacklisted(tokenString) { - c.JSON(http.StatusUnauthorized, gin.H{"error": "token已失效,请重新登录"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Token expired, please login again"}) c.Abort() return } - // 验证JWT token + // Validate JWT token claims, err := auth.ValidateJWT(tokenString) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token: " + err.Error()}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token: " + err.Error()}) c.Abort() return } - // 将用户信息存储到上下文中 + // Store user information in context c.Set("user_id", claims.UserID) c.Set("email", claims.Email) c.Next() @@ -1743,22 +1743,22 @@ func (s *Server) authMiddleware() gin.HandlerFunc { } -// handleLogout 将当前token加入黑名单 +// handleLogout Add current token to blacklist func (s *Server) handleLogout(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"}) return } parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的Authorization格式"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"}) return } tokenString := parts[1] claims, err := auth.ValidateJWT(tokenString) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) return } var exp time.Time @@ -1768,14 +1768,14 @@ func (s *Server) handleLogout(c *gin.Context) { exp = time.Now().Add(24 * time.Hour) } auth.BlacklistToken(tokenString, exp) - c.JSON(http.StatusOK, gin.H{"message": "已登出"}) + c.JSON(http.StatusOK, gin.H{"message": "Logged out"}) } -// handleRegister 处理用户注册请求 +// handleRegister Handle user registration request func (s *Server) handleRegister(c *gin.Context) { - // 检查是否允许注册 + // Check if registration is allowed if !config.Get().RegistrationEnabled { - c.JSON(http.StatusForbidden, gin.H{"error": "注册功能已关闭"}) + c.JSON(http.StatusForbidden, gin.H{"error": "Registration is disabled"}) return } @@ -1789,28 +1789,28 @@ func (s *Server) handleRegister(c *gin.Context) { return } - // 检查邮箱是否已存在 + // Check if email already exists _, err := s.store.User().GetByEmail(req.Email) if err == nil { - c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"}) + c.JSON(http.StatusConflict, gin.H{"error": "Email already registered"}) return } - // 生成密码哈希 + // Generate password hash passwordHash, err := auth.HashPassword(req.Password) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Password processing failed"}) return } - // 生成OTP密钥 + // Generate OTP secret otpSecret, err := auth.GenerateOTPSecret() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "OTP密钥生成失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "OTP secret generation failed"}) return } - // 创建用户(未验证OTP状态) + // Create user (unverified OTP status) userID := uuid.New().String() user := &store.User{ ID: userID, @@ -1822,22 +1822,22 @@ func (s *Server) handleRegister(c *gin.Context) { err = s.store.User().Create(user) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败: " + err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()}) return } - // 返回OTP设置信息 + // Return OTP setup information qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email) c.JSON(http.StatusOK, gin.H{ "user_id": userID, "email": req.Email, "otp_secret": otpSecret, "qr_code_url": qrCodeURL, - "message": "请使用Google Authenticator扫描二维码并验证OTP", + "message": "Please scan the QR code with Google Authenticator and verify OTP", }) } -// handleCompleteRegistration 完成注册(验证OTP) +// handleCompleteRegistration Complete registration (verify OTP) func (s *Server) handleCompleteRegistration(c *gin.Context) { var req struct { UserID string `json:"user_id" binding:"required"` @@ -1849,48 +1849,48 @@ func (s *Server) handleCompleteRegistration(c *gin.Context) { return } - // 获取用户信息 + // Get user information user, err := s.store.User().GetByID(req.UserID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "User does not exist"}) return } - // 验证OTP + // Verify OTP if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": "OTP验证码错误"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "OTP code error"}) return } - // 更新用户OTP验证状态 + // Update user OTP verified status err = s.store.User().UpdateOTPVerified(req.UserID, true) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户状态失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user status"}) return } - // 生成JWT token + // Generate JWT token token, err := auth.GenerateJWT(user.ID, user.Email) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } - // 初始化用户的默认模型和交易所配置 + // Initialize default model and exchange configs for user err = s.initUserDefaultConfigs(user.ID) if err != nil { - logger.Infof("初始化用户默认配置失败: %v", err) + logger.Infof("Failed to initialize user default configs: %v", err) } c.JSON(http.StatusOK, gin.H{ "token": token, "user_id": user.ID, "email": user.Email, - "message": "注册完成", + "message": "Registration completed", }) } -// handleLogin 处理用户登录请求 +// handleLogin Handle user login request func (s *Server) handleLogin(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` @@ -1902,39 +1902,39 @@ func (s *Server) handleLogin(c *gin.Context) { return } - // 获取用户信息 + // Get user information user, err := s.store.User().GetByEmail(req.Email) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Email or password incorrect"}) return } - // 验证密码 + // Verify password if !auth.CheckPassword(req.Password, user.PasswordHash) { - c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Email or password incorrect"}) return } - // 检查OTP是否已验证 + // Check if OTP is verified if !user.OTPVerified { c.JSON(http.StatusUnauthorized, gin.H{ - "error": "账户未完成OTP设置", + "error": "Account has not completed OTP setup", "user_id": user.ID, "requires_otp_setup": true, }) return } - // 返回需要OTP验证的状态 + // Return status requiring OTP verification c.JSON(http.StatusOK, gin.H{ "user_id": user.ID, "email": user.Email, - "message": "请输入Google Authenticator验证码", + "message": "Please enter Google Authenticator code", "requires_otp": true, }) } -// handleVerifyOTP 验证OTP并完成登录 +// handleVerifyOTP Verify OTP and complete login func (s *Server) handleVerifyOTP(c *gin.Context) { var req struct { UserID string `json:"user_id" binding:"required"` @@ -1946,23 +1946,23 @@ func (s *Server) handleVerifyOTP(c *gin.Context) { return } - // 获取用户信息 + // Get user information user, err := s.store.User().GetByID(req.UserID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "User does not exist"}) return } - // 验证OTP + // Verify OTP if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Verification code error"}) return } - // 生成JWT token + // Generate JWT token token, err := auth.GenerateJWT(user.ID, user.Email) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } @@ -1970,11 +1970,11 @@ func (s *Server) handleVerifyOTP(c *gin.Context) { "token": token, "user_id": user.ID, "email": user.Email, - "message": "登录成功", + "message": "Login successful", }) } -// handleResetPassword 重置密码(通过邮箱 + OTP 验证) +// handleResetPassword Reset password (via email + OTP verification) func (s *Server) handleResetPassword(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` @@ -1987,69 +1987,69 @@ func (s *Server) handleResetPassword(c *gin.Context) { return } - // 查询用户 + // Query user user, err := s.store.User().GetByEmail(req.Email) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "邮箱不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Email does not exist"}) return } - // 验证 OTP + // Verify OTP if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) { - c.JSON(http.StatusBadRequest, gin.H{"error": "Google Authenticator 验证码错误"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Google Authenticator code error"}) return } - // 生成新密码哈希 + // Generate new password hash newPasswordHash, err := auth.HashPassword(req.NewPassword) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Password processing failed"}) return } - // 更新密码 + // Update password err = s.store.User().UpdatePassword(user.ID, newPasswordHash) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "密码更新失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Password update failed"}) return } - logger.Infof("✓ 用户 %s 密码已重置", user.Email) - c.JSON(http.StatusOK, gin.H{"message": "密码重置成功,请使用新密码登录"}) + logger.Infof("✓ User %s password has been reset", user.Email) + c.JSON(http.StatusOK, gin.H{"message": "Password reset successful, please login with new password"}) } -// initUserDefaultConfigs 为新用户初始化默认的模型和交易所配置 +// initUserDefaultConfigs Initialize default model and exchange configs for new user func (s *Server) initUserDefaultConfigs(userID string) error { - // 注释掉自动创建默认配置,让用户手动添加 - // 这样新用户注册后不会自动有配置项 - logger.Infof("用户 %s 注册完成,等待手动配置AI模型和交易所", userID) + // Commented out auto-creation of default configs, let users add manually + // This way new users won't have config items automatically after registration + logger.Infof("User %s registration completed, waiting for manual AI model and exchange configuration", userID) return nil } -// handleGetSupportedModels 获取系统支持的AI模型列表 +// handleGetSupportedModels Get list of AI models supported by the system func (s *Server) handleGetSupportedModels(c *gin.Context) { - // 返回系统支持的AI模型(从default用户获取) + // Return system-supported AI models (get from default user) models, err := s.store.AIModel().List("default") if err != nil { - logger.Infof("❌ 获取支持的AI模型失败: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的AI模型失败"}) + logger.Infof("❌ Failed to get supported AI models: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get supported AI models"}) return } c.JSON(http.StatusOK, models) } -// handleGetSupportedExchanges 获取系统支持的交易所列表 +// handleGetSupportedExchanges Get list of exchanges supported by the system func (s *Server) handleGetSupportedExchanges(c *gin.Context) { - // 返回系统支持的交易所(从default用户获取) + // Return system-supported exchanges (get from default user) exchanges, err := s.store.Exchange().List("default") if err != nil { - logger.Infof("❌ 获取支持的交易所失败: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的交易所失败"}) + logger.Infof("❌ Failed to get supported exchanges: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get supported exchanges"}) return } - // 转换为安全的响应结构,移除敏感信息 + // Convert to safe response structure, remove sensitive information safeExchanges := make([]SafeExchangeConfig, len(exchanges)) for i, exchange := range exchanges { safeExchanges[i] = SafeExchangeConfig{ @@ -2058,8 +2058,8 @@ func (s *Server) handleGetSupportedExchanges(c *gin.Context) { Type: exchange.Type, Enabled: exchange.Enabled, Testnet: exchange.Testnet, - HyperliquidWalletAddr: "", // 默认配置不包含钱包地址 - AsterUser: "", // 默认配置不包含用户信息 + HyperliquidWalletAddr: "", // Default config does not include wallet address + AsterUser: "", // Default config does not include user info AsterSigner: "", } } @@ -2067,33 +2067,33 @@ func (s *Server) handleGetSupportedExchanges(c *gin.Context) { c.JSON(http.StatusOK, safeExchanges) } -// Start 启动服务器 +// Start Start server func (s *Server) Start() error { addr := fmt.Sprintf(":%d", s.port) - logger.Infof("🌐 API服务器启动在 http://localhost%s", addr) - logger.Infof("📊 API文档:") - logger.Infof(" • GET /api/health - 健康检查") - logger.Infof(" • GET /api/traders - 公开的AI交易员排行榜前50名(无需认证)") - logger.Infof(" • GET /api/competition - 公开的竞赛数据(无需认证)") - logger.Infof(" • GET /api/top-traders - 前5名交易员数据(无需认证,表现对比用)") - logger.Infof(" • GET /api/equity-history?trader_id=xxx - 公开的收益率历史数据(无需认证,竞赛用)") - logger.Infof(" • GET /api/equity-history-batch?trader_ids=a,b,c - 批量获取历史数据(无需认证,表现对比优化)") - logger.Infof(" • GET /api/traders/:id/public-config - 公开的交易员配置(无需认证,不含敏感信息)") - logger.Infof(" • POST /api/traders - 创建新的AI交易员") - logger.Infof(" • DELETE /api/traders/:id - 删除AI交易员") - logger.Infof(" • POST /api/traders/:id/start - 启动AI交易员") - logger.Infof(" • POST /api/traders/:id/stop - 停止AI交易员") - logger.Infof(" • GET /api/models - 获取AI模型配置") - logger.Infof(" • PUT /api/models - 更新AI模型配置") - logger.Infof(" • GET /api/exchanges - 获取交易所配置") - logger.Infof(" • PUT /api/exchanges - 更新交易所配置") - logger.Infof(" • GET /api/status?trader_id=xxx - 指定trader的系统状态") - logger.Infof(" • GET /api/account?trader_id=xxx - 指定trader的账户信息") - logger.Infof(" • GET /api/positions?trader_id=xxx - 指定trader的持仓列表") - logger.Infof(" • GET /api/decisions?trader_id=xxx - 指定trader的决策日志") - logger.Infof(" • GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策") - logger.Infof(" • GET /api/statistics?trader_id=xxx - 指定trader的统计信息") - logger.Infof(" • GET /api/performance?trader_id=xxx - 指定trader的AI学习表现分析") + logger.Infof("🌐 API server starting at http://localhost%s", addr) + logger.Infof("📊 API Documentation:") + logger.Infof(" • GET /api/health - Health check") + logger.Infof(" • GET /api/traders - Public AI trader leaderboard top 50 (no auth required)") + logger.Infof(" • GET /api/competition - Public competition data (no auth required)") + logger.Infof(" • GET /api/top-traders - Top 5 trader data (no auth required, for performance comparison)") + logger.Infof(" • GET /api/equity-history?trader_id=xxx - Public return rate historical data (no auth required, for competition)") + logger.Infof(" • GET /api/equity-history-batch?trader_ids=a,b,c - Batch get historical data (no auth required, performance comparison optimization)") + logger.Infof(" • GET /api/traders/:id/public-config - Public trader config (no auth required, no sensitive info)") + logger.Infof(" • POST /api/traders - Create new AI trader") + logger.Infof(" • DELETE /api/traders/:id - Delete AI trader") + logger.Infof(" • POST /api/traders/:id/start - Start AI trader") + logger.Infof(" • POST /api/traders/:id/stop - Stop AI trader") + logger.Infof(" • GET /api/models - Get AI model config") + logger.Infof(" • PUT /api/models - Update AI model config") + logger.Infof(" • GET /api/exchanges - Get exchange config") + logger.Infof(" • PUT /api/exchanges - Update exchange config") + logger.Infof(" • GET /api/status?trader_id=xxx - Specified trader's system status") + logger.Infof(" • GET /api/account?trader_id=xxx - Specified trader's account info") + logger.Infof(" • GET /api/positions?trader_id=xxx - Specified trader's position list") + logger.Infof(" • GET /api/decisions?trader_id=xxx - Specified trader's decision log") + logger.Infof(" • GET /api/decisions/latest?trader_id=xxx - Specified trader's latest decisions") + logger.Infof(" • GET /api/statistics?trader_id=xxx - Specified trader's statistics") + logger.Infof(" • GET /api/performance?trader_id=xxx - Specified trader's AI learning performance analysis") logger.Info() s.httpServer = &http.Server{ @@ -2103,7 +2103,7 @@ func (s *Server) Start() error { return s.httpServer.ListenAndServe() } -// Shutdown 优雅关闭服务器 +// Shutdown Gracefully shutdown server func (s *Server) Shutdown() error { if s.httpServer == nil { return nil @@ -2113,12 +2113,12 @@ func (s *Server) Shutdown() error { return s.httpServer.Shutdown(ctx) } -// handleGetPromptTemplates 获取所有系统提示词模板列表 +// handleGetPromptTemplates Get all system prompt template list func (s *Server) handleGetPromptTemplates(c *gin.Context) { - // 导入 decision 包 + // Import decision package templates := decision.GetAllPromptTemplates() - // 转换为响应格式 + // Convert to response format response := make([]map[string]interface{}, 0, len(templates)) for _, tmpl := range templates { response = append(response, map[string]interface{}{ @@ -2131,13 +2131,13 @@ func (s *Server) handleGetPromptTemplates(c *gin.Context) { }) } -// handleGetPromptTemplate 获取指定名称的提示词模板内容 +// handleGetPromptTemplate Get prompt template content by specified name func (s *Server) handleGetPromptTemplate(c *gin.Context) { templateName := c.Param("name") template, err := decision.GetPromptTemplate(templateName) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("模板不存在: %s", templateName)}) + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("Template does not exist: %s", templateName)}) return } @@ -2147,18 +2147,18 @@ func (s *Server) handleGetPromptTemplate(c *gin.Context) { }) } -// handlePublicTraderList 获取公开的交易员列表(无需认证) +// handlePublicTraderList Get public trader list (no authentication required) func (s *Server) handlePublicTraderList(c *gin.Context) { - // 从所有用户获取交易员信息 + // Get trader information from all users competition, err := s.traderManager.GetCompetitionData() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取交易员列表失败: %v", err), + "error": fmt.Sprintf("Failed to get trader list: %v", err), }) return } - // 获取traders数组 + // Get traders array tradersData, exists := competition["traders"] if !exists { c.JSON(http.StatusOK, []map[string]interface{}{}) @@ -2168,12 +2168,12 @@ func (s *Server) handlePublicTraderList(c *gin.Context) { traders, ok := tradersData.([]map[string]interface{}) if !ok { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "交易员数据格式错误", + "error": "Trader data format error", }) return } - // 返回交易员基本信息,过滤敏感信息 + // Return trader basic information, filter sensitive information result := make([]map[string]interface{}, 0, len(traders)) for _, trader := range traders { result = append(result, map[string]interface{}{ @@ -2193,12 +2193,12 @@ func (s *Server) handlePublicTraderList(c *gin.Context) { c.JSON(http.StatusOK, result) } -// handlePublicCompetition 获取公开的竞赛数据(无需认证) +// handlePublicCompetition Get public competition data (no authentication required) func (s *Server) handlePublicCompetition(c *gin.Context) { competition, err := s.traderManager.GetCompetitionData() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取竞赛数据失败: %v", err), + "error": fmt.Sprintf("Failed to get competition data: %v", err), }) return } @@ -2206,12 +2206,12 @@ func (s *Server) handlePublicCompetition(c *gin.Context) { c.JSON(http.StatusOK, competition) } -// handleTopTraders 获取前5名交易员数据(无需认证,用于表现对比) +// handleTopTraders Get top 5 trader data (no authentication required, for performance comparison) func (s *Server) handleTopTraders(c *gin.Context) { topTraders, err := s.traderManager.GetTopTradersData() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取前10名交易员数据失败: %v", err), + "error": fmt.Sprintf("Failed to get top 10 trader data: %v", err), }) return } @@ -2219,33 +2219,33 @@ func (s *Server) handleTopTraders(c *gin.Context) { c.JSON(http.StatusOK, topTraders) } -// handleEquityHistoryBatch 批量获取多个交易员的收益率历史数据(无需认证,用于表现对比) +// handleEquityHistoryBatch Batch get return rate historical data for multiple traders (no authentication required, for performance comparison) func (s *Server) handleEquityHistoryBatch(c *gin.Context) { var requestBody struct { TraderIDs []string `json:"trader_ids"` } - // 尝试解析POST请求的JSON body + // Try to parse POST request JSON body if err := c.ShouldBindJSON(&requestBody); err != nil { - // 如果JSON解析失败,尝试从query参数获取(兼容GET请求) + // If JSON parse fails, try to get from query parameters (compatible with GET request) traderIDsParam := c.Query("trader_ids") if traderIDsParam == "" { - // 如果没有指定trader_ids,则返回前5名的历史数据 + // If no trader_ids specified, return historical data for top 5 topTraders, err := s.traderManager.GetTopTradersData() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取前5名交易员失败: %v", err), + "error": fmt.Sprintf("Failed to get top 5 traders: %v", err), }) return } traders, ok := topTraders["traders"].([]map[string]interface{}) if !ok { - c.JSON(http.StatusInternalServerError, gin.H{"error": "交易员数据格式错误"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Trader data format error"}) return } - // 提取trader IDs + // Extract trader IDs traderIDs := make([]string, 0, len(traders)) for _, trader := range traders { if traderID, ok := trader["trader_id"].(string); ok { @@ -2258,14 +2258,14 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { return } - // 解析逗号分隔的trader IDs + // Parse comma-separated trader IDs requestBody.TraderIDs = strings.Split(traderIDsParam, ",") for i := range requestBody.TraderIDs { requestBody.TraderIDs[i] = strings.TrimSpace(requestBody.TraderIDs[i]) } } - // 限制最多20个交易员,防止请求过大 + // Limit to maximum 20 traders to prevent oversized requests if len(requestBody.TraderIDs) > 20 { requestBody.TraderIDs = requestBody.TraderIDs[:20] } @@ -2274,8 +2274,8 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { c.JSON(http.StatusOK, result) } -// getEquityHistoryForTraders 获取多个交易员的历史数据 -// 直接从数据库查询,不依赖内存中的 trader(这样重启后也能获取历史数据) +// getEquityHistoryForTraders Get historical data for multiple traders +// Query directly from database, not dependent on trader in memory (so historical data can be retrieved after restart) func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]interface{} { result := make(map[string]interface{}) histories := make(map[string]interface{}) @@ -2286,20 +2286,20 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter continue } - // 从新的 equity 表获取净值历史数据 + // Get equity historical data from new equity table snapshots, err := s.store.Equity().GetLatest(traderID, 500) if err != nil { - errors[traderID] = fmt.Sprintf("获取历史数据失败: %v", err) + errors[traderID] = fmt.Sprintf("Failed to get historical data: %v", err) continue } if len(snapshots) == 0 { - // 没有历史记录,返回空数组 + // No historical records, return empty array histories[traderID] = []map[string]interface{}{} continue } - // 构建收益率历史数据 + // Build return rate historical data history := make([]map[string]interface{}, 0, len(snapshots)) for _, snap := range snapshots { history = append(history, map[string]interface{}{ @@ -2322,24 +2322,24 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter return result } -// handleGetPublicTraderConfig 获取公开的交易员配置信息(无需认证,不包含敏感信息) +// handleGetPublicTraderConfig Get public trader configuration information (no authentication required, does not include sensitive information) func (s *Server) handleGetPublicTraderConfig(c *gin.Context) { traderID := c.Param("id") if traderID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "交易员ID不能为空"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Trader ID cannot be empty"}) return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Trader does not exist"}) return } - // 获取交易员的状态信息 + // Get trader status information status := trader.GetStatus() - // 只返回公开的配置信息,不包含API密钥等敏感数据 + // Only return public configuration information, not including sensitive data like API keys result := map[string]interface{}{ "trader_id": trader.GetID(), "trader_name": trader.GetName(), diff --git a/api/server_test.go b/api/server_test.go index 0b9997a2..5b499ecd 100644 --- a/api/server_test.go +++ b/api/server_test.go @@ -7,7 +7,7 @@ import ( "nofx/store" ) -// TestUpdateTraderRequest_SystemPromptTemplate 测试更新交易员时 SystemPromptTemplate 字段是否存在 +// TestUpdateTraderRequest_SystemPromptTemplate Test whether SystemPromptTemplate field exists when updating trader func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) { tests := []struct { name string @@ -15,7 +15,7 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) { expectedPromptTemplate string }{ { - name: "更新时应该能接收 system_prompt_template=nof1", + name: "Should accept system_prompt_template=nof1 during update", requestJSON: `{ "name": "Test Trader", "ai_model_id": "gpt-4", @@ -33,7 +33,7 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) { expectedPromptTemplate: "nof1", }, { - name: "更新时应该能接收 system_prompt_template=default", + name: "Should accept system_prompt_template=default during update", requestJSON: `{ "name": "Test Trader", "ai_model_id": "gpt-4", @@ -51,7 +51,7 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) { expectedPromptTemplate: "default", }, { - name: "更新时应该能接收 system_prompt_template=custom", + name: "Should accept system_prompt_template=custom during update", requestJSON: `{ "name": "Test Trader", "ai_model_id": "gpt-4", @@ -72,20 +72,20 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // 测试 UpdateTraderRequest 结构体是否能正确解析 system_prompt_template 字段 + // Test whether UpdateTraderRequest struct can correctly parse system_prompt_template field var req UpdateTraderRequest err := json.Unmarshal([]byte(tt.requestJSON), &req) if err != nil { t.Fatalf("Failed to unmarshal JSON: %v", err) } - // ✅ 验证 SystemPromptTemplate 字段是否被正确读取 + // Verify SystemPromptTemplate field is correctly read if req.SystemPromptTemplate != tt.expectedPromptTemplate { t.Errorf("Expected SystemPromptTemplate=%q, got %q", tt.expectedPromptTemplate, req.SystemPromptTemplate) } - // 验证其他字段也被正确解析 + // Verify other fields are also correctly parsed if req.Name != "Test Trader" { t.Errorf("Name not parsed correctly") } @@ -96,7 +96,7 @@ func TestUpdateTraderRequest_SystemPromptTemplate(t *testing.T) { } } -// TestGetTraderConfigResponse_SystemPromptTemplate 测试获取交易员配置时返回值是否包含 system_prompt_template +// TestGetTraderConfigResponse_SystemPromptTemplate Test whether return value contains system_prompt_template when getting trader config func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) { tests := []struct { name string @@ -104,7 +104,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) { expectedTemplate string }{ { - name: "获取配置应该返回 system_prompt_template=nof1", + name: "Get config should return system_prompt_template=nof1", traderConfig: &store.Trader{ ID: "trader-123", UserID: "user-1", @@ -125,7 +125,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) { expectedTemplate: "nof1", }, { - name: "获取配置应该返回 system_prompt_template=default", + name: "Get config should return system_prompt_template=default", traderConfig: &store.Trader{ ID: "trader-456", UserID: "user-1", @@ -149,7 +149,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // 模拟 handleGetTraderConfig 的返回值构造逻辑(修复后的实现) + // Simulate handleGetTraderConfig return value construction logic (fixed implementation) result := map[string]interface{}{ "trader_id": tt.traderConfig.ID, "trader_name": tt.traderConfig.Name, @@ -167,7 +167,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) { "is_running": tt.traderConfig.IsRunning, } - // ✅ 检查响应中是否包含 system_prompt_template + // Check if response contains system_prompt_template if _, exists := result["system_prompt_template"]; !exists { t.Errorf("Response is missing 'system_prompt_template' field") } else { @@ -178,7 +178,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) { } } - // 验证其他字段是否正确 + // Verify other fields are correct if result["trader_id"] != tt.traderConfig.ID { t.Errorf("trader_id mismatch") } @@ -189,7 +189,7 @@ func TestGetTraderConfigResponse_SystemPromptTemplate(t *testing.T) { } } -// TestUpdateTraderRequest_CompleteFields 验证 UpdateTraderRequest 结构体定义完整性 +// TestUpdateTraderRequest_CompleteFields Verify UpdateTraderRequest struct definition completeness func TestUpdateTraderRequest_CompleteFields(t *testing.T) { jsonData := `{ "name": "Test Trader", @@ -212,7 +212,7 @@ func TestUpdateTraderRequest_CompleteFields(t *testing.T) { t.Fatalf("Failed to unmarshal JSON: %v", err) } - // 验证基本字段是否正确解析 + // Verify basic fields are correctly parsed if req.Name != "Test Trader" { t.Errorf("Name mismatch: got %q", req.Name) } @@ -220,15 +220,15 @@ func TestUpdateTraderRequest_CompleteFields(t *testing.T) { t.Errorf("AIModelID mismatch: got %q", req.AIModelID) } - // ✅ 验证 SystemPromptTemplate 字段已正确添加到结构体 + // Verify SystemPromptTemplate field has been correctly added to struct if req.SystemPromptTemplate != "nof1" { t.Errorf("SystemPromptTemplate mismatch: expected %q, got %q", "nof1", req.SystemPromptTemplate) } } -// TestTraderListResponse_SystemPromptTemplate 测试 handleTraderList API 返回的 trader 对象是否包含 system_prompt_template 字段 +// TestTraderListResponse_SystemPromptTemplate Test whether trader object returned by handleTraderList API contains system_prompt_template field func TestTraderListResponse_SystemPromptTemplate(t *testing.T) { - // 模拟 handleTraderList 中的 trader 对象构造 + // Simulate trader object construction in handleTraderList trader := &store.Trader{ ID: "trader-001", UserID: "user-1", @@ -240,7 +240,7 @@ func TestTraderListResponse_SystemPromptTemplate(t *testing.T) { IsRunning: true, } - // 构造 API 响应对象(与 api/server.go 中的逻辑一致) + // Construct API response object (consistent with logic in api/server.go) response := map[string]interface{}{ "trader_id": trader.ID, "trader_name": trader.Name, @@ -251,20 +251,20 @@ func TestTraderListResponse_SystemPromptTemplate(t *testing.T) { "system_prompt_template": trader.SystemPromptTemplate, } - // ✅ 验证 system_prompt_template 字段存在 + // Verify system_prompt_template field exists if _, exists := response["system_prompt_template"]; !exists { t.Errorf("Trader list response is missing 'system_prompt_template' field") } - // ✅ 验证 system_prompt_template 值正确 + // Verify system_prompt_template value is correct if response["system_prompt_template"] != "nof1" { t.Errorf("Expected system_prompt_template='nof1', got %v", response["system_prompt_template"]) } } -// TestPublicTraderListResponse_SystemPromptTemplate 测试 handlePublicTraderList API 返回的 trader 对象是否包含 system_prompt_template 字段 +// TestPublicTraderListResponse_SystemPromptTemplate Test whether trader object returned by handlePublicTraderList API contains system_prompt_template field func TestPublicTraderListResponse_SystemPromptTemplate(t *testing.T) { - // 模拟 getConcurrentTraderData 返回的 trader 数据 + // Simulate trader data returned by getConcurrentTraderData traderData := map[string]interface{}{ "trader_id": "trader-002", "trader_name": "Public Trader", @@ -279,7 +279,7 @@ func TestPublicTraderListResponse_SystemPromptTemplate(t *testing.T) { "system_prompt_template": "default", } - // 构造 API 响应对象(与 api/server.go handlePublicTraderList 中的逻辑一致) + // Construct API response object (consistent with logic in api/server.go handlePublicTraderList) response := map[string]interface{}{ "trader_id": traderData["trader_id"], "trader_name": traderData["trader_name"], @@ -293,12 +293,12 @@ func TestPublicTraderListResponse_SystemPromptTemplate(t *testing.T) { "system_prompt_template": traderData["system_prompt_template"], } - // ✅ 验证 system_prompt_template 字段存在 + // Verify system_prompt_template field exists if _, exists := response["system_prompt_template"]; !exists { t.Errorf("Public trader list response is missing 'system_prompt_template' field") } - // ✅ 验证 system_prompt_template 值正确 + // Verify system_prompt_template value is correct if response["system_prompt_template"] != "default" { t.Errorf("Expected system_prompt_template='default', got %v", response["system_prompt_template"]) } diff --git a/api/strategy.go b/api/strategy.go index 93903b87..8b50a7ef 100644 --- a/api/strategy.go +++ b/api/strategy.go @@ -14,21 +14,21 @@ import ( "github.com/google/uuid" ) -// handleGetStrategies 获取策略列表 +// handleGetStrategies Get strategy list func (s *Server) handleGetStrategies(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } strategies, err := s.store.Strategy().List(userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "获取策略列表失败: " + err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get strategy list: " + err.Error()}) return } - // 转换为前端格式 + // Convert to frontend format result := make([]gin.H, 0, len(strategies)) for _, st := range strategies { var config store.StrategyConfig @@ -51,19 +51,19 @@ func (s *Server) handleGetStrategies(c *gin.Context) { }) } -// handleGetStrategy 获取单个策略 +// handleGetStrategy Get single strategy func (s *Server) handleGetStrategy(c *gin.Context) { userID := c.GetString("user_id") strategyID := c.Param("id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } strategy, err := s.store.Strategy().Get(userID, strategyID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "策略不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Strategy not found"}) return } @@ -82,11 +82,11 @@ func (s *Server) handleGetStrategy(c *gin.Context) { }) } -// handleCreateStrategy 创建策略 +// handleCreateStrategy Create strategy func (s *Server) handleCreateStrategy(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } @@ -97,14 +97,14 @@ func (s *Server) handleCreateStrategy(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } - // 序列化配置 + // Serialize configuration configJSON, err := json.Marshal(req.Config) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化配置失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize configuration"}) return } @@ -119,34 +119,34 @@ func (s *Server) handleCreateStrategy(c *gin.Context) { } if err := s.store.Strategy().Create(strategy); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "创建策略失败: " + err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create strategy: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "id": strategy.ID, - "message": "策略创建成功", + "message": "Strategy created successfully", }) } -// handleUpdateStrategy 更新策略 +// handleUpdateStrategy Update strategy func (s *Server) handleUpdateStrategy(c *gin.Context) { userID := c.GetString("user_id") strategyID := c.Param("id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } - // 检查是否是系统默认策略 + // Check if it's a system default strategy existing, err := s.store.Strategy().Get(userID, strategyID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "策略不存在"}) + c.JSON(http.StatusNotFound, gin.H{"error": "Strategy not found"}) return } if existing.IsDefault { - c.JSON(http.StatusForbidden, gin.H{"error": "不能修改系统默认策略"}) + c.JSON(http.StatusForbidden, gin.H{"error": "Cannot modify system default strategy"}) return } @@ -157,14 +157,14 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } - // 序列化配置 + // Serialize configuration configJSON, err := json.Marshal(req.Config) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化配置失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize configuration"}) return } @@ -177,56 +177,56 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) { } if err := s.store.Strategy().Update(strategy); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "更新策略失败: " + err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update strategy: " + err.Error()}) return } - c.JSON(http.StatusOK, gin.H{"message": "策略更新成功"}) + c.JSON(http.StatusOK, gin.H{"message": "Strategy updated successfully"}) } -// handleDeleteStrategy 删除策略 +// handleDeleteStrategy Delete strategy func (s *Server) handleDeleteStrategy(c *gin.Context) { userID := c.GetString("user_id") strategyID := c.Param("id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } if err := s.store.Strategy().Delete(userID, strategyID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "删除策略失败: " + err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete strategy: " + err.Error()}) return } - c.JSON(http.StatusOK, gin.H{"message": "策略删除成功"}) + c.JSON(http.StatusOK, gin.H{"message": "Strategy deleted successfully"}) } -// handleActivateStrategy 激活策略 +// handleActivateStrategy Activate strategy func (s *Server) handleActivateStrategy(c *gin.Context) { userID := c.GetString("user_id") strategyID := c.Param("id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } if err := s.store.Strategy().SetActive(userID, strategyID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "激活策略失败: " + err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to activate strategy: " + err.Error()}) return } - c.JSON(http.StatusOK, gin.H{"message": "策略激活成功"}) + c.JSON(http.StatusOK, gin.H{"message": "Strategy activated successfully"}) } -// handleDuplicateStrategy 复制策略 +// handleDuplicateStrategy Duplicate strategy func (s *Server) handleDuplicateStrategy(c *gin.Context) { userID := c.GetString("user_id") sourceID := c.Param("id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } @@ -235,34 +235,34 @@ func (s *Server) handleDuplicateStrategy(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } newID := uuid.New().String() if err := s.store.Strategy().Duplicate(userID, sourceID, newID, req.Name); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "复制策略失败: " + err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to duplicate strategy: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "id": newID, - "message": "策略复制成功", + "message": "Strategy duplicated successfully", }) } -// handleGetActiveStrategy 获取当前激活的策略 +// handleGetActiveStrategy Get currently active strategy func (s *Server) handleGetActiveStrategy(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } strategy, err := s.store.Strategy().GetActive(userID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "没有激活的策略"}) + c.JSON(http.StatusNotFound, gin.H{"error": "No active strategy"}) return } @@ -281,9 +281,9 @@ func (s *Server) handleGetActiveStrategy(c *gin.Context) { }) } -// handleGetDefaultStrategyConfig 获取默认策略配置模板 +// handleGetDefaultStrategyConfig Get default strategy configuration template func (s *Server) handleGetDefaultStrategyConfig(c *gin.Context) { - // 返回默认配置结构,供前端创建新策略时使用 + // Return default configuration structure for frontend to use when creating new strategies defaultConfig := store.StrategyConfig{ CoinSource: store.CoinSourceConfig{ SourceType: "coinpool", @@ -324,42 +324,42 @@ func (s *Server) handleGetDefaultStrategyConfig(c *gin.Context) { MinConfidence: 75, }, PromptSections: store.PromptSectionsConfig{ - RoleDefinition: `# 你是专业的加密货币交易AI + RoleDefinition: `# You are a professional cryptocurrency trading AI -你专注于技术分析和风险管理,基于市场数据做出理性的交易决策。 -你的目标是在控制风险的前提下,捕捉高概率的交易机会。`, - TradingFrequency: `# ⏱️ 交易频率认知 +You focus on technical analysis and risk management, making rational trading decisions based on market data. +Your goal is to capture high-probability trading opportunities while controlling risk.`, + TradingFrequency: `# ⏱️ Trading Frequency Awareness -- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔 -- 每小时>2笔 = 过度交易 -- 单笔持仓时间≥30-60分钟 -如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。`, - EntryStandards: `# 🎯 开仓标准(严格) +- Excellent traders: 2-4 trades per day ≈ 0.1-0.2 trades per hour +- >2 trades per hour = overtrading +- Single position holding time ≥30-60 minutes +If you find yourself trading every cycle → standards too low; if closing positions <30 minutes → too impatient.`, + EntryStandards: `# 🎯 Entry Standards (Strict) -只在多重信号共振时开仓: -- 趋势方向明确(EMA排列、价格位置) -- 动量确认(MACD、RSI协同) -- 波动率适中(ATR合理范围) -- 量价配合(成交量支持方向) +Only enter when multiple signals align: +- Clear trend direction (EMA alignment, price position) +- Momentum confirmation (MACD, RSI cooperation) +- Moderate volatility (ATR reasonable range) +- Volume-price coordination (volume supports direction) -避免:单一指标、信号矛盾、横盘震荡、刚平仓即重启。`, - DecisionProcess: `# 📋 决策流程 +Avoid: single indicator, conflicting signals, sideways consolidation, reopening immediately after closing.`, + DecisionProcess: `# 📋 Decision Process -1. 检查持仓 → 是否该止盈/止损 -2. 扫描候选币 + 多时间框 → 是否存在强信号 -3. 评估风险回报比 → 是否满足最小要求 -4. 先写思维链,再输出结构化JSON`, +1. Check positions → Should take profit/stop loss +2. Scan candidate coins + multiple timeframes → Are there strong signals +3. Evaluate risk-reward ratio → Does it meet minimum requirements +4. Write chain of thought first, then output structured JSON`, }, } c.JSON(http.StatusOK, defaultConfig) } -// handlePreviewPrompt 预览策略生成的 Prompt +// handlePreviewPrompt Preview prompt generated by strategy func (s *Server) handlePreviewPrompt(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } @@ -370,28 +370,28 @@ func (s *Server) handlePreviewPrompt(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } - // 使用默认值 + // Use default values if req.AccountEquity <= 0 { - req.AccountEquity = 1000.0 // 默认模拟账户净值 + req.AccountEquity = 1000.0 // Default simulated account equity } if req.PromptVariant == "" { req.PromptVariant = "balanced" } - // 创建策略引擎来构建 prompt + // Create strategy engine to build prompt engine := decision.NewStrategyEngine(&req.Config) - // 构建系统 prompt(使用策略引擎内置的方法) + // Build system prompt (using built-in method from strategy engine) systemPrompt := engine.BuildSystemPrompt( req.AccountEquity, req.PromptVariant, ) - // 获取可用的 prompt 模板列表 + // Get list of available prompt templates templateNames := decision.GetAllPromptTemplateNames() c.JSON(http.StatusOK, gin.H{ @@ -408,11 +408,11 @@ func (s *Server) handlePreviewPrompt(c *gin.Context) { }) } -// handleStrategyTestRun AI 测试运行(不执行交易,只返回 AI 分析结果) +// handleStrategyTestRun AI test run (does not execute trades, only returns AI analysis results) func (s *Server) handleStrategyTestRun(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } @@ -424,7 +424,7 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } @@ -432,27 +432,27 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { req.PromptVariant = "balanced" } - // 创建策略引擎来构建 prompt + // Create strategy engine to build prompt engine := decision.NewStrategyEngine(&req.Config) - // 获取候选币种 + // Get candidate coins candidates, err := engine.GetCandidateCoins() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "获取候选币种失败: " + err.Error(), + "error": "Failed to get candidate coins: " + err.Error(), "ai_response": "", }) return } - // 获取时间周期配置 + // Get timeframe configuration timeframes := req.Config.Indicators.Klines.SelectedTimeframes primaryTimeframe := req.Config.Indicators.Klines.PrimaryTimeframe klineCount := req.Config.Indicators.Klines.PrimaryCount - // 如果没有选择时间周期,使用默认值 + // If no timeframes selected, use default values if len(timeframes) == 0 { - // 兼容旧配置:使用主周期和长周期 + // Backward compatibility: use primary and longer timeframes if primaryTimeframe != "" { timeframes = append(timeframes, primaryTimeframe) } else { @@ -469,21 +469,21 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { klineCount = 30 } - fmt.Printf("📊 使用时间周期: %v, 主周期: %s, K线数量: %d\n", timeframes, primaryTimeframe, klineCount) + fmt.Printf("📊 Using timeframes: %v, primary: %s, kline count: %d\n", timeframes, primaryTimeframe, klineCount) - // 获取真实市场数据(使用多时间周期) + // Get real market data (using multiple timeframes) marketDataMap := make(map[string]*market.Data) for _, coin := range candidates { data, err := market.GetWithTimeframes(coin.Symbol, timeframes, primaryTimeframe, klineCount) if err != nil { - // 如果获取某个币种数据失败,记录日志但继续 - fmt.Printf("⚠️ 获取 %s 市场数据失败: %v\n", coin.Symbol, err) + // If getting data for a coin fails, log but continue + fmt.Printf("⚠️ Failed to get market data for %s: %v\n", coin.Symbol, err) continue } marketDataMap[coin.Symbol] = data } - // 构建真实的上下文(用于生成 User Prompt) + // Build real context (for generating User Prompt) testContext := &decision.Context{ CurrentTime: time.Now().Format("2006-01-02 15:04:05"), RuntimeMinutes: 0, @@ -504,13 +504,13 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { MarketDataMap: marketDataMap, } - // 构建 System Prompt + // Build System Prompt systemPrompt := engine.BuildSystemPrompt(1000.0, req.PromptVariant) - // 构建 User Prompt(使用真实市场数据) + // Build User Prompt (using real market data) userPrompt := engine.BuildUserPrompt(testContext) - // 如果请求真实 AI 调用 + // If requesting real AI call if req.RunRealAI && req.AIModelID != "" { aiResponse, aiErr := s.runRealAITest(userID, req.AIModelID, systemPrompt, userPrompt) if aiErr != nil { @@ -520,9 +520,9 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { "candidate_count": len(candidates), "candidates": candidates, "prompt_variant": req.PromptVariant, - "ai_response": fmt.Sprintf("❌ AI 调用失败: %s", aiErr.Error()), + "ai_response": fmt.Sprintf("❌ AI call failed: %s", aiErr.Error()), "ai_error": aiErr.Error(), - "note": "AI 调用出错", + "note": "AI call error", }) return } @@ -534,40 +534,40 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { "candidates": candidates, "prompt_variant": req.PromptVariant, "ai_response": aiResponse, - "note": "✅ 真实 AI 测试运行成功", + "note": "✅ Real AI test run successful", }) return } - // 返回结果(不实际调用 AI,只返回构建的 prompt) + // Return result (without actually calling AI, only return built prompt) c.JSON(http.StatusOK, gin.H{ "system_prompt": systemPrompt, "user_prompt": userPrompt, "candidate_count": len(candidates), "candidates": candidates, "prompt_variant": req.PromptVariant, - "ai_response": "请选择 AI 模型并点击「运行测试」来执行真实的 AI 分析。", - "note": "未选择 AI 模型或未启用真实 AI 调用", + "ai_response": "Please select an AI model and click 'Run Test' to perform real AI analysis.", + "note": "AI model not selected or real AI call not enabled", }) } -// runRealAITest 执行真实的 AI 测试调用 +// runRealAITest Execute real AI test call func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string) (string, error) { - // 获取 AI 模型配置 + // Get AI model configuration model, err := s.store.AIModel().Get(userID, modelID) if err != nil { - return "", fmt.Errorf("获取 AI 模型失败: %w", err) + return "", fmt.Errorf("failed to get AI model: %w", err) } if !model.Enabled { - return "", fmt.Errorf("AI 模型 %s 尚未启用", model.Name) + return "", fmt.Errorf("AI model %s is not enabled", model.Name) } if model.APIKey == "" { - return "", fmt.Errorf("AI 模型 %s 缺少 API Key", model.Name) + return "", fmt.Errorf("AI model %s is missing API Key", model.Name) } - // 创建 AI 客户端 + // Create AI client var aiClient mcp.AIClient provider := model.Provider @@ -579,15 +579,15 @@ func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string) aiClient = mcp.NewDeepSeekClient() aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName) default: - // 使用通用客户端 + // Use generic client aiClient = mcp.NewClient() aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName) } - // 调用 AI API + // Call AI API response, err := aiClient.CallWithMessages(systemPrompt, userPrompt) if err != nil { - return "", fmt.Errorf("AI API 调用失败: %w", err) + return "", fmt.Errorf("AI API call failed: %w", err) } return response, nil diff --git a/api/traderid_test.go b/api/traderid_test.go index 52b581c8..3ff5bf9f 100644 --- a/api/traderid_test.go +++ b/api/traderid_test.go @@ -8,67 +8,67 @@ import ( "github.com/google/uuid" ) -// TestTraderIDUniqueness 测试 traderID 的唯一性(修复 Issue #893) -// 验证即使在相同的 exchange 和 AI model 下,也能生成唯一的 traderID +// TestTraderIDUniqueness Test traderID uniqueness (fixes Issue #893) +// Verify that unique traderIDs can be generated even with the same exchange and AI model func TestTraderIDUniqueness(t *testing.T) { exchangeID := "binance" aiModelID := "gpt-4" - // 模拟同时创建 100 个 trader(相同参数) + // Simulate creating 100 traders simultaneously (with same parameters) traderIDs := make(map[string]bool) const numTraders = 100 for i := 0; i < numTraders; i++ { - // 模拟 api/server.go:497 的 traderID 生成逻辑 + // Simulate traderID generation logic from api/server.go:497 traderID := generateTraderID(exchangeID, aiModelID) - // ✅ 检查是否重复 + // Check for duplicates if traderIDs[traderID] { t.Errorf("Duplicate traderID detected: %s", traderID) } traderIDs[traderID] = true - // ✅ 验证格式:应该是 "exchange_model_uuid" + // Verify format: should be "exchange_model_uuid" if !isValidTraderIDFormat(traderID, exchangeID, aiModelID) { t.Errorf("Invalid traderID format: %s", traderID) } } - // ✅ 验证生成了预期数量的唯一 ID + // Verify expected number of unique IDs were generated if len(traderIDs) != numTraders { t.Errorf("Expected %d unique traderIDs, got %d", numTraders, len(traderIDs)) } } -// generateTraderID 辅助函数,模拟 api/server.go 中的 traderID 生成逻辑 +// generateTraderID Helper function that simulates traderID generation logic from api/server.go func generateTraderID(exchangeID, aiModelID string) string { return fmt.Sprintf("%s_%s_%s", exchangeID, aiModelID, uuid.New().String()) } -// isValidTraderIDFormat 验证 traderID 格式是否符合预期 +// isValidTraderIDFormat Verify traderID format matches expected format func isValidTraderIDFormat(traderID, expectedExchange, expectedModel string) bool { - // 格式:exchange_model_uuid - // 例如:binance_gpt-4_a1b2c3d4-e5f6-7890-abcd-ef1234567890 + // Format: exchange_model_uuid + // Example: binance_gpt-4_a1b2c3d4-e5f6-7890-abcd-ef1234567890 parts := strings.Split(traderID, "_") if len(parts) < 3 { return false } - // 验证前缀 + // Verify prefix if parts[0] != expectedExchange { return false } - // AI model 可能包含连字符(如 gpt-4),所以需要重组 - // 最后一部分应该是 UUID + // AI model may contain hyphens (e.g. gpt-4), so need to reconstruct + // Last part should be UUID uuidPart := parts[len(parts)-1] - // 验证 UUID 格式(36 个字符,包含 4 个连字符) + // Verify UUID format (36 characters, containing 4 hyphens) _, err := uuid.Parse(uuidPart) return err == nil } -// TestTraderIDFormat 测试 traderID 格式的正确性 +// TestTraderIDFormat Test traderID format correctness func TestTraderIDFormat(t *testing.T) { tests := []struct { name string @@ -84,18 +84,18 @@ func TestTraderIDFormat(t *testing.T) { t.Run(tt.name, func(t *testing.T) { traderID := generateTraderID(tt.exchangeID, tt.aiModelID) - // ✅ 验证包含正确的前缀 + // Verify correct prefix if !strings.HasPrefix(traderID, tt.exchangeID+"_"+tt.aiModelID+"_") { t.Errorf("traderID does not have correct prefix. Got: %s", traderID) } - // ✅ 验证格式有效 + // Verify format is valid if !isValidTraderIDFormat(traderID, tt.exchangeID, tt.aiModelID) { t.Errorf("Invalid traderID format: %s", traderID) } - // ✅ 验证长度合理(至少应该有 exchange + model + "_" + UUID(36) 的长度) - minLength := len(tt.exchangeID) + len(tt.aiModelID) + 2 + 36 // 2个下划线 + 36字符UUID + // Verify reasonable length (should be at least exchange + model + "_" + UUID(36)) + minLength := len(tt.exchangeID) + len(tt.aiModelID) + 2 + 36 // 2 underscores + 36 character UUID if len(traderID) < minLength { t.Errorf("traderID too short: expected at least %d chars, got %d", minLength, len(traderID)) } @@ -103,12 +103,12 @@ func TestTraderIDFormat(t *testing.T) { } } -// TestTraderIDNoCollision 测试在高并发场景下不会产生碰撞 +// TestTraderIDNoCollision Test that no collisions occur in high concurrency scenarios func TestTraderIDNoCollision(t *testing.T) { const iterations = 1000 uniqueIDs := make(map[string]bool, iterations) - // 模拟高并发场景 + // Simulate high concurrency scenario for i := 0; i < iterations; i++ { id := generateTraderID("binance", "gpt-4") if uniqueIDs[id] { diff --git a/api/utils.go b/api/utils.go index 6a1a31b5..147c2cf1 100644 --- a/api/utils.go +++ b/api/utils.go @@ -2,20 +2,20 @@ package api import "strings" -// MaskSensitiveString 脱敏敏感字符串,只显示前4位和后4位 -// 用于脱敏 API Key、Secret Key、Private Key 等敏感信息 +// MaskSensitiveString Mask sensitive strings, showing only first 4 and last 4 characters +// Used to mask API Key, Secret Key, Private Key and other sensitive information func MaskSensitiveString(s string) string { if s == "" { return "" } length := len(s) if length <= 8 { - return "****" // 字符串太短,全部隐藏 + return "****" // String too short, hide everything } return s[:4] + "****" + s[length-4:] } -// SanitizeModelConfigForLog 脱敏模型配置用于日志输出 +// SanitizeModelConfigForLog Sanitize model configuration for log output func SanitizeModelConfigForLog(models map[string]struct { Enabled bool `json:"enabled"` APIKey string `json:"api_key"` @@ -34,7 +34,7 @@ func SanitizeModelConfigForLog(models map[string]struct { return safe } -// SanitizeExchangeConfigForLog 脱敏交易所配置用于日志输出 +// SanitizeExchangeConfigForLog Sanitize exchange configuration for log output func SanitizeExchangeConfigForLog(exchanges map[string]struct { Enabled bool `json:"enabled"` APIKey string `json:"api_key"` @@ -54,7 +54,7 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct { "testnet": cfg.Testnet, } - // 只在有值时才添加脱敏后的敏感字段 + // Only add masked sensitive fields when they have values if cfg.APIKey != "" { safeExchange["api_key"] = MaskSensitiveString(cfg.APIKey) } @@ -68,7 +68,7 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct { safeExchange["lighter_private_key"] = MaskSensitiveString(cfg.LighterPrivateKey) } - // 非敏感字段直接添加 + // Add non-sensitive fields directly if cfg.HyperliquidWalletAddr != "" { safeExchange["hyperliquid_wallet_addr"] = cfg.HyperliquidWalletAddr } @@ -87,14 +87,14 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct { return safe } -// MaskEmail 脱敏邮箱地址,保留前2位和@后部分 +// MaskEmail Mask email address, keeping first 2 characters and domain part func MaskEmail(email string) string { if email == "" { return "" } parts := strings.Split(email, "@") if len(parts) != 2 { - return "****" // 格式不正确 + return "****" // Incorrect format } username := parts[0] domain := parts[1] diff --git a/api/utils_test.go b/api/utils_test.go index 3fecda4a..7e053a95 100644 --- a/api/utils_test.go +++ b/api/utils_test.go @@ -11,27 +11,27 @@ func TestMaskSensitiveString(t *testing.T) { expected string }{ { - name: "空字符串", + name: "Empty string", input: "", expected: "", }, { - name: "短字符串(小于等于8位)", + name: "Short string (8 characters or less)", input: "short", expected: "****", }, { - name: "正常API key", + name: "Normal API key", input: "sk-1234567890abcdefghijklmnopqrstuvwxyz", expected: "sk-1****wxyz", }, { - name: "正常私钥", + name: "Normal private key", input: "0x1234567890abcdef1234567890abcdef12345678", expected: "0x12****5678", }, { - name: "刚好9位", + name: "Exactly 9 characters", input: "123456789", expected: "1234****6789", }, @@ -119,7 +119,7 @@ func TestSanitizeExchangeConfigForLog(t *testing.T) { result := SanitizeExchangeConfigForLog(exchanges) - // 检查币安配置 + // Check Binance configuration binanceConfig, ok := result["binance"].(map[string]interface{}) if !ok { t.Fatal("binance config not found or wrong type") @@ -143,7 +143,7 @@ func TestSanitizeExchangeConfigForLog(t *testing.T) { t.Errorf("expected masked secret_key='bina****cdef', got %q", maskedSecretKey) } - // 检查 Hyperliquid 配置 + // Check Hyperliquid configuration hlConfig, ok := result["hyperliquid"].(map[string]interface{}) if !ok { t.Fatal("hyperliquid config not found or wrong type") @@ -154,7 +154,7 @@ func TestSanitizeExchangeConfigForLog(t *testing.T) { t.Fatal("hyperliquid_wallet_addr not found or wrong type") } - // 钱包地址不应该被脱敏 + // Wallet address should not be masked if walletAddr != "0x1234567890abcdef1234567890abcdef12345678" { t.Errorf("wallet address should not be masked, got %q", walletAddr) } @@ -167,22 +167,22 @@ func TestMaskEmail(t *testing.T) { expected string }{ { - name: "空邮箱", + name: "Empty email", input: "", expected: "", }, { - name: "格式错误", + name: "Invalid format", input: "notanemail", expected: "****", }, { - name: "正常邮箱", + name: "Normal email", input: "user@example.com", expected: "us****@example.com", }, { - name: "短用户名", + name: "Short username", input: "a@example.com", expected: "**@example.com", }, diff --git a/auth/auth.go b/auth/auth.go index 85fa184f..a6bbe736 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -13,33 +13,33 @@ import ( "golang.org/x/crypto/bcrypt" ) -// JWTSecret JWT密钥,将从配置中动态设置 +// JWTSecret is the JWT secret key, will be dynamically set from config var JWTSecret []byte -// tokenBlacklist 用于登出后的token黑名单(仅内存,按过期时间清理) +// tokenBlacklist for logged out tokens (memory only, cleaned by expiration time) var tokenBlacklist = struct { sync.RWMutex items map[string]time.Time }{items: make(map[string]time.Time)} -// maxBlacklistEntries 黑名单最大容量阈值 +// maxBlacklistEntries is the maximum capacity threshold for blacklist const maxBlacklistEntries = 100_000 -// OTPIssuer OTP发行者名称 +// OTPIssuer is the OTP issuer name const OTPIssuer = "nofxAI" -// SetJWTSecret 设置JWT密钥 +// SetJWTSecret sets the JWT secret key func SetJWTSecret(secret string) { JWTSecret = []byte(secret) } -// BlacklistToken 将token加入黑名单直到过期 +// BlacklistToken adds token to blacklist until expiration func BlacklistToken(token string, exp time.Time) { tokenBlacklist.Lock() defer tokenBlacklist.Unlock() tokenBlacklist.items[token] = exp - // 如果超过容量阈值,则进行一次过期清理;若仍超限,记录警告日志 + // If exceeds capacity threshold, perform expired cleanup; if still over limit, log warning if len(tokenBlacklist.items) > maxBlacklistEntries { now := time.Now() for t, e := range tokenBlacklist.items { @@ -54,7 +54,7 @@ func BlacklistToken(token string, exp time.Time) { } } -// IsTokenBlacklisted 检查token是否在黑名单中(过期自动清理) +// IsTokenBlacklisted checks if token is in blacklist (auto cleanup on expiration) func IsTokenBlacklisted(token string) bool { tokenBlacklist.Lock() defer tokenBlacklist.Unlock() @@ -68,26 +68,26 @@ func IsTokenBlacklisted(token string) bool { return false } -// Claims JWT声明 +// Claims represents JWT claims type Claims struct { UserID string `json:"user_id"` Email string `json:"email"` jwt.RegisteredClaims } -// HashPassword 哈希密码 +// HashPassword hashes the password func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err } -// CheckPassword 验证密码 +// CheckPassword verifies the password func CheckPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } -// GenerateOTPSecret 生成OTP密钥 +// GenerateOTPSecret generates OTP secret func GenerateOTPSecret() (string, error) { secret := make([]byte, 20) _, err := rand.Read(secret) @@ -106,18 +106,18 @@ func GenerateOTPSecret() (string, error) { return key.Secret(), nil } -// VerifyOTP 验证OTP码 +// VerifyOTP verifies OTP code func VerifyOTP(secret, code string) bool { return totp.Validate(code, secret) } -// GenerateJWT 生成JWT token +// GenerateJWT generates JWT token func GenerateJWT(userID, email string) (string, error) { claims := Claims{ UserID: userID, Email: email, RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // 24小时过期 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Expires in 24 hours IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "nofxAI", @@ -128,11 +128,11 @@ func GenerateJWT(userID, email string) (string, error) { return token.SignedString(JWTSecret) } -// ValidateJWT 验证JWT token +// ValidateJWT validates JWT token func ValidateJWT(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("意外的签名方法: %v", token.Header["alg"]) + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return JWTSecret, nil }) @@ -145,10 +145,10 @@ func ValidateJWT(tokenString string) (*Claims, error) { return claims, nil } - return nil, fmt.Errorf("无效的token") + return nil, fmt.Errorf("invalid token") } -// GetOTPQRCodeURL 获取OTP二维码URL +// GetOTPQRCodeURL gets OTP QR code URL func GetOTPQRCodeURL(secret, email string) string { return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", OTPIssuer, email, secret, OTPIssuer) } diff --git a/backtest/account.go b/backtest/account.go index 7838a41f..2ef85762 100644 --- a/backtest/account.go +++ b/backtest/account.go @@ -89,7 +89,7 @@ func (acc *BacktestAccount) Open(symbol, side string, quantity float64, leverage pos.LiquidationPrice = computeLiquidation(execPrice, leverage, side) } else { if leverage != pos.Leverage { - // 采用权重平均杠杆(近似) + // Use weighted average leverage (approximate) weightedMargin := pos.Margin + margin pos.Leverage = int(math.Round((pos.Notional + notional) / weightedMargin)) } @@ -227,7 +227,7 @@ func (acc *BacktestAccount) RealizedPnL() float64 { return acc.realizedPnL } -// RestoreFromSnapshots 用于从检查点恢复账户状态。 +// RestoreFromSnapshots restores account state from checkpoint. func (acc *BacktestAccount) RestoreFromSnapshots(cash float64, realized float64, snaps []PositionSnapshot) { acc.cash = cash acc.realizedPnL = realized diff --git a/backtest/ai_client.go b/backtest/ai_client.go index 9a93c225..302a86ed 100644 --- a/backtest/ai_client.go +++ b/backtest/ai_client.go @@ -7,8 +7,8 @@ import ( "nofx/mcp" ) -// configureMCPClient 根据配置创建/克隆 MCP 客户端(返回 mcp.AIClient 接口)。 -// 说明:mcp.New() 返回接口类型,这里统一转为具体实现再做拷贝,避免并发共享状态。 +// configureMCPClient creates/clones an MCP client based on configuration (returns mcp.AIClient interface). +// Note: mcp.New() returns an interface type; here we convert to concrete implementation before copying to avoid concurrent shared state. func configureMCPClient(cfg BacktestConfig, base mcp.AIClient) (mcp.AIClient, error) { provider := strings.ToLower(strings.TrimSpace(cfg.AICfg.Provider)) @@ -48,9 +48,9 @@ func configureMCPClient(cfg BacktestConfig, base mcp.AIClient) (mcp.AIClient, er } } -// cloneBaseClient 复制基础客户端以避免共享可变状态。 +// cloneBaseClient copies the base client to avoid shared mutable state. func cloneBaseClient(base mcp.AIClient) *mcp.Client { - // 优先尝试复用传入的基础客户端(深拷贝) + // Prefer to reuse the passed-in base client (deep copy) switch c := base.(type) { case *mcp.Client: cp := *c @@ -66,6 +66,6 @@ func cloneBaseClient(base mcp.AIClient) *mcp.Client { return &cp } } - // 回退到新的默认客户端 + // Fall back to a new default client return mcp.NewClient().(*mcp.Client) } diff --git a/backtest/aicache.go b/backtest/aicache.go index 141aff60..7d4f7168 100644 --- a/backtest/aicache.go +++ b/backtest/aicache.go @@ -20,7 +20,7 @@ type cachedDecision struct { Decision *decision.FullDecision `json:"decision"` } -// AICache 持久化 AI 决策,便于重复回测或重放。 +// AICache persists AI decisions for repeated backtesting or replay. type AICache struct { mu sync.RWMutex path string diff --git a/backtest/config.go b/backtest/config.go index 6aabcc3f..2062f25e 100644 --- a/backtest/config.go +++ b/backtest/config.go @@ -8,7 +8,7 @@ import ( "nofx/market" ) -// AIConfig 定义回测中使用的 AI 客户端配置。 +// AIConfig defines the AI client configuration used in backtesting. type AIConfig struct { Provider string `json:"provider"` Model string `json:"model"` @@ -23,7 +23,7 @@ type LeverageConfig struct { AltcoinLeverage int `json:"altcoin_leverage"` } -// BacktestConfig 描述一次回测运行的输入配置。 +// BacktestConfig describes the input configuration for a backtest run. type BacktestConfig struct { RunID string `json:"run_id"` UserID string `json:"user_id,omitempty"` @@ -54,7 +54,7 @@ type BacktestConfig struct { ReplayDecisionDir string `json:"replay_decision_dir,omitempty"` } -// Validate 对配置进行合法性检查并填充默认值。 +// Validate performs validity checks on the configuration and fills in default values. func (cfg *BacktestConfig) Validate() error { if cfg == nil { return fmt.Errorf("config is nil") @@ -151,7 +151,7 @@ func (cfg *BacktestConfig) Validate() error { return nil } -// Duration 返回回测区间时长。 +// Duration returns the backtest interval duration. func (cfg *BacktestConfig) Duration() time.Duration { if cfg == nil { return 0 @@ -160,11 +160,11 @@ func (cfg *BacktestConfig) Duration() time.Duration { } const ( - // FillPolicyNextOpen 使用下一根 K 线的开盘价成交。 + // FillPolicyNextOpen uses the open price of the next bar for execution. FillPolicyNextOpen = "next_open" - // FillPolicyBarVWAP 采用当前 K 线的近似 VWAP 成交。 + // FillPolicyBarVWAP uses the approximate VWAP of the current bar for execution. FillPolicyBarVWAP = "bar_vwap" - // FillPolicyMidPrice 采用 (high+low)/2 的中间价成交。 + // FillPolicyMidPrice uses the mid-price (high+low)/2 for execution. FillPolicyMidPrice = "mid" ) diff --git a/backtest/datafeed.go b/backtest/datafeed.go index 05b21edf..5480fffc 100644 --- a/backtest/datafeed.go +++ b/backtest/datafeed.go @@ -17,7 +17,7 @@ type symbolSeries struct { byTF map[string]*timeframeSeries } -// DataFeed 管理历史K线数据,为回测提供按时间推进的快照。 +// DataFeed manages historical kline data and provides time-progressive snapshots for backtesting. type DataFeed struct { cfg BacktestConfig symbols []string @@ -49,7 +49,7 @@ func (df *DataFeed) loadAll() error { start := time.Unix(df.cfg.StartTS, 0) end := time.Unix(df.cfg.EndTS, 0) - // longest timeframe用于辅助指标 + // longest timeframe used for auxiliary indicators var longestDur time.Duration for _, tf := range df.timeframes { dur, err := market.TFDuration(tf) @@ -93,7 +93,7 @@ func (df *DataFeed) loadAll() error { df.symbolSeries[symbol] = ss } - // 以第一个符号的主周期生成回测进度时间轴 + // Generate backtest progress timeline using the primary timeframe of the first symbol firstSymbol := df.symbols[0] primarySeries := df.symbolSeries[firstSymbol].byTF[df.primaryTF] startMs := start.UnixMilli() @@ -106,7 +106,7 @@ func (df *DataFeed) loadAll() error { break } df.decisionTimes = append(df.decisionTimes, ts) - // 对齐其他符号,如果缺数据则提前报错 + // Align other symbols; report error early if data is missing for _, symbol := range df.symbols[1:] { if _, ok := df.symbolSeries[symbol].byTF[df.primaryTF]; !ok { return fmt.Errorf("symbol %s missing timeframe %s", symbol, df.primaryTF) diff --git a/backtest/equity.go b/backtest/equity.go index 6d143931..8f153268 100644 --- a/backtest/equity.go +++ b/backtest/equity.go @@ -7,7 +7,7 @@ import ( "nofx/market" ) -// ResampleEquity 根据时间周期重采样资金曲线。 +// ResampleEquity resamples equity curve based on timeframe. func ResampleEquity(points []EquityPoint, timeframe string) ([]EquityPoint, error) { if timeframe == "" { return points, nil @@ -49,7 +49,7 @@ func ResampleEquity(points []EquityPoint, timeframe string) ([]EquityPoint, erro return resampled, nil } -// LimitEquityPoints 将数据点数量限制在给定范围内(均匀抽样)。 +// LimitEquityPoints limits the number of data points within a given range (uniform sampling). func LimitEquityPoints(points []EquityPoint, limit int) []EquityPoint { if limit <= 0 || len(points) <= limit { return points @@ -68,7 +68,7 @@ func LimitEquityPoints(points []EquityPoint, limit int) []EquityPoint { return result } -// LimitTradeEvents 同样对交易事件按均匀抽样。 +// LimitTradeEvents applies uniform sampling to trade events. func LimitTradeEvents(events []TradeEvent, limit int) []TradeEvent { if limit <= 0 || len(events) <= limit { return events @@ -86,7 +86,7 @@ func LimitTradeEvents(events []TradeEvent, limit int) []TradeEvent { return result } -// AlignEquityTimestamps 确保时间戳按升序排列。 +// AlignEquityTimestamps ensures timestamps are sorted in ascending order. func AlignEquityTimestamps(points []EquityPoint) []EquityPoint { sort.Slice(points, func(i, j int) bool { return points[i].Timestamp < points[j].Timestamp diff --git a/backtest/lock.go b/backtest/lock.go index 26edbfc4..6f26ed08 100644 --- a/backtest/lock.go +++ b/backtest/lock.go @@ -15,7 +15,7 @@ const ( lockStaleAfter = 10 * time.Second ) -// RunLockInfo 表示回测运行的锁文件结构。 +// RunLockInfo represents the lock file structure for a backtest run. type RunLockInfo struct { RunID string `json:"run_id"` PID int `json:"pid"` diff --git a/backtest/manager.go b/backtest/manager.go index e0359cac..34ec9ec2 100644 --- a/backtest/manager.go +++ b/backtest/manager.go @@ -438,7 +438,7 @@ func (m *Manager) resolveAIConfig(cfg *BacktestConfig) error { m.mu.RUnlock() if resolver == nil { if apiKey == "" { - return fmt.Errorf("AI配置缺少密钥且未配置解析器") + return fmt.Errorf("AI configuration missing key and no resolver configured") } return nil } @@ -453,7 +453,7 @@ func (m *Manager) ExportRun(runID string) (string, error) { return CreateRunExport(runID) } -// RestoreRunsFromDisk 扫描 backtests 目录并恢复现有 run 的元数据(服务重启场景)。 +// RestoreRuns scans the backtests directory and restores metadata for existing runs (service restart scenario). func (m *Manager) RestoreRuns() error { runIDs, err := LoadRunIDs() if err != nil { @@ -487,7 +487,7 @@ func (m *Manager) RestoreRuns() error { return nil } -// RestoreRunsFromDisk 保留旧方法名,兼容历史调用。 +// RestoreRunsFromDisk retains the old method name for backward compatibility. func (m *Manager) RestoreRunsFromDisk() error { return m.RestoreRuns() } diff --git a/backtest/metrics.go b/backtest/metrics.go index 789abedc..e7fe5bc5 100644 --- a/backtest/metrics.go +++ b/backtest/metrics.go @@ -6,7 +6,7 @@ import ( "strings" ) -// CalculateMetrics 读取已有日志并计算汇总指标。state 可选,用于补充尚未落盘的信息。 +// CalculateMetrics reads existing logs and calculates summary metrics. state is optional, used to supplement information not yet persisted. func CalculateMetrics(runID string, cfg *BacktestConfig, state *BacktestState) (*Metrics, error) { if cfg == nil { return nil, fmt.Errorf("config is nil") diff --git a/backtest/runner.go b/backtest/runner.go index 2c94954b..761196b1 100644 --- a/backtest/runner.go +++ b/backtest/runner.go @@ -29,7 +29,7 @@ const ( aiDecisionMaxRetries = 3 ) -// Runner 封装单次回测运行的生命周期。 +// Runner encapsulates the lifecycle of a single backtest run. type Runner struct { cfg BacktestConfig feed *DataFeed @@ -63,7 +63,7 @@ type Runner struct { lockStop chan struct{} } -// NewRunner 构建回测运行器。 +// NewRunner constructs a backtest runner. func NewRunner(cfg BacktestConfig, mcpClient mcp.AIClient) (*Runner, error) { if err := ensureRunDir(cfg.RunID); err != nil { return nil, err @@ -179,7 +179,7 @@ func (r *Runner) releaseLock() { r.lockInfo = nil } -// Start 启动回测循环。 +// Start launches the backtest loop. func (r *Runner) Start(ctx context.Context) error { r.statusMu.Lock() if r.status != RunStateCreated && r.status != RunStatePaused { @@ -193,7 +193,7 @@ func (r *Runner) Start(ctx context.Context) error { return nil } -// PersistMetadata 将当前快照写入 run.json。 +// PersistMetadata writes the current snapshot to run.json. func (r *Runner) PersistMetadata() { r.persistMetadata() } @@ -214,7 +214,7 @@ func (r *Runner) lastErrorString() string { return r.lastError } -// CurrentMetadata 返回当前内存状态对应的元数据。 +// CurrentMetadata returns the metadata corresponding to the current in-memory state. func (r *Runner) CurrentMetadata() *RunMetadata { state := r.snapshotState() meta := r.buildMetadata(state, r.Status()) @@ -292,7 +292,7 @@ func (r *Runner) stepOnce() error { ctx, rec, err := r.buildDecisionContext(ts, marketData, multiTF, priceMap, callCount) if err != nil { rec.Success = false - rec.ErrorMessage = fmt.Sprintf("构建交易上下文失败: %v", err) + rec.ErrorMessage = fmt.Sprintf("failed to build trading context: %v", err) _ = r.logDecision(rec) return err } @@ -312,7 +312,7 @@ func (r *Runner) stepOnce() error { } else if r.cfg.ReplayOnly { decisionErr := fmt.Errorf("replay_only enabled but cache miss at %d", ts) record.Success = false - record.ErrorMessage = fmt.Sprintf("没有找到 ts=%d 的缓存决策", ts) + record.ErrorMessage = fmt.Sprintf("cached decision not found for ts=%d", ts) _ = r.logDecision(record) return decisionErr } @@ -327,8 +327,8 @@ func (r *Runner) stepOnce() error { decisionAttempted = true hadError = true record.Success = false - record.ErrorMessage = fmt.Sprintf("AI决策失败: %v", err) - execLog = append(execLog, fmt.Sprintf("⚠️ AI决策失败: %v", err)) + record.ErrorMessage = fmt.Sprintf("AI decision failed: %v", err) + execLog = append(execLog, fmt.Sprintf("⚠️ AI decision failed: %v", err)) r.setLastError(err) } else { fullDecision = fd @@ -392,7 +392,7 @@ func (r *Runner) stepOnce() error { hadError = true tradeEvents = append(tradeEvents, liquidationEvents...) if record != nil { - execLog = append(execLog, fmt.Sprintf("⚠️ 强制平仓: %s", liquidationNote)) + execLog = append(execLog, fmt.Sprintf("⚠️ Forced liquidation: %s", liquidationNote)) } } @@ -690,7 +690,7 @@ func (r *Runner) executeDecision(dec decision.Decision, priceMap map[string]floa return actionRecord, []TradeEvent{trade}, "", nil case "hold", "wait": - return actionRecord, nil, fmt.Sprintf("保持仓位: %s", dec.Action), nil + return actionRecord, nil, fmt.Sprintf("hold position: %s", dec.Action), nil default: return actionRecord, nil, "", fmt.Errorf("unsupported action %s", dec.Action) } @@ -1078,14 +1078,14 @@ func (r *Runner) Wait() error { return r.err } -// Status 返回当前运行状态。 +// Status returns the current run state. func (r *Runner) Status() RunState { r.statusMu.RLock() defer r.statusMu.RUnlock() return r.status } -// StatusPayload 构建用于 API 的状态响应。 +// StatusPayload builds the status response for the API. func (r *Runner) StatusPayload() StatusPayload { snapshot := r.snapshotState() progress := progressPercent(snapshot, r.cfg) diff --git a/backtest/storage.go b/backtest/storage.go index c5bf1405..84cbcaa6 100644 --- a/backtest/storage.go +++ b/backtest/storage.go @@ -132,7 +132,7 @@ func appendJSONLine(path string, payload any) error { return f.Sync() } -// SaveCheckpoint 将检查点写入磁盘。 +// SaveCheckpoint writes the checkpoint to disk. func SaveCheckpoint(runID string, ckpt *Checkpoint) error { if ckpt == nil { return fmt.Errorf("checkpoint is nil") @@ -143,7 +143,7 @@ func SaveCheckpoint(runID string, ckpt *Checkpoint) error { return writeJSONAtomic(checkpointPath(runID), ckpt) } -// LoadCheckpoint 读取最近一次检查点。 +// LoadCheckpoint reads the most recent checkpoint. func LoadCheckpoint(runID string) (*Checkpoint, error) { if usingDB() { return loadCheckpointDB(runID) @@ -160,7 +160,7 @@ func LoadCheckpoint(runID string) (*Checkpoint, error) { return &ckpt, nil } -// SaveRunMetadata 写入 run.json。 +// SaveRunMetadata writes to run.json. func SaveRunMetadata(meta *RunMetadata) error { if meta == nil { return fmt.Errorf("run metadata is nil") @@ -178,7 +178,7 @@ func SaveRunMetadata(meta *RunMetadata) error { return writeJSONAtomic(runMetadataPath(meta.RunID), meta) } -// LoadRunMetadata 读取 run.json。 +// LoadRunMetadata reads run.json. func LoadRunMetadata(runID string) (*RunMetadata, error) { if usingDB() { return loadRunMetadataDB(runID) diff --git a/backtest/types.go b/backtest/types.go index b52b2d8f..dbd42abd 100644 --- a/backtest/types.go +++ b/backtest/types.go @@ -2,7 +2,7 @@ package backtest import "time" -// RunState 表示回测运行当前状态。 +// RunState represents the current state of a backtest run. type RunState string const ( @@ -15,7 +15,7 @@ const ( RunStateLiquidated RunState = "liquidated" ) -// PositionSnapshot 表示当前持仓的核心数据,用于回测状态与持久化。 +// PositionSnapshot represents core position data for backtest state and persistence. type PositionSnapshot struct { Symbol string `json:"symbol"` Side string `json:"side"` @@ -27,7 +27,7 @@ type PositionSnapshot struct { OpenTime int64 `json:"open_time"` } -// BacktestState 表示执行过程中的实时状态(内存态)。 +// BacktestState represents the real-time state during execution (in-memory state). type BacktestState struct { BarIndex int BarTimestamp int64 @@ -46,7 +46,7 @@ type BacktestState struct { LiquidationNote string } -// EquityPoint 表示资金曲线中的单个节点。 +// EquityPoint represents a single point on the equity curve. type EquityPoint struct { Timestamp int64 `json:"ts"` Equity float64 `json:"equity"` @@ -57,7 +57,7 @@ type EquityPoint struct { Cycle int `json:"cycle"` } -// TradeEvent 记录一次交易执行结果或特殊事件(如爆仓)。 +// TradeEvent records a trade execution result or special event (such as liquidation). type TradeEvent struct { Timestamp int64 `json:"ts"` Symbol string `json:"symbol"` @@ -76,7 +76,7 @@ type TradeEvent struct { Note string `json:"note,omitempty"` } -// Metrics 汇总回测表现指标。 +// Metrics summarizes backtest performance metrics. type Metrics struct { TotalReturnPct float64 `json:"total_return_pct"` MaxDrawdownPct float64 `json:"max_drawdown_pct"` @@ -92,7 +92,7 @@ type Metrics struct { Liquidated bool `json:"liquidated"` } -// SymbolMetrics 记录单个标的的表现。 +// SymbolMetrics records performance for a single symbol. type SymbolMetrics struct { TotalTrades int `json:"total_trades"` WinningTrades int `json:"winning_trades"` @@ -102,7 +102,7 @@ type SymbolMetrics struct { WinRate float64 `json:"win_rate"` } -// Checkpoint 表示磁盘保存的检查点信息,用于暂停、恢复与崩溃恢复。 +// Checkpoint represents checkpoint information saved to disk for pause, resume, and crash recovery. type Checkpoint struct { BarIndex int `json:"bar_index"` BarTimestamp int64 `json:"bar_ts"` @@ -122,7 +122,7 @@ type Checkpoint struct { LiquidationNote string `json:"liquidation_note,omitempty"` } -// RunMetadata 记录 run.json 所需摘要。 +// RunMetadata records the summary required for run.json. type RunMetadata struct { RunID string `json:"run_id"` Label string `json:"label,omitempty"` @@ -135,7 +135,7 @@ type RunMetadata struct { Summary RunSummary `json:"summary"` } -// RunSummary 为 run.json 中的 summary 字段。 +// RunSummary represents the summary field in run.json. type RunSummary struct { SymbolCount int `json:"symbol_count"` DecisionTF string `json:"decision_tf"` @@ -147,7 +147,7 @@ type RunSummary struct { LiquidationNote string `json:"liquidation_note,omitempty"` } -// StatusPayload 用于 /status API 的响应。 +// StatusPayload is used for /status API responses. type StatusPayload struct { RunID string `json:"run_id"` State RunState `json:"state"` diff --git a/config/config.go b/config/config.go index e8dfc75e..3b550dcb 100644 --- a/config/config.go +++ b/config/config.go @@ -6,26 +6,26 @@ import ( "strings" ) -// 全局配置实例 +// Global configuration instance var global *Config -// Config 全局配置(从 .env 加载) -// 只包含真正的全局配置,交易相关配置在 trader/策略 级别 +// Config is the global configuration (loaded from .env) +// Only contains truly global config, trading related config is at trader/strategy level type Config struct { - // 服务配置 + // Service configuration APIServerPort int JWTSecret string RegistrationEnabled bool } -// Init 初始化全局配置(从 .env 加载) +// Init initializes global configuration (from .env) func Init() { cfg := &Config{ APIServerPort: 8080, RegistrationEnabled: true, } - // 从环境变量加载 + // Load from environment variables if v := os.Getenv("JWT_SECRET"); v != "" { cfg.JWTSecret = strings.TrimSpace(v) } @@ -46,7 +46,7 @@ func Init() { global = cfg } -// Get 获取全局配置 +// Get returns the global configuration func Get() *Config { if global == nil { Init() diff --git a/crypto/crypto.go b/crypto/crypto.go index 9c33cfe6..76927f6c 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -23,10 +23,10 @@ const ( storageDelimiter = ":" ) -// 环境变量名称 +// Environment variable names const ( - EnvDataEncryptionKey = "DATA_ENCRYPTION_KEY" // AES 数据加密密钥 (Base64) - EnvRSAPrivateKey = "RSA_PRIVATE_KEY" // RSA 私钥 (PEM 格式,换行用 \n) + EnvDataEncryptionKey = "DATA_ENCRYPTION_KEY" // AES data encryption key (Base64) + EnvRSAPrivateKey = "RSA_PRIVATE_KEY" // RSA private key (PEM format, use \n for newlines) ) type EncryptedPayload struct { @@ -51,18 +51,18 @@ type CryptoService struct { dataKey []byte } -// NewCryptoService 创建加密服务(从环境变量加载密钥) +// NewCryptoService creates crypto service (loads keys from environment variables) func NewCryptoService() (*CryptoService, error) { - // 1. 加载 RSA 私钥 + // 1. Load RSA private key privateKey, err := loadRSAPrivateKeyFromEnv() if err != nil { - return nil, fmt.Errorf("RSA 私钥加载失败: %w", err) + return nil, fmt.Errorf("failed to load RSA private key: %w", err) } - // 2. 加载 AES 数据加密密钥 + // 2. Load AES data encryption key dataKey, err := loadDataKeyFromEnv() if err != nil { - return nil, fmt.Errorf("数据加密密钥加载失败: %w", err) + return nil, fmt.Errorf("failed to load data encryption key: %w", err) } return &CryptoService{ @@ -72,43 +72,43 @@ func NewCryptoService() (*CryptoService, error) { }, nil } -// loadRSAPrivateKeyFromEnv 从环境变量加载 RSA 私钥 +// loadRSAPrivateKeyFromEnv loads RSA private key from environment variable func loadRSAPrivateKeyFromEnv() (*rsa.PrivateKey, error) { keyPEM := os.Getenv(EnvRSAPrivateKey) if keyPEM == "" { - return nil, fmt.Errorf("环境变量 %s 未设置,请在 .env 中配置 RSA 私钥", EnvRSAPrivateKey) + return nil, fmt.Errorf("environment variable %s not set, please configure RSA private key in .env", EnvRSAPrivateKey) } - // 处理环境变量中的换行符(\n -> 实际换行) + // Handle newlines in environment variable (\n -> actual newline) keyPEM = strings.ReplaceAll(keyPEM, "\\n", "\n") return ParseRSAPrivateKeyFromPEM([]byte(keyPEM)) } -// loadDataKeyFromEnv 从环境变量加载 AES 数据加密密钥 +// loadDataKeyFromEnv loads AES data encryption key from environment variable func loadDataKeyFromEnv() ([]byte, error) { keyStr := strings.TrimSpace(os.Getenv(EnvDataEncryptionKey)) if keyStr == "" { - return nil, fmt.Errorf("环境变量 %s 未设置,请在 .env 中配置数据加密密钥", EnvDataEncryptionKey) + return nil, fmt.Errorf("environment variable %s not set, please configure data encryption key in .env", EnvDataEncryptionKey) } - // 尝试解码 + // Try to decode if key, ok := decodePossibleKey(keyStr); ok { return key, nil } - // 如果无法解码,使用 SHA256 哈希作为密钥 + // If decoding fails, use SHA256 hash as key sum := sha256.Sum256([]byte(keyStr)) key := make([]byte, len(sum)) copy(key, sum[:]) return key, nil } -// ParseRSAPrivateKeyFromPEM 解析 PEM 格式的 RSA 私钥 +// ParseRSAPrivateKeyFromPEM parses RSA private key from PEM format func ParseRSAPrivateKeyFromPEM(pemBytes []byte) (*rsa.PrivateKey, error) { block, _ := pem.Decode(pemBytes) if block == nil { - return nil, errors.New("无效的 PEM 格式") + return nil, errors.New("invalid PEM format") } switch block.Type { @@ -121,15 +121,15 @@ func ParseRSAPrivateKeyFromPEM(pemBytes []byte) (*rsa.PrivateKey, error) { } rsaKey, ok := key.(*rsa.PrivateKey) if !ok { - return nil, errors.New("不是 RSA 密钥") + return nil, errors.New("not an RSA key") } return rsaKey, nil default: - return nil, errors.New("不支持的密钥类型: " + block.Type) + return nil, errors.New("unsupported key type: " + block.Type) } } -// decodePossibleKey 尝试用多种编码方式解码密钥 +// decodePossibleKey tries to decode key using multiple encoding methods func decodePossibleKey(value string) ([]byte, bool) { decoders := []func(string) ([]byte, error){ base64.StdEncoding.DecodeString, @@ -148,7 +148,7 @@ func decodePossibleKey(value string) ([]byte, bool) { return nil, false } -// normalizeAESKey 标准化 AES 密钥长度 +// normalizeAESKey normalizes AES key length func normalizeAESKey(raw []byte) ([]byte, bool) { switch len(raw) { case 16, 24, 32: @@ -186,7 +186,7 @@ func (cs *CryptoService) EncryptForStorage(plaintext string, aadParts ...string) return "", nil } if !cs.HasDataKey() { - return "", errors.New("数据加密密钥未配置") + return "", errors.New("data encryption key not configured") } if isEncryptedStorageValue(plaintext) { return plaintext, nil @@ -220,26 +220,26 @@ func (cs *CryptoService) DecryptFromStorage(value string, aadParts ...string) (s return "", nil } if !cs.HasDataKey() { - return "", errors.New("数据加密密钥未配置") + return "", errors.New("data encryption key not configured") } if !isEncryptedStorageValue(value) { - return "", errors.New("数据未加密") + return "", errors.New("data not encrypted") } payload := strings.TrimPrefix(value, storagePrefix) parts := strings.SplitN(payload, storageDelimiter, 2) if len(parts) != 2 { - return "", errors.New("无效的加密数据格式") + return "", errors.New("invalid encrypted data format") } nonce, err := base64.StdEncoding.DecodeString(parts[0]) if err != nil { - return "", fmt.Errorf("解码 nonce 失败: %w", err) + return "", fmt.Errorf("failed to decode nonce: %w", err) } ciphertext, err := base64.StdEncoding.DecodeString(parts[1]) if err != nil { - return "", fmt.Errorf("解码密文失败: %w", err) + return "", fmt.Errorf("failed to decode ciphertext: %w", err) } block, err := aes.NewCipher(cs.dataKey) @@ -253,13 +253,13 @@ func (cs *CryptoService) DecryptFromStorage(value string, aadParts ...string) (s } if len(nonce) != gcm.NonceSize() { - return "", fmt.Errorf("无效的 nonce 长度: 期望 %d, 实际 %d", gcm.NonceSize(), len(nonce)) + return "", fmt.Errorf("invalid nonce length: expected %d, got %d", gcm.NonceSize(), len(nonce)) } aad := composeAAD(aadParts) plaintext, err := gcm.Open(nil, nonce, ciphertext, aad) if err != nil { - return "", fmt.Errorf("解密失败: %w", err) + return "", fmt.Errorf("decryption failed: %w", err) } return string(plaintext), nil @@ -281,67 +281,67 @@ func isEncryptedStorageValue(value string) bool { } func (cs *CryptoService) DecryptPayload(payload *EncryptedPayload) ([]byte, error) { - // 1. 验证时间戳(防止重放攻击) + // 1. Validate timestamp (prevent replay attacks) if payload.TS != 0 { elapsed := time.Since(time.Unix(payload.TS, 0)) if elapsed > 5*time.Minute || elapsed < -1*time.Minute { - return nil, errors.New("时间戳无效或已过期") + return nil, errors.New("timestamp invalid or expired") } } - // 2. 解码 base64url + // 2. Decode base64url wrappedKey, err := base64.RawURLEncoding.DecodeString(payload.WrappedKey) if err != nil { - return nil, fmt.Errorf("解码 wrapped key 失败: %w", err) + return nil, fmt.Errorf("failed to decode wrapped key: %w", err) } iv, err := base64.RawURLEncoding.DecodeString(payload.IV) if err != nil { - return nil, fmt.Errorf("解码 IV 失败: %w", err) + return nil, fmt.Errorf("failed to decode IV: %w", err) } ciphertext, err := base64.RawURLEncoding.DecodeString(payload.Ciphertext) if err != nil { - return nil, fmt.Errorf("解码密文失败: %w", err) + return nil, fmt.Errorf("failed to decode ciphertext: %w", err) } var aad []byte if payload.AAD != "" { aad, err = base64.RawURLEncoding.DecodeString(payload.AAD) if err != nil { - return nil, fmt.Errorf("解码 AAD 失败: %w", err) + return nil, fmt.Errorf("failed to decode AAD: %w", err) } var aadData AADData if err := json.Unmarshal(aad, &aadData); err == nil { - // 可以在这里添加额外的验证逻辑 + // Additional validation logic can be added here } } - // 3. 使用 RSA-OAEP 解密 AES 密钥 + // 3. Decrypt AES key using RSA-OAEP aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, cs.privateKey, wrappedKey, nil) if err != nil { - return nil, fmt.Errorf("RSA 解密失败: %w", err) + return nil, fmt.Errorf("RSA decryption failed: %w", err) } - // 4. 使用 AES-GCM 解密数据 + // 4. Decrypt data using AES-GCM block, err := aes.NewCipher(aesKey) if err != nil { - return nil, fmt.Errorf("创建 AES cipher 失败: %w", err) + return nil, fmt.Errorf("failed to create AES cipher: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { - return nil, fmt.Errorf("创建 GCM 失败: %w", err) + return nil, fmt.Errorf("failed to create GCM: %w", err) } if len(iv) != gcm.NonceSize() { - return nil, fmt.Errorf("无效的 IV 长度: 期望 %d, 实际 %d", gcm.NonceSize(), len(iv)) + return nil, fmt.Errorf("invalid IV length: expected %d, got %d", gcm.NonceSize(), len(iv)) } plaintext, err := gcm.Open(nil, iv, ciphertext, aad) if err != nil { - return nil, fmt.Errorf("解密验证失败: %w", err) + return nil, fmt.Errorf("decryption verification failed: %w", err) } return plaintext, nil @@ -355,21 +355,21 @@ func (cs *CryptoService) DecryptSensitiveData(payload *EncryptedPayload) (string return string(plaintext), nil } -// GenerateKeyPair 生成 RSA 密钥对(用于初始化时生成密钥) -// 返回 PEM 格式的私钥和公钥 +// GenerateKeyPair generates RSA key pair (for key generation during initialization) +// Returns PEM format private key and public key func GenerateKeyPair() (privateKeyPEM, publicKeyPEM string, err error) { privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return "", "", err } - // 编码私钥 + // Encode private key privPEM := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey), }) - // 编码公钥 + // Encode public key publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) if err != nil { return "", "", err @@ -383,8 +383,8 @@ func GenerateKeyPair() (privateKeyPEM, publicKeyPEM string, err error) { return string(privPEM), string(pubPEM), nil } -// GenerateDataKey 生成 AES 数据加密密钥 -// 返回 Base64 编码的 32 字节密钥 +// GenerateDataKey generates AES data encryption key +// Returns Base64 encoded 32-byte key func GenerateDataKey() (string, error) { key := make([]byte, 32) if _, err := rand.Read(key); err != nil { diff --git a/decision/engine.go b/decision/engine.go index 2d73bab6..bab18fb9 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -13,22 +13,22 @@ import ( "time" ) -// 预编译正则表达式(性能优化:避免每次调用时重新编译) +// Pre-compiled regular expressions (performance optimization: avoid recompiling on each call) var ( - // ✅ 安全的正則:精確匹配 ```json 代碼塊 - // 使用反引號 + 拼接避免轉義問題 + // Safe regex: precisely match ```json code blocks + // Use backtick + concatenation to avoid escape issues reJSONFence = regexp.MustCompile(`(?is)` + "```json\\s*(\\[\\s*\\{.*?\\}\\s*\\])\\s*```") reJSONArray = regexp.MustCompile(`(?is)\[\s*\{.*?\}\s*\]`) reArrayHead = regexp.MustCompile(`^\[\s*\{`) reArrayOpenSpace = regexp.MustCompile(`^\[\s+\{`) reInvisibleRunes = regexp.MustCompile("[\u200B\u200C\u200D\uFEFF]") - // 新增:XML标签提取(支持思维链中包含任何字符) + // XML tag extraction (supports any characters in reasoning chain) reReasoningTag = regexp.MustCompile(`(?s)(.*?)`) reDecisionTag = regexp.MustCompile(`(?s)(.*?)`) ) -// PositionInfo 持仓信息 +// PositionInfo position information type PositionInfo struct { Symbol string `json:"symbol"` Side string `json:"side"` // "long" or "short" @@ -38,64 +38,64 @@ type PositionInfo struct { Leverage int `json:"leverage"` UnrealizedPnL float64 `json:"unrealized_pnl"` UnrealizedPnLPct float64 `json:"unrealized_pnl_pct"` - PeakPnLPct float64 `json:"peak_pnl_pct"` // 历史最高收益率(百分比) + PeakPnLPct float64 `json:"peak_pnl_pct"` // Historical peak profit percentage LiquidationPrice float64 `json:"liquidation_price"` MarginUsed float64 `json:"margin_used"` - UpdateTime int64 `json:"update_time"` // 持仓更新时间戳(毫秒) + UpdateTime int64 `json:"update_time"` // Position update timestamp (milliseconds) } -// AccountInfo 账户信息 +// AccountInfo account information type AccountInfo struct { - TotalEquity float64 `json:"total_equity"` // 账户净值 - AvailableBalance float64 `json:"available_balance"` // 可用余额 - UnrealizedPnL float64 `json:"unrealized_pnl"` // 未实现盈亏 - TotalPnL float64 `json:"total_pnl"` // 总盈亏 - TotalPnLPct float64 `json:"total_pnl_pct"` // 总盈亏百分比 - MarginUsed float64 `json:"margin_used"` // 已用保证金 - MarginUsedPct float64 `json:"margin_used_pct"` // 保证金使用率 - PositionCount int `json:"position_count"` // 持仓数量 + TotalEquity float64 `json:"total_equity"` // Account equity + AvailableBalance float64 `json:"available_balance"` // Available balance + UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized profit/loss + TotalPnL float64 `json:"total_pnl"` // Total profit/loss + TotalPnLPct float64 `json:"total_pnl_pct"` // Total profit/loss percentage + MarginUsed float64 `json:"margin_used"` // Used margin + MarginUsedPct float64 `json:"margin_used_pct"` // Margin usage rate + PositionCount int `json:"position_count"` // Number of positions } -// CandidateCoin 候选币种(来自币种池) +// CandidateCoin candidate coin (from coin pool) type CandidateCoin struct { Symbol string `json:"symbol"` - Sources []string `json:"sources"` // 来源: "ai500" 和/或 "oi_top" + Sources []string `json:"sources"` // Sources: "ai500" and/or "oi_top" } -// OITopData 持仓量增长Top数据(用于AI决策参考) +// OITopData open interest growth top data (for AI decision reference) type OITopData struct { - Rank int // OI Top排名 - OIDeltaPercent float64 // 持仓量变化百分比(1小时) - OIDeltaValue float64 // 持仓量变化价值 - PriceDeltaPercent float64 // 价格变化百分比 - NetLong float64 // 净多仓 - NetShort float64 // 净空仓 + Rank int // OI Top ranking + OIDeltaPercent float64 // Open interest change percentage (1 hour) + OIDeltaValue float64 // Open interest change value + PriceDeltaPercent float64 // Price change percentage + NetLong float64 // Net long positions + NetShort float64 // Net short positions } -// TradingStats 交易统计(用于AI输入) +// TradingStats trading statistics (for AI input) type TradingStats struct { - TotalTrades int `json:"total_trades"` // 总交易数(已平仓) - WinRate float64 `json:"win_rate"` // 胜率 (%) - ProfitFactor float64 `json:"profit_factor"` // 盈亏比 - SharpeRatio float64 `json:"sharpe_ratio"` // 夏普比 - TotalPnL float64 `json:"total_pnl"` // 总盈亏 - AvgWin float64 `json:"avg_win"` // 平均盈利 - AvgLoss float64 `json:"avg_loss"` // 平均亏损 - MaxDrawdownPct float64 `json:"max_drawdown_pct"` // 最大回撤 (%) + TotalTrades int `json:"total_trades"` // Total number of trades (closed) + WinRate float64 `json:"win_rate"` // Win rate (%) + ProfitFactor float64 `json:"profit_factor"` // Profit factor + SharpeRatio float64 `json:"sharpe_ratio"` // Sharpe ratio + TotalPnL float64 `json:"total_pnl"` // Total profit/loss + AvgWin float64 `json:"avg_win"` // Average win + AvgLoss float64 `json:"avg_loss"` // Average loss + MaxDrawdownPct float64 `json:"max_drawdown_pct"` // Maximum drawdown (%) } -// RecentOrder 最近完成的订单(用于AI输入) +// RecentOrder recently completed order (for AI input) type RecentOrder struct { - Symbol string `json:"symbol"` // 交易对 + Symbol string `json:"symbol"` // Trading pair Side string `json:"side"` // long/short - EntryPrice float64 `json:"entry_price"` // 开仓价 - ExitPrice float64 `json:"exit_price"` // 平仓价 - RealizedPnL float64 `json:"realized_pnl"` // 已实现盈亏 - PnLPct float64 `json:"pnl_pct"` // 盈亏百分比 - FilledAt string `json:"filled_at"` // 成交时间 + EntryPrice float64 `json:"entry_price"` // Entry price + ExitPrice float64 `json:"exit_price"` // Exit price + RealizedPnL float64 `json:"realized_pnl"` // Realized profit/loss + PnLPct float64 `json:"pnl_pct"` // Profit/loss percentage + FilledAt string `json:"filled_at"` // Fill time } -// Context 交易上下文(传递给AI的完整信息) +// Context trading context (complete information passed to AI) type Context struct { CurrentTime string `json:"current_time"` RuntimeMinutes int `json:"runtime_minutes"` @@ -104,71 +104,71 @@ type Context struct { Positions []PositionInfo `json:"positions"` CandidateCoins []CandidateCoin `json:"candidate_coins"` PromptVariant string `json:"prompt_variant,omitempty"` - TradingStats *TradingStats `json:"trading_stats,omitempty"` // 交易统计指标 - RecentOrders []RecentOrder `json:"recent_orders,omitempty"` // 最近完成的订单(10条) - MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用 + TradingStats *TradingStats `json:"trading_stats,omitempty"` // Trading statistics + RecentOrders []RecentOrder `json:"recent_orders,omitempty"` // Recently completed orders (10) + MarketDataMap map[string]*market.Data `json:"-"` // Not serialized, but used internally MultiTFMarket map[string]map[string]*market.Data `json:"-"` - OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射 - QuantDataMap map[string]*QuantData `json:"-"` // 量化数据映射(资金流向、持仓变化) - BTCETHLeverage int `json:"-"` // BTC/ETH杠杆倍数(从配置读取) - AltcoinLeverage int `json:"-"` // 山寨币杠杆倍数(从配置读取) + OITopDataMap map[string]*OITopData `json:"-"` // OI Top data mapping + QuantDataMap map[string]*QuantData `json:"-"` // Quantitative data mapping (fund flow, position changes) + BTCETHLeverage int `json:"-"` // BTC/ETH leverage multiplier (read from config) + AltcoinLeverage int `json:"-"` // Altcoin leverage multiplier (read from config) } -// Decision AI的交易决策 +// Decision AI trading decision type Decision struct { Symbol string `json:"symbol"` Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "hold", "wait" - // 开仓参数 + // Opening position parameters Leverage int `json:"leverage,omitempty"` PositionSizeUSD float64 `json:"position_size_usd,omitempty"` StopLoss float64 `json:"stop_loss,omitempty"` TakeProfit float64 `json:"take_profit,omitempty"` - // 通用参数 - Confidence int `json:"confidence,omitempty"` // 信心度 (0-100) - RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险 + // Common parameters + Confidence int `json:"confidence,omitempty"` // Confidence level (0-100) + RiskUSD float64 `json:"risk_usd,omitempty"` // Maximum USD risk Reasoning string `json:"reasoning"` } -// FullDecision AI的完整决策(包含思维链) +// FullDecision AI's complete decision (including chain of thought) type FullDecision struct { - SystemPrompt string `json:"system_prompt"` // 系统提示词(发送给AI的系统prompt) - UserPrompt string `json:"user_prompt"` // 发送给AI的输入prompt - CoTTrace string `json:"cot_trace"` // 思维链分析(AI输出) - Decisions []Decision `json:"decisions"` // 具体决策列表 + SystemPrompt string `json:"system_prompt"` // System prompt (system prompt sent to AI) + UserPrompt string `json:"user_prompt"` // Input prompt sent to AI + CoTTrace string `json:"cot_trace"` // Chain of thought analysis (AI output) + Decisions []Decision `json:"decisions"` // Specific decision list Timestamp time.Time `json:"timestamp"` - // AIRequestDurationMs 记录 AI API 调用耗时(毫秒)方便排查延迟问题 + // AIRequestDurationMs records AI API call duration (milliseconds) for troubleshooting latency issues AIRequestDurationMs int64 `json:"ai_request_duration_ms,omitempty"` } -// GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓) +// GetFullDecision gets AI's complete trading decision (batch analysis of all coins and positions) func GetFullDecision(ctx *Context, mcpClient mcp.AIClient) (*FullDecision, error) { return GetFullDecisionWithCustomPrompt(ctx, mcpClient, "", false, "") } -// GetFullDecisionWithStrategy 使用 StrategyEngine 获取AI决策(新版:策略驱动) -// 关键:使用策略配置的时间周期来获取市场数据,与 api/strategy.go 的测试运行逻辑保持一致 +// GetFullDecisionWithStrategy uses StrategyEngine to get AI decision (new version: strategy-driven) +// Key: uses strategy-configured timeframes to fetch market data, consistent with api/strategy.go test run logic func GetFullDecisionWithStrategy(ctx *Context, mcpClient mcp.AIClient, engine *StrategyEngine, variant string) (*FullDecision, error) { if ctx == nil { return nil, fmt.Errorf("context is nil") } if engine == nil { - // 如果没有策略引擎,回退到默认行为 + // If no strategy engine, fallback to default behavior return GetFullDecisionWithCustomPrompt(ctx, mcpClient, "", false, "") } - // 1. 使用策略配置获取市场数据(关键:使用多时间周期) + // 1. Fetch market data using strategy config (key: use multiple timeframes) if len(ctx.MarketDataMap) == 0 { if err := fetchMarketDataWithStrategy(ctx, engine); err != nil { - return nil, fmt.Errorf("获取市场数据失败: %w", err) + return nil, fmt.Errorf("failed to fetch market data: %w", err) } } - // 确保 OITopDataMap 已初始化 + // Ensure OITopDataMap is initialized if ctx.OITopDataMap == nil { ctx.OITopDataMap = make(map[string]*OITopData) - // 加载 OI Top 数据 + // Load OI Top data oiPositions, err := pool.GetOITopPositions() if err == nil { for _, pos := range oiPositions { @@ -184,22 +184,22 @@ func GetFullDecisionWithStrategy(ctx *Context, mcpClient mcp.AIClient, engine *S } } - // 2. 使用策略引擎构建 System Prompt + // 2. Build System Prompt using strategy engine riskConfig := engine.GetRiskControlConfig() systemPrompt := engine.BuildSystemPrompt(ctx.Account.TotalEquity, variant) - // 3. 使用策略引擎构建 User Prompt(包含多周期数据) + // 3. Build User Prompt using strategy engine (including multi-timeframe data) userPrompt := engine.BuildUserPrompt(ctx) - // 4. 调用AI API + // 4. Call AI API aiCallStart := time.Now() aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt) aiCallDuration := time.Since(aiCallStart) if err != nil { - return nil, fmt.Errorf("调用AI API失败: %w", err) + return nil, fmt.Errorf("AI API call failed: %w", err) } - // 5. 解析AI响应 + // 5. Parse AI response decision, err := parseFullDecisionResponse( aiResponse, ctx.Account.TotalEquity, @@ -215,24 +215,24 @@ func GetFullDecisionWithStrategy(ctx *Context, mcpClient mcp.AIClient, engine *S } if err != nil { - return decision, fmt.Errorf("解析AI响应失败: %w", err) + return decision, fmt.Errorf("failed to parse AI response: %w", err) } return decision, nil } -// fetchMarketDataWithStrategy 使用策略配置获取市场数据(多时间周期) -// 完全按照 api/strategy.go handleStrategyTestRun 的逻辑实现 +// fetchMarketDataWithStrategy fetches market data using strategy config (multiple timeframes) +// Fully implemented according to api/strategy.go handleStrategyTestRun logic func fetchMarketDataWithStrategy(ctx *Context, engine *StrategyEngine) error { config := engine.GetConfig() ctx.MarketDataMap = make(map[string]*market.Data) - // 获取时间周期配置(与 api/strategy.go 逻辑完全一致) + // Get timeframe configuration (fully consistent with api/strategy.go logic) timeframes := config.Indicators.Klines.SelectedTimeframes primaryTimeframe := config.Indicators.Klines.PrimaryTimeframe klineCount := config.Indicators.Klines.PrimaryCount - // 兼容旧配置 + // Compatible with old configuration if len(timeframes) == 0 { if primaryTimeframe != "" { timeframes = append(timeframes, primaryTimeframe) @@ -250,49 +250,49 @@ func fetchMarketDataWithStrategy(ctx *Context, engine *StrategyEngine) error { klineCount = 30 } - logger.Infof("📊 策略时间周期: %v, 主周期: %s, K线数量: %d", timeframes, primaryTimeframe, klineCount) + logger.Infof("📊 Strategy timeframes: %v, Primary: %s, Kline count: %d", timeframes, primaryTimeframe, klineCount) - // 1. 先获取持仓币种的数据(必须获取) + // 1. First fetch data for position coins (must fetch) for _, pos := range ctx.Positions { data, err := market.GetWithTimeframes(pos.Symbol, timeframes, primaryTimeframe, klineCount) if err != nil { - logger.Infof("⚠️ 获取持仓 %s 市场数据失败: %v", pos.Symbol, err) + logger.Infof("⚠️ Failed to fetch market data for position %s: %v", pos.Symbol, err) continue } ctx.MarketDataMap[pos.Symbol] = data } - // 2. 获取所有候选币种的数据(与 api/strategy.go 完全一致,不做数量限制) - // 持仓币种集合(用于判断是否跳过OI检查) + // 2. Fetch data for all candidate coins (fully consistent with api/strategy.go, no quantity limit) + // Position coin set (used to determine whether to skip OI check) positionSymbols := make(map[string]bool) for _, pos := range ctx.Positions { positionSymbols[pos.Symbol] = true } - // OI 流动性过滤阈值(百万美元) - const minOIThresholdMillions = 15.0 // 15M USD 最小持仓价值 + // OI liquidity filter threshold (million USD) + const minOIThresholdMillions = 15.0 // 15M USD minimum open interest value for _, coin := range ctx.CandidateCoins { - // 跳过已获取的持仓币种 + // Skip already fetched position coins if _, exists := ctx.MarketDataMap[coin.Symbol]; exists { continue } data, err := market.GetWithTimeframes(coin.Symbol, timeframes, primaryTimeframe, klineCount) if err != nil { - logger.Infof("⚠️ 获取 %s 市场数据失败: %v", coin.Symbol, err) + logger.Infof("⚠️ Failed to fetch market data for %s: %v", coin.Symbol, err) continue } - // ⚠️ 流动性过滤:持仓价值低于阈值的币种不做(多空都不做) - // 但现有持仓必须保留(需要决策是否平仓) + // Liquidity filter: skip coins with OI value below threshold (both long and short) + // But existing positions must be retained (need to decide whether to close) isExistingPosition := positionSymbols[coin.Symbol] if !isExistingPosition && data.OpenInterest != nil && data.CurrentPrice > 0 { - // 计算持仓价值(USD)= 持仓量 × 当前价格 + // Calculate OI value (USD) = OI quantity × current price oiValue := data.OpenInterest.Latest * data.CurrentPrice - oiValueInMillions := oiValue / 1_000_000 // 转换为百万美元单位 + oiValueInMillions := oiValue / 1_000_000 // Convert to million USD if oiValueInMillions < minOIThresholdMillions { - logger.Infof("⚠️ %s 持仓价值过低(%.2fM USD < %.1fM),跳过此币种 [持仓量:%.0f × 价格:%.4f]", + logger.Infof("⚠️ %s OI value too low (%.2fM USD < %.1fM), skipping coin [OI:%.0f × Price:%.4f]", coin.Symbol, oiValueInMillions, minOIThresholdMillions, data.OpenInterest.Latest, data.CurrentPrice) continue } @@ -301,27 +301,27 @@ func fetchMarketDataWithStrategy(ctx *Context, engine *StrategyEngine) error { ctx.MarketDataMap[coin.Symbol] = data } - logger.Infof("📊 成功获取 %d 个币种的多时间周期市场数据(已过滤低流动性币种)", len(ctx.MarketDataMap)) + logger.Infof("📊 Successfully fetched multi-timeframe market data for %d coins (low liquidity coins filtered)", len(ctx.MarketDataMap)) return nil } -// GetFullDecisionWithCustomPrompt 获取AI的完整交易决策(支持自定义prompt和模板选择) +// GetFullDecisionWithCustomPrompt gets AI's complete trading decision (supports custom prompt and template selection) func GetFullDecisionWithCustomPrompt(ctx *Context, mcpClient mcp.AIClient, customPrompt string, overrideBase bool, templateName string) (*FullDecision, error) { if ctx == nil { return nil, fmt.Errorf("context is nil") } - // 1. 为所有币种获取市场数据(若上层已提供,则无需重复拉取) + // 1. Fetch market data for all coins (if already provided by upper layer, no need to re-fetch) if len(ctx.MarketDataMap) == 0 { if err := fetchMarketDataForContext(ctx); err != nil { - return nil, fmt.Errorf("获取市场数据失败: %w", err) + return nil, fmt.Errorf("failed to fetch market data: %w", err) } } else if ctx.OITopDataMap == nil { - // 确保 OI 数据映射已初始化,避免后续访问空指针 + // Ensure OI data mapping is initialized to avoid null pointer access later ctx.OITopDataMap = make(map[string]*OITopData) } - // 2. 构建 System Prompt(固定规则)和 User Prompt(动态数据) + // 2. Build System Prompt (fixed rules) and User Prompt (dynamic data) systemPrompt := buildSystemPromptWithCustom( ctx.Account.TotalEquity, ctx.BTCETHLeverage, @@ -333,49 +333,49 @@ func GetFullDecisionWithCustomPrompt(ctx *Context, mcpClient mcp.AIClient, custo ) userPrompt := buildUserPrompt(ctx) - // 3. 调用AI API(使用 system + user prompt) + // 3. Call AI API (using system + user prompt) aiCallStart := time.Now() aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt) aiCallDuration := time.Since(aiCallStart) if err != nil { - return nil, fmt.Errorf("调用AI API失败: %w", err) + return nil, fmt.Errorf("AI API call failed: %w", err) } - // 4. 解析AI响应 + // 4. Parse AI response decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage) - // 无论是否有错误,都要保存 SystemPrompt 和 UserPrompt(用于调试和决策未执行后的问题定位) + // Save SystemPrompt and UserPrompt regardless of error (for debugging and troubleshooting unexecuted decisions) if decision != nil { decision.Timestamp = time.Now() - decision.SystemPrompt = systemPrompt // 保存系统prompt - decision.UserPrompt = userPrompt // 保存输入prompt + decision.SystemPrompt = systemPrompt // Save system prompt + decision.UserPrompt = userPrompt // Save input prompt decision.AIRequestDurationMs = aiCallDuration.Milliseconds() } if err != nil { - return decision, fmt.Errorf("解析AI响应失败: %w", err) + return decision, fmt.Errorf("failed to parse AI response: %w", err) } decision.Timestamp = time.Now() - decision.SystemPrompt = systemPrompt // 保存系统prompt - decision.UserPrompt = userPrompt // 保存输入prompt + decision.SystemPrompt = systemPrompt // Save system prompt + decision.UserPrompt = userPrompt // Save input prompt return decision, nil } -// fetchMarketDataForContext 为上下文中的所有币种获取市场数据和OI数据 +// fetchMarketDataForContext fetches market data and OI data for all coins in context func fetchMarketDataForContext(ctx *Context) error { ctx.MarketDataMap = make(map[string]*market.Data) ctx.OITopDataMap = make(map[string]*OITopData) - // 收集所有需要获取数据的币种 + // Collect all symbols that need data symbolSet := make(map[string]bool) - // 1. 优先获取持仓币种的数据(这是必须的) + // 1. Prioritize fetching position coin data (this is required) for _, pos := range ctx.Positions { symbolSet[pos.Symbol] = true } - // 2. 候选币种数量根据账户状态动态调整 + // 2. Candidate coin count dynamically adjusted based on account status maxCandidates := calculateMaxCandidates(ctx) for i, coin := range ctx.CandidateCoins { if i >= maxCandidates { @@ -384,8 +384,8 @@ func fetchMarketDataForContext(ctx *Context) error { symbolSet[coin.Symbol] = true } - // 并发获取市场数据 - // 持仓币种集合(用于判断是否跳过OI检查) + // Fetch market data concurrently + // Position coin set (used to determine whether to skip OI check) positionSymbols := make(map[string]bool) for _, pos := range ctx.Positions { positionSymbols[pos.Symbol] = true @@ -394,23 +394,23 @@ func fetchMarketDataForContext(ctx *Context) error { for symbol := range symbolSet { data, err := market.Get(symbol) if err != nil { - // 单个币种失败不影响整体,只记录错误 + // Single coin failure doesn't affect overall, just log error continue } - // ⚠️ 流动性过滤:持仓价值低于阈值的币种不做(多空都不做) - // 持仓价值 = 持仓量 × 当前价格 - // 但现有持仓必须保留(需要决策是否平仓) - // 💡 OI 門檻配置:用戶可根據風險偏好調整 - const minOIThresholdMillions = 15.0 // 可調整:15M(保守) / 10M(平衡) / 8M(寬鬆) / 5M(激進) + // Liquidity filter: skip coins with OI value below threshold (both long and short) + // OI value = OI quantity × current price + // But existing positions must be retained (need to decide whether to close) + // OI threshold configuration: users can adjust based on risk preference + const minOIThresholdMillions = 15.0 // Adjustable: 15M(conservative) / 10M(balanced) / 8M(loose) / 5M(aggressive) isExistingPosition := positionSymbols[symbol] if !isExistingPosition && data.OpenInterest != nil && data.CurrentPrice > 0 { - // 计算持仓价值(USD)= 持仓量 × 当前价格 + // Calculate OI value (USD) = OI quantity × current price oiValue := data.OpenInterest.Latest * data.CurrentPrice - oiValueInMillions := oiValue / 1_000_000 // 转换为百万美元单位 + oiValueInMillions := oiValue / 1_000_000 // Convert to million USD if oiValueInMillions < minOIThresholdMillions { - logger.Infof("⚠️ %s 持仓价值过低(%.2fM USD < %.1fM),跳过此币种 [持仓量:%.0f × 价格:%.4f]", + logger.Infof("⚠️ %s OI value too low (%.2fM USD < %.1fM), skipping coin [OI:%.0f × Price:%.4f]", symbol, oiValueInMillions, minOIThresholdMillions, data.OpenInterest.Latest, data.CurrentPrice) continue } @@ -419,11 +419,11 @@ func fetchMarketDataForContext(ctx *Context) error { ctx.MarketDataMap[symbol] = data } - // 加载OI Top数据(不影响主流程) + // Load OI Top data (doesn't affect main flow) oiPositions, err := pool.GetOITopPositions() if err == nil { for _, pos := range oiPositions { - // 标准化符号匹配 + // Normalize symbol matching symbol := pos.Symbol ctx.OITopDataMap[symbol] = &OITopData{ Rank: pos.Rank, @@ -439,15 +439,15 @@ func fetchMarketDataForContext(ctx *Context) error { return nil } -// calculateMaxCandidates 根据账户状态计算需要分析的候选币种数量 +// calculateMaxCandidates calculates the number of candidate coins to analyze based on account status func calculateMaxCandidates(ctx *Context) int { - // ⚠️ 重要:限制候选币种数量,避免 Prompt 过大 - // 根据持仓数量动态调整:持仓越少,可以分析更多候选币 + // Important: limit candidate coin count to avoid prompt being too large + // Dynamically adjust based on position count: fewer positions allow analyzing more candidates const ( - maxCandidatesWhenEmpty = 30 // 无持仓时最多分析30个候选币 - maxCandidatesWhenHolding1 = 25 // 持仓1个时最多分析25个候选币 - maxCandidatesWhenHolding2 = 20 // 持仓2个时最多分析20个候选币 - maxCandidatesWhenHolding3 = 15 // 持仓3个时最多分析15个候选币(避免 Prompt 过大) + maxCandidatesWhenEmpty = 30 // Max 30 candidates when no positions + maxCandidatesWhenHolding1 = 25 // Max 25 candidates when holding 1 position + maxCandidatesWhenHolding2 = 20 // Max 20 candidates when holding 2 positions + maxCandidatesWhenHolding3 = 15 // Max 15 candidates when holding 3 positions (avoid prompt being too large) ) positionCount := len(ctx.Positions) @@ -460,59 +460,59 @@ func calculateMaxCandidates(ctx *Context) int { maxCandidates = maxCandidatesWhenHolding1 case 2: maxCandidates = maxCandidatesWhenHolding2 - default: // 3+ 持仓 + default: // 3+ positions maxCandidates = maxCandidatesWhenHolding3 } - // 返回实际候选币数量和上限中的较小值 + // Return the smaller value between actual candidate count and max limit return min(len(ctx.CandidateCoins), maxCandidates) } -// buildSystemPromptWithCustom 构建包含自定义内容的 System Prompt +// buildSystemPromptWithCustom builds System Prompt with custom content func buildSystemPromptWithCustom(accountEquity float64, btcEthLeverage, altcoinLeverage int, customPrompt string, overrideBase bool, templateName string, variant string) string { - // 如果覆盖基础prompt且有自定义prompt,只使用自定义prompt + // If override base prompt and has custom prompt, only use custom prompt if overrideBase && customPrompt != "" { return customPrompt } - // 获取基础prompt(使用指定的模板) + // Get base prompt (using specified template) basePrompt := buildSystemPrompt(accountEquity, btcEthLeverage, altcoinLeverage, templateName, variant) - // 如果没有自定义prompt,直接返回基础prompt + // If no custom prompt, directly return base prompt if customPrompt == "" { return basePrompt } - // 添加自定义prompt部分到基础prompt + // Add custom prompt section to base prompt var sb strings.Builder sb.WriteString(basePrompt) sb.WriteString("\n\n") - sb.WriteString("# 📌 个性化交易策略\n\n") + sb.WriteString("# 📌 Personalized Trading Strategy\n\n") sb.WriteString(customPrompt) sb.WriteString("\n\n") - sb.WriteString("注意: 以上个性化策略是对基础规则的补充,不能违背基础风险控制原则。\n") + sb.WriteString("Note: The above personalized strategy is a supplement to basic rules and cannot violate basic risk control principles.\n") return sb.String() } -// buildSystemPrompt 构建 System Prompt(使用模板+动态部分) +// buildSystemPrompt builds System Prompt (using template + dynamic parts) func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage int, templateName string, variant string) string { var sb strings.Builder - // 1. 加载提示词模板(核心交易策略部分) + // 1. Load prompt template (core trading strategy part) if templateName == "" { - templateName = "default" // 默认使用 default 模板 + templateName = "default" // Default to using default template } template, err := GetPromptTemplate(templateName) if err != nil { - // 如果模板不存在,记录错误并使用 default - logger.Infof("⚠️ 提示词模板 '%s' 不存在,使用 default: %v", templateName, err) + // If template doesn't exist, log error and use default + logger.Infof("⚠️ Prompt template '%s' doesn't exist, using default: %v", templateName, err) template, err = GetPromptTemplate("default") if err != nil { - // 如果连 default 都不存在,使用内置的简化版本 - logger.Infof("❌ 无法加载任何提示词模板,使用内置简化版本") - sb.WriteString("你是专业的加密货币交易AI。请根据市场数据做出交易决策。\n\n") + // If even default doesn't exist, use built-in simplified version + logger.Infof("❌ Cannot load any prompt template, using built-in simplified version") + sb.WriteString("You are a professional cryptocurrency trading AI. Please make trading decisions based on market data.\n\n") } else { sb.WriteString(template.Content) sb.WriteString("\n\n") @@ -522,87 +522,87 @@ func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage in sb.WriteString("\n\n") } - // 2. 交易模式变体 + // 2. Trading mode variants 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 profit/loss 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 holding cash, 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, target smaller profits but require swift 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("1. 风险回报比: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n") - sb.WriteString("2. 最多持仓: 3个币种(质量>数量)\n") - sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U | BTC/ETH %.0f-%.0f U\n", + // 3. Hard constraints (risk control) + sb.WriteString("# Hard Constraints (Risk Control)\n\n") + sb.WriteString("1. Risk/reward ratio: Must be ≥ 1:3 (risk 1% to earn 3%+ profit)\n") + sb.WriteString("2. Max positions: 3 coins (quality > quantity)\n") + sb.WriteString(fmt.Sprintf("3. Single coin position: Altcoins %.0f-%.0f U | BTC/ETH %.0f-%.0f U\n", accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10)) - sb.WriteString(fmt.Sprintf("4. 杠杆限制: **山寨币最大%dx杠杆** | **BTC/ETH最大%dx杠杆**\n", altcoinLeverage, btcEthLeverage)) - sb.WriteString("5. 保证金使用率 ≤ 90%\n") - sb.WriteString("6. 开仓金额: 建议 ≥12 USDT(交易所最小名义价值10 USDT + 安全边际)\n\n") + sb.WriteString(fmt.Sprintf("4. Leverage limit: **Altcoins max %dx leverage** | **BTC/ETH max %dx leverage**\n", altcoinLeverage, btcEthLeverage)) + sb.WriteString("5. Margin usage rate ≤ 90%%\n") + sb.WriteString("6. Opening amount: Recommended ≥12 USDT (exchange minimum notional value 10 USDT + safety margin)\n\n") - // 4. 交易频率与信号质量 - 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") + // 4. Trading frequency and signal quality + 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 holding time ≥30-60 minutes\n") + sb.WriteString("If you find yourself trading every period → standards too low; if closing position <30 minutes → too impatient.\n\n") - sb.WriteString("# 🎯 开仓标准(严格)\n\n") - sb.WriteString("只在多重信号共振时开仓。你拥有:\n") - sb.WriteString("- 3分钟价格序列 + 4小时K线序列\n") - sb.WriteString("- EMA20 / MACD / RSI7 / RSI14 等指标序列\n") - sb.WriteString("- 成交量、持仓量(OI)、资金费率等资金面序列\n") - sb.WriteString("- AI500 / OI_Top 筛选标签(若有)\n\n") - sb.WriteString("自由运用任何有效的分析方法,但**信心度 ≥75** 才能开仓;避免单一指标、信号矛盾、横盘震荡、刚平仓即重启等低质量行为。\n\n") + sb.WriteString("# 🎯 Opening Standards (Strict)\n\n") + sb.WriteString("Only open positions when multiple signals resonate. You have:\n") + sb.WriteString("- 3-minute price series + 4-hour K-line series\n") + sb.WriteString("- EMA20 / MACD / RSI7 / RSI14 indicator series\n") + sb.WriteString("- Volume, open interest (OI), funding rate and other fund flow series\n") + sb.WriteString("- AI500 / OI_Top screening tags (if any)\n\n") + sb.WriteString("Freely use any effective analysis method, but **confidence ≥75** required to open positions; avoid low-quality behaviors such as single indicators, contradictory signals, sideways consolidation, reopening immediately after closing, etc.\n\n") - // 5. 决策流程提示 - sb.WriteString("# 📋 决策流程\n\n") - sb.WriteString("1. 检查持仓 → 是否该止盈/止损\n") - sb.WriteString("2. 扫描候选币 + 多时间框 → 是否存在强信号\n") - sb.WriteString("3. 先写思维链,再输出结构化JSON\n\n") + // 5. Decision process tips + sb.WriteString("# 📋 Decision Process\n\n") + sb.WriteString("1. Check positions → Should take profit/stop loss?\n") + sb.WriteString("2. Scan candidate coins + multi-timeframe → Any strong signals?\n") + sb.WriteString("3. Write reasoning chain first, then output structured JSON\n\n") - // 7. 输出格式 - 动态生成 - sb.WriteString("# 输出格式 (严格遵守)\n\n") - sb.WriteString("**必须使用XML标签 标签分隔思维链和决策JSON,避免解析错误**\n\n") - sb.WriteString("## 格式要求\n\n") + // 7. Output format - dynamically generated + sb.WriteString("# Output Format (Strictly Follow)\n\n") + sb.WriteString("**Must use XML tags and to separate reasoning chain and decision JSON, avoid parsing errors**\n\n") + sb.WriteString("## Format Requirements\n\n") sb.WriteString("\n") - sb.WriteString("你的思维链分析...\n") - sb.WriteString("- 简洁分析你的思考过程 \n") + sb.WriteString("Your reasoning chain analysis...\n") + sb.WriteString("- Concisely analyze your thought process \n") sb.WriteString("\n\n") sb.WriteString("\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", btcEthLeverage, accountEquity*5)) sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\"}\n") sb.WriteString("]\n```\n") sb.WriteString("\n\n") - sb.WriteString("## 字段说明\n\n") + sb.WriteString("## Field Descriptions\n\n") sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n") - sb.WriteString("- `confidence`: 0-100(开仓建议≥75)\n") - sb.WriteString("- 开仓时必填: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n\n") + sb.WriteString("- `confidence`: 0-100 (opening recommended ≥75)\n") + sb.WriteString("- Required for opening: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n\n") return sb.String() } -// buildUserPrompt 构建 User Prompt(动态数据) +// buildUserPrompt builds User Prompt (dynamic data) func 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 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 + 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, @@ -610,66 +610,66 @@ func buildUserPrompt(ctx *Context) string { ctx.Account.MarginUsedPct, ctx.Account.PositionCount)) - // 持仓(完整市场数据) + // Positions (complete market data) if len(ctx.Positions) > 0 { - sb.WriteString("## 当前持仓\n") + sb.WriteString("## Current Positions\n") for i, pos := range ctx.Positions { - // 计算持仓时长 + // Calculate holding duration holdingDuration := "" if pos.UpdateTime > 0 { durationMs := time.Now().UnixMilli() - pos.UpdateTime - durationMin := durationMs / (1000 * 60) // 转换为分钟 + durationMin := durationMs / (1000 * 60) // Convert to minutes if durationMin < 60 { - holdingDuration = fmt.Sprintf(" | 持仓时长%d分钟", durationMin) + holdingDuration = fmt.Sprintf(" | Holding %d mins", durationMin) } else { durationHour := durationMin / 60 durationMinRemainder := durationMin % 60 - holdingDuration = fmt.Sprintf(" | 持仓时长%d小时%d分钟", durationHour, durationMinRemainder) + holdingDuration = fmt.Sprintf(" | Holding %dh %dm", durationHour, durationMinRemainder) } } - // 计算仓位价值 + // Calculate position value positionValue := math.Abs(pos.Quantity) * pos.MarkPrice - 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 | Value %.2f USDT | PnL %+.2f%% | PnL Amount %+.2f USDT | Peak PnL %.2f%% | Leverage %dx | Margin %.0f | Liq %.4f%s\n\n", i+1, 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)) - // 使用FormatMarketData输出完整市场数据 + // Use FormatMarketData to output complete market data if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok { sb.WriteString(market.Format(marketData)) sb.WriteString("\n") } } } else { - sb.WriteString("当前持仓: 无\n\n") + sb.WriteString("Current Positions: None\n\n") } - // 交易统计(如果有) + // Trading statistics (if any) 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 PnL: %.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 any) if len(ctx.RecentOrders) > 0 { - sb.WriteString("## 最近完成的交易\n") + sb.WriteString("## Recently 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, @@ -678,8 +678,8 @@ func buildUserPrompt(ctx *Context) string { sb.WriteString("\n") } - // 候选币种(完整市场数据) - sb.WriteString(fmt.Sprintf("## 候选币种 (%d个)\n\n", len(ctx.MarketDataMap))) + // Candidate coins (complete market data) + 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] @@ -690,12 +690,12 @@ func buildUserPrompt(ctx *Context) string { sourceTags := "" if len(coin.Sources) > 1 { - sourceTags = " (AI500+OI_Top双重信号)" + sourceTags = " (AI500+OI_Top dual signal)" } else if len(coin.Sources) == 1 && coin.Sources[0] == "oi_top" { - sourceTags = " (OI_Top持仓增长)" + sourceTags = " (OI_Top growing)" } - // 使用FormatMarketData输出完整市场数据 + // Use FormatMarketData to output complete market data sb.WriteString(fmt.Sprintf("### %d. %s%s\n\n", displayedCount, coin.Symbol, sourceTags)) sb.WriteString(market.Format(marketData)) sb.WriteString("\n") @@ -703,31 +703,31 @@ func buildUserPrompt(ctx *Context) string { sb.WriteString("\n") sb.WriteString("---\n\n") - sb.WriteString("现在请分析并输出决策(思维链 + JSON)\n") + sb.WriteString("Now please analyze and output decision (reasoning chain + JSON)\n") return sb.String() } -// parseFullDecisionResponse 解析AI的完整决策响应 +// parseFullDecisionResponse parses AI's complete decision response func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthLeverage, altcoinLeverage int) (*FullDecision, error) { - // 1. 提取思维链 + // 1. Extract chain of thought cotTrace := extractCoTTrace(aiResponse) - // 2. 提取JSON决策列表 + // 2. Extract JSON decision list decisions, err := extractDecisions(aiResponse) if err != nil { return &FullDecision{ CoTTrace: cotTrace, Decisions: []Decision{}, - }, fmt.Errorf("提取决策失败: %w", err) + }, fmt.Errorf("failed to extract decisions: %w", err) } - // 3. 验证决策 + // 3. Validate decisions if err := validateDecisions(decisions, accountEquity, btcEthLeverage, altcoinLeverage); err != nil { return &FullDecision{ CoTTrace: cotTrace, Decisions: decisions, - }, fmt.Errorf("决策验证失败: %w", err) + }, fmt.Errorf("decision validation failed: %w", err) } return &FullDecision{ @@ -736,174 +736,174 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthL }, nil } -// extractCoTTrace 提取思维链分析 +// extractCoTTrace extracts chain of thought analysis func extractCoTTrace(response string) string { - // 方法1: 优先尝试提取 标签内容 + // Method 1: Prioritize extracting tag content if match := reReasoningTag.FindStringSubmatch(response); match != nil && len(match) > 1 { - logger.Infof("✓ 使用 标签提取思维链") + logger.Infof("✓ Extracted reasoning chain using tag") return strings.TrimSpace(match[1]) } - // 方法2: 如果没有 标签,但有 标签,提取 之前的内容 + // Method 2: If no tag but has tag, extract content before if decisionIdx := strings.Index(response, ""); decisionIdx > 0 { - logger.Infof("✓ 提取 标签之前的内容作为思维链") + logger.Infof("✓ Extracted content before tag as reasoning chain") return strings.TrimSpace(response[:decisionIdx]) } - // 方法3: 后备方案 - 查找JSON数组的开始位置 + // Method 3: Fallback - find start position of JSON array jsonStart := strings.Index(response, "[") if jsonStart > 0 { - logger.Infof("⚠️ 使用旧版格式([ 字符分离)提取思维链") + logger.Infof("⚠️ Extracted reasoning chain using old format ([ character separator)") return strings.TrimSpace(response[:jsonStart]) } - // 如果找不到任何标记,整个响应都是思维链 + // If no markers found, entire response is reasoning chain return strings.TrimSpace(response) } -// extractDecisions 提取JSON决策列表 +// extractDecisions extracts JSON decision list func extractDecisions(response string) ([]Decision, error) { - // 预清洗:去零宽/BOM + // Pre-clean: remove zero-width/BOM s := removeInvisibleRunes(response) s = strings.TrimSpace(s) - // 🔧 关键修复 (Critical Fix):在正则匹配之前就先修复全角字符! - // 否则正则表达式 \[ 无法匹配全角的 [ + // Critical Fix: fix full-width characters before regex matching! + // Otherwise regex \[ cannot match full-width [ s = fixMissingQuotes(s) - // 方法1: 优先尝试从 标签中提取 + // Method 1: Prioritize extracting from tag var jsonPart string if match := reDecisionTag.FindStringSubmatch(s); match != nil && len(match) > 1 { jsonPart = strings.TrimSpace(match[1]) - logger.Infof("✓ 使用 标签提取JSON") + logger.Infof("✓ Extracted JSON using tag") } else { - // 后备方案:使用整个响应 + // Fallback: use entire response jsonPart = s - logger.Infof("⚠️ 未找到 标签,使用全文搜索JSON") + logger.Infof("⚠️ tag not found, searching JSON in full text") } - // 修复 jsonPart 中的全角字符 + // Fix full-width characters in jsonPart jsonPart = fixMissingQuotes(jsonPart) - // 1) 优先从 ```json 代码块中提取 + // 1) Prioritize extracting from ```json code block if m := reJSONFence.FindStringSubmatch(jsonPart); m != nil && len(m) > 1 { jsonContent := strings.TrimSpace(m[1]) - jsonContent = compactArrayOpen(jsonContent) // 把 "[ {" 规整为 "[{" - jsonContent = fixMissingQuotes(jsonContent) // 二次修复(防止 regex 提取后还有残留全角) + jsonContent = compactArrayOpen(jsonContent) // Normalize "[ {" to "[{" + jsonContent = fixMissingQuotes(jsonContent) // Second fix (prevent residual full-width after regex extraction) if err := validateJSONFormat(jsonContent); err != nil { - return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response) + return nil, fmt.Errorf("JSON format validation failed: %w\nJSON content: %s\nFull response:\n%s", err, jsonContent, response) } var decisions []Decision if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil { - return nil, fmt.Errorf("JSON解析失败: %w\nJSON内容: %s", err, jsonContent) + return nil, fmt.Errorf("JSON parsing failed: %w\nJSON content: %s", err, jsonContent) } return decisions, nil } - // 2) 退而求其次 (Fallback):全文寻找首个对象数组 - // 注意:此时 jsonPart 已经过 fixMissingQuotes(),全角字符已转换为半角 + // 2) Fallback: search for first object array in full text + // Note: at this point jsonPart has already been processed by fixMissingQuotes(), full-width converted to half-width jsonContent := strings.TrimSpace(reJSONArray.FindString(jsonPart)) if jsonContent == "" { - // 🔧 安全回退 (Safe Fallback):当AI只输出思维链没有JSON时,生成保底决策(避免系统崩溃) - logger.Infof("⚠️ [SafeFallback] AI未输出JSON决策,进入安全等待模式 (AI response without JSON, entering safe wait mode)") + // Safe Fallback: when AI only outputs reasoning without JSON, generate fallback decision (avoid system crash) + logger.Infof("⚠️ [SafeFallback] AI didn't output JSON decision, entering safe wait mode") - // 提取思维链摘要(最多 240 字符) + // Extract reasoning summary (max 240 characters) cotSummary := jsonPart if len(cotSummary) > 240 { cotSummary = cotSummary[:240] + "..." } - // 生成保底决策:所有币种进入 wait 状态 + // Generate fallback decision: all coins enter wait state fallbackDecision := Decision{ Symbol: "ALL", Action: "wait", - Reasoning: fmt.Sprintf("模型未输出结构化JSON决策,进入安全等待;摘要:%s", cotSummary), + Reasoning: fmt.Sprintf("Model didn't output structured JSON decision, entering safe wait; summary: %s", cotSummary), } return []Decision{fallbackDecision}, nil } - // 🔧 规整格式(此时全角字符已在前面修复过) + // Normalize format (full-width characters already fixed earlier) jsonContent = compactArrayOpen(jsonContent) - jsonContent = fixMissingQuotes(jsonContent) // 二次修复(防止 regex 提取后还有残留全角) + jsonContent = fixMissingQuotes(jsonContent) // Second fix (prevent residual full-width after regex extraction) - // 🔧 验证 JSON 格式(检测常见错误) + // Validate JSON format (detect common errors) if err := validateJSONFormat(jsonContent); err != nil { - return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response) + return nil, fmt.Errorf("JSON format validation failed: %w\nJSON content: %s\nFull response:\n%s", err, jsonContent, response) } - // 解析JSON + // Parse JSON var decisions []Decision if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil { - return nil, fmt.Errorf("JSON解析失败: %w\nJSON内容: %s", err, jsonContent) + return nil, fmt.Errorf("JSON parsing failed: %w\nJSON content: %s", err, jsonContent) } return decisions, nil } -// fixMissingQuotes 替换中文引号和全角字符为英文引号和半角字符(避免AI输出全角JSON字符导致解析失败) +// fixMissingQuotes replaces Chinese quotes and full-width characters with English quotes and half-width characters (avoid parsing failure due to AI outputting full-width JSON characters) func fixMissingQuotes(jsonStr string) string { - // 替换中文引号 + // Replace Chinese quotes jsonStr = strings.ReplaceAll(jsonStr, "\u201c", "\"") // " jsonStr = strings.ReplaceAll(jsonStr, "\u201d", "\"") // " jsonStr = strings.ReplaceAll(jsonStr, "\u2018", "'") // ' jsonStr = strings.ReplaceAll(jsonStr, "\u2019", "'") // ' - // ⚠️ 替换全角括号、冒号、逗号(防止AI输出全角JSON字符) - jsonStr = strings.ReplaceAll(jsonStr, "[", "[") // U+FF3B 全角左方括号 - jsonStr = strings.ReplaceAll(jsonStr, "]", "]") // U+FF3D 全角右方括号 - jsonStr = strings.ReplaceAll(jsonStr, "{", "{") // U+FF5B 全角左花括号 - jsonStr = strings.ReplaceAll(jsonStr, "}", "}") // U+FF5D 全角右花括号 - jsonStr = strings.ReplaceAll(jsonStr, ":", ":") // U+FF1A 全角冒号 - jsonStr = strings.ReplaceAll(jsonStr, ",", ",") // U+FF0C 全角逗号 + // Replace full-width brackets, colons, commas (prevent AI outputting full-width JSON characters) + jsonStr = strings.ReplaceAll(jsonStr, "[", "[") // U+FF3B full-width left square bracket + jsonStr = strings.ReplaceAll(jsonStr, "]", "]") // U+FF3D full-width right square bracket + jsonStr = strings.ReplaceAll(jsonStr, "{", "{") // U+FF5B full-width left curly bracket + jsonStr = strings.ReplaceAll(jsonStr, "}", "}") // U+FF5D full-width right curly bracket + jsonStr = strings.ReplaceAll(jsonStr, ":", ":") // U+FF1A full-width colon + jsonStr = strings.ReplaceAll(jsonStr, ",", ",") // U+FF0C full-width comma - // ⚠️ 替换CJK标点符号(AI在中文上下文中也可能输出这些) - jsonStr = strings.ReplaceAll(jsonStr, "【", "[") // CJK左方头括号 U+3010 - jsonStr = strings.ReplaceAll(jsonStr, "】", "]") // CJK右方头括号 U+3011 - jsonStr = strings.ReplaceAll(jsonStr, "〔", "[") // CJK左龟壳括号 U+3014 - jsonStr = strings.ReplaceAll(jsonStr, "〕", "]") // CJK右龟壳括号 U+3015 - jsonStr = strings.ReplaceAll(jsonStr, "、", ",") // CJK顿号 U+3001 + // Replace CJK punctuation (AI may also output these in Chinese context) + jsonStr = strings.ReplaceAll(jsonStr, "【", "[") // CJK left corner bracket U+3010 + jsonStr = strings.ReplaceAll(jsonStr, "】", "]") // CJK right corner bracket U+3011 + jsonStr = strings.ReplaceAll(jsonStr, "〔", "[") // CJK left tortoise shell bracket U+3014 + jsonStr = strings.ReplaceAll(jsonStr, "〕", "]") // CJK right tortoise shell bracket U+3015 + jsonStr = strings.ReplaceAll(jsonStr, "、", ",") // CJK ideographic comma U+3001 - // ⚠️ 替换全角空格为半角空格(JSON中不应该有全角空格) - jsonStr = strings.ReplaceAll(jsonStr, " ", " ") // U+3000 全角空格 + // Replace full-width space with half-width space (JSON shouldn't have full-width spaces) + jsonStr = strings.ReplaceAll(jsonStr, " ", " ") // U+3000 full-width space return jsonStr } -// validateJSONFormat 验证 JSON 格式,检测常见错误 +// validateJSONFormat validates JSON format, detecting common errors func validateJSONFormat(jsonStr string) error { trimmed := strings.TrimSpace(jsonStr) - // 允许 [ 和 { 之间存在任意空白(含零宽) + // Allow any whitespace (including zero-width) between [ and { if !reArrayHead.MatchString(trimmed) { - // 检查是否是纯数字/范围数组(常见错误) + // Check if it's a pure number/range array (common error) if strings.HasPrefix(trimmed, "[") && !strings.Contains(trimmed[:min(20, len(trimmed))], "{") { - return fmt.Errorf("不是有效的决策数组(必须包含对象 {}),实际内容: %s", trimmed[:min(50, len(trimmed))]) + return fmt.Errorf("not a valid decision array (must contain objects {}), actual content: %s", trimmed[:min(50, len(trimmed))]) } - return fmt.Errorf("JSON 必须以 [{ 开头(允许空白),实际: %s", trimmed[:min(20, len(trimmed))]) + return fmt.Errorf("JSON must start with [{ (whitespace allowed), actual: %s", trimmed[:min(20, len(trimmed))]) } - // 检查是否包含范围符号 ~(LLM 常见错误) + // Check if contains range symbol ~ (common LLM error) if strings.Contains(jsonStr, "~") { - return fmt.Errorf("JSON 中不可包含范围符号 ~,所有数字必须是精确的单一值") + return fmt.Errorf("JSON cannot contain range symbol ~, all numbers must be precise single values") } - // 检查是否包含千位分隔符(如 98,000) - // 使用简单的模式匹配:数字+逗号+3位数字 + // Check if contains thousand separators (like 98,000) + // Use simple pattern matching: digit + comma + 3 digits for i := 0; i < len(jsonStr)-4; i++ { if jsonStr[i] >= '0' && jsonStr[i] <= '9' && jsonStr[i+1] == ',' && jsonStr[i+2] >= '0' && jsonStr[i+2] <= '9' && jsonStr[i+3] >= '0' && jsonStr[i+3] <= '9' && jsonStr[i+4] >= '0' && jsonStr[i+4] <= '9' { - return fmt.Errorf("JSON 数字不可包含千位分隔符逗号,发现: %s", jsonStr[i:min(i+10, len(jsonStr))]) + return fmt.Errorf("JSON numbers cannot contain thousand separator comma, found: %s", jsonStr[i:min(i+10, len(jsonStr))]) } } return nil } -// min 返回两个整数中的较小值 +// min returns the smaller of two integers func min(a, b int) int { if a < b { return a @@ -911,27 +911,27 @@ func min(a, b int) int { return b } -// removeInvisibleRunes 去除零宽字符和 BOM,避免肉眼看不见的前缀破坏校验 +// removeInvisibleRunes removes zero-width characters and BOM, avoiding invisible prefixes breaking validation func removeInvisibleRunes(s string) string { return reInvisibleRunes.ReplaceAllString(s, "") } -// compactArrayOpen 规整开头的 "[ {" → "[{" +// compactArrayOpen normalizes opening "[ {" → "[{" func compactArrayOpen(s string) string { return reArrayOpenSpace.ReplaceAllString(strings.TrimSpace(s), "[{") } -// validateDecisions 验证所有决策(需要账户信息和杠杆配置) +// validateDecisions validates all decisions (requires account information and leverage configuration) func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { for i, decision := range decisions { if err := validateDecision(&decision, accountEquity, btcEthLeverage, altcoinLeverage); err != nil { - return fmt.Errorf("决策 #%d 验证失败: %w", i+1, err) + return fmt.Errorf("decision #%d validation failed: %w", i+1, err) } } return nil } -// findMatchingBracket 查找匹配的右括号 +// findMatchingBracket finds matching right bracket func findMatchingBracket(s string, start int) int { if start >= len(s) || s[start] != '[' { return -1 @@ -953,9 +953,9 @@ func findMatchingBracket(s string, start int) int { return -1 } -// validateDecision 验证单个决策的有效性 +// validateDecision validates the validity of a single decision func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { - // 验证action + // Validate action validActions := map[string]bool{ "open_long": true, "open_short": true, @@ -966,80 +966,80 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi } if !validActions[d.Action] { - return fmt.Errorf("无效的action: %s", d.Action) + return fmt.Errorf("invalid action: %s", d.Action) } - // 开仓操作必须提供完整参数 + // Opening operations must provide complete parameters if d.Action == "open_long" || d.Action == "open_short" { - // 根据币种使用配置的杠杆上限 - maxLeverage := altcoinLeverage // 山寨币使用配置的杠杆 - maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值 + // Use configured leverage limit based on coin type + maxLeverage := altcoinLeverage // Altcoins use configured leverage + maxPositionValue := accountEquity * 1.5 // Altcoins max 1.5x account equity if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { - maxLeverage = btcEthLeverage // BTC和ETH使用配置的杠杆 - maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值 + maxLeverage = btcEthLeverage // BTC and ETH use configured leverage + maxPositionValue = accountEquity * 10 // BTC/ETH max 10x account equity } - // ✅ Fallback 机制:杠杆超限时自动修正为上限值(而不是直接拒绝决策) + // Fallback mechanism: auto-correct leverage to limit when exceeded (instead of directly rejecting decision) if d.Leverage <= 0 { - return fmt.Errorf("杠杆必须大于0: %d", d.Leverage) + return fmt.Errorf("leverage must be greater than 0: %d", d.Leverage) } if d.Leverage > maxLeverage { - logger.Infof("⚠️ [Leverage Fallback] %s 杠杆超限 (%dx > %dx),自动调整为上限值 %dx", + logger.Infof("⚠️ [Leverage Fallback] %s leverage exceeded (%dx > %dx), auto-adjusting to limit %dx", d.Symbol, d.Leverage, maxLeverage, maxLeverage) - d.Leverage = maxLeverage // 自动修正为上限值 + d.Leverage = maxLeverage // Auto-correct to limit value } if d.PositionSizeUSD <= 0 { - return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD) + return fmt.Errorf("position size must be greater than 0: %.2f", d.PositionSizeUSD) } - // ✅ 验证最小开仓金额(防止数量格式化为 0 的错误) - // Binance 最小名义价值 10 USDT + 安全边际 - const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际 - const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活) + // Validate minimum opening amount (prevent quantity rounding to 0 error) + // Binance minimum notional value 10 USDT + safety margin + const minPositionSizeGeneral = 12.0 // 10 + 20% safety margin + const minPositionSizeBTCETH = 60.0 // BTC/ETH requires larger amount due to high price and precision limits (more flexible) if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { if d.PositionSizeUSD < minPositionSizeBTCETH { - return fmt.Errorf("%s 开仓金额过小(%.2f USDT),必须≥%.2f USDT(因价格高且精度限制,避免数量四舍五入为0)", d.Symbol, d.PositionSizeUSD, minPositionSizeBTCETH) + return fmt.Errorf("%s opening amount too small (%.2f USDT), must be ≥%.2f USDT (due to high price and precision limits, avoid quantity rounding to 0)", d.Symbol, d.PositionSizeUSD, minPositionSizeBTCETH) } } else { if d.PositionSizeUSD < minPositionSizeGeneral { - return fmt.Errorf("开仓金额过小(%.2f USDT),必须≥%.2f USDT(Binance 最小名义价值要求)", d.PositionSizeUSD, minPositionSizeGeneral) + return fmt.Errorf("opening amount too small (%.2f USDT), must be ≥%.2f USDT (Binance minimum notional value requirement)", d.PositionSizeUSD, minPositionSizeGeneral) } } - // 验证仓位价值上限(加1%容差以避免浮点数精度问题) - tolerance := maxPositionValue * 0.01 // 1%容差 + // Validate position value limit (add 1% tolerance to avoid floating point precision issues) + tolerance := maxPositionValue * 0.01 // 1% tolerance if d.PositionSizeUSD > maxPositionValue+tolerance { if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { - return fmt.Errorf("BTC/ETH单币种仓位价值不能超过%.0f USDT(10倍账户净值),实际: %.0f", maxPositionValue, d.PositionSizeUSD) + return fmt.Errorf("BTC/ETH single coin position value cannot exceed %.0f USDT (10x account equity), actual: %.0f", maxPositionValue, d.PositionSizeUSD) } else { - return fmt.Errorf("山寨币单币种仓位价值不能超过%.0f USDT(1.5倍账户净值),实际: %.0f", maxPositionValue, d.PositionSizeUSD) + return fmt.Errorf("altcoin single coin position value cannot exceed %.0f USDT (1.5x account equity), actual: %.0f", maxPositionValue, d.PositionSizeUSD) } } if d.StopLoss <= 0 || d.TakeProfit <= 0 { - return fmt.Errorf("止损和止盈必须大于0") + return fmt.Errorf("stop loss and take profit must be greater than 0") } - // 验证止损止盈的合理性 + // Validate rationality of stop loss and take profit if d.Action == "open_long" { if d.StopLoss >= d.TakeProfit { - return fmt.Errorf("做多时止损价必须小于止盈价") + return fmt.Errorf("for long positions, stop loss price must be less than take profit price") } } else { if d.StopLoss <= d.TakeProfit { - return fmt.Errorf("做空时止损价必须大于止盈价") + return fmt.Errorf("for short positions, stop loss price must be greater than take profit price") } } - // 验证风险回报比(必须≥1:3) - // 计算入场价(假设当前市价) + // Validate risk/reward ratio (must be ≥1:3) + // Calculate entry price (assume current market price) var entryPrice float64 if d.Action == "open_long" { - // 做多:入场价在止损和止盈之间 - entryPrice = d.StopLoss + (d.TakeProfit-d.StopLoss)*0.2 // 假设在20%位置入场 + // Long: entry price between stop loss and take profit + entryPrice = d.StopLoss + (d.TakeProfit-d.StopLoss)*0.2 // Assume entry at 20% position } else { - // 做空:入场价在止损和止盈之间 - entryPrice = d.StopLoss - (d.StopLoss-d.TakeProfit)*0.2 // 假设在20%位置入场 + // Short: entry price between stop loss and take profit + entryPrice = d.StopLoss - (d.StopLoss-d.TakeProfit)*0.2 // Assume entry at 20% position } var riskPercent, rewardPercent, riskRewardRatio float64 @@ -1057,9 +1057,9 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi } } - // 硬约束:风险回报比必须≥3.0 + // Hard constraint: risk/reward ratio must be ≥3.0 if riskRewardRatio < 3.0 { - return fmt.Errorf("风险回报比过低(%.2f:1),必须≥3.0:1 [风险:%.2f%% 收益:%.2f%%] [止损:%.2f 止盈:%.2f]", + return fmt.Errorf("risk/reward ratio too low (%.2f:1), must be ≥3.0:1 [risk: %.2f%% reward: %.2f%%] [stop loss: %.2f take profit: %.2f]", riskRewardRatio, riskPercent, rewardPercent, d.StopLoss, d.TakeProfit) } } diff --git a/decision/prompt_manager.go b/decision/prompt_manager.go index 3bece5ea..202d2291 100644 --- a/decision/prompt_manager.go +++ b/decision/prompt_manager.go @@ -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) } diff --git a/decision/prompt_manager_test.go b/decision/prompt_manager_test.go index ea57cabd..720d766d 100644 --- a/decision/prompt_manager_test.go +++ b/decision/prompt_manager_test.go @@ -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) } } diff --git a/decision/prompt_reload_integration_test.go b/decision/prompt_reload_integration_test.go index c7310c07..8139ddd1 100644 --- a/decision/prompt_reload_integration_test.go +++ b/decision/prompt_reload_integration_test.go @@ -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") } diff --git a/decision/prompt_test.go b/decision/prompt_test.go index 69bec67f..4890f7ea 100644 --- a/decision/prompt_test.go +++ b/decision/prompt_test.go @@ -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) } } } diff --git a/decision/strategy_engine.go b/decision/strategy_engine.go index cf29fd76..be986eca 100644 --- a/decision/strategy_engine.go +++ b/decision/strategy_engine.go @@ -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标签 标签分隔思维链和决策JSON,避免解析错误**\n\n") - sb.WriteString("## 格式要求\n\n") + // 7. Output format + sb.WriteString("# Output Format (Strictly Follow)\n\n") + sb.WriteString("**Must use XML tags and to separate chain of thought and decision JSON, avoiding parsing errors**\n\n") + sb.WriteString("## Format Requirements\n\n") sb.WriteString("\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("\n\n") sb.WriteString("\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("\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 } diff --git a/decision/validate_test.go b/decision/validate_test.go index 468f9778..3f2da737 100644 --- a/decision/validate_test.go +++ b/decision/validate_test.go @@ -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))) diff --git a/hook/http_client_hook.go b/hook/http_client_hook.go index 5540b23a..1cd4b555 100644 --- a/hook/http_client_hook.go +++ b/hook/http_client_hook.go @@ -12,7 +12,7 @@ type SetHttpClientResult struct { func (r *SetHttpClientResult) Error() error { if r.Err != nil { - log.Printf("⚠️ 执行NewAsterTraderResult时出错: %v", r.Err) + log.Printf("⚠️ Error executing SetHttpClientResult: %v", r.Err) } return r.Err } diff --git a/hook/ip_hook.go b/hook/ip_hook.go index 9ad597d3..d6f7f943 100644 --- a/hook/ip_hook.go +++ b/hook/ip_hook.go @@ -13,7 +13,7 @@ func (r *IpResult) Error() error { func (r *IpResult) GetResult() string { if r.Err != nil { - log.Printf("⚠️ 执行GetIP时出错: %v", r.Err) + log.Printf("⚠️ Error executing GetIP: %v", r.Err) } return r.IP } diff --git a/hook/trader_hook.go b/hook/trader_hook.go index cbd7a1f1..a24d30ef 100644 --- a/hook/trader_hook.go +++ b/hook/trader_hook.go @@ -14,7 +14,7 @@ type NewBinanceTraderResult struct { func (r *NewBinanceTraderResult) Error() error { if r.Err != nil { - log.Printf("⚠️ 执行NewBinanceTraderResult时出错: %v", r.Err) + log.Printf("⚠️ Error executing NewBinanceTraderResult: %v", r.Err) } return r.Err } @@ -31,7 +31,7 @@ type NewAsterTraderResult struct { func (r *NewAsterTraderResult) Error() error { if r.Err != nil { - log.Printf("⚠️ 执行NewAsterTraderResult时出错: %v", r.Err) + log.Printf("⚠️ Error executing NewAsterTraderResult: %v", r.Err) } return r.Err } diff --git a/logger/config.go b/logger/config.go index f18eb041..f9e659f0 100644 --- a/logger/config.go +++ b/logger/config.go @@ -1,11 +1,11 @@ package logger -// Config 日志配置(简化版) +// Config is the logger configuration (simplified version) type Config struct { - Level string `json:"level"` // 日志级别: debug, info, warn, error (默认: info) + Level string `json:"level"` // Log level: debug, info, warn, error (default: info) } -// SetDefaults 设置默认值 +// SetDefaults sets default values func (c *Config) SetDefaults() { if c.Level == "" { c.Level = "info" diff --git a/logger/logger.go b/logger/logger.go index fd5b87b7..2e52a74f 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -7,12 +7,12 @@ import ( ) var ( - // Log 全局logger实例 + // Log is the global logger instance Log *logrus.Logger ) func init() { - // 自动初始化默认 logger,确保在 Init 被调用前也能使用 + // Auto-initialize default logger to ensure it works before Init is called Log = logrus.New() Log.SetLevel(logrus.InfoLevel) Log.SetFormatter(&logrus.TextFormatter{ @@ -24,66 +24,66 @@ func init() { } // ============================================================================ -// 初始化函数 +// Initialization functions // ============================================================================ -// Init 初始化全局logger -// 如果config为nil,使用默认配置(console输出,info级别) +// Init initializes the global logger +// If config is nil, uses default configuration (console output, info level) func Init(cfg *Config) error { Log = logrus.New() - // 如果没有配置,使用默认值 + // Use default values if no config provided if cfg == nil { cfg = &Config{Level: "info"} } - // 设置默认值 + // Set default values cfg.SetDefaults() - // 设置日志级别 + // Set log level level, err := logrus.ParseLevel(cfg.Level) if err != nil { level = logrus.InfoLevel } Log.SetLevel(level) - // 设置格式化器(固定使用彩色文本格式) + // Set formatter (always use colored text format) Log.SetFormatter(&logrus.TextFormatter{ FullTimestamp: true, TimestampFormat: "2006-01-02 15:04:05", ForceColors: true, }) - // 设置输出目标(默认stdout) + // Set output target (default stdout) Log.SetOutput(os.Stdout) - // 启用调用位置信息 + // Enable caller location info Log.SetReportCaller(true) return nil } -// InitWithSimpleConfig 使用简化配置初始化logger -// 适用于只需要基本功能的场景 +// InitWithSimpleConfig initializes logger with simplified config +// Suitable for scenarios that only need basic functionality func InitWithSimpleConfig(level string) error { return Init(&Config{Level: level}) } -// Shutdown 优雅关闭logger +// Shutdown gracefully shuts down the logger func Shutdown() { - // 预留用于未来扩展 + // Reserved for future extensions } // ============================================================================ -// 日志记录函数 +// Logging functions // ============================================================================ -// WithFields 创建带字段的logger entry +// WithFields creates logger entry with fields func WithFields(fields logrus.Fields) *logrus.Entry { return Log.WithFields(fields) } -// WithField 创建带单个字段的logger entry +// WithField creates logger entry with a single field func WithField(key string, value interface{}) *logrus.Entry { return Log.WithField(key, value) } @@ -138,14 +138,14 @@ func Panicf(format string, args ...interface{}) { } // ============================================================================ -// MCP Logger 适配器 +// MCP Logger adapter // ============================================================================ -// MCPLogger 适配器,使 MCP 包使用全局 logger -// 实现 mcp.Logger 接口 +// MCPLogger adapter that allows MCP package to use the global logger +// Implements mcp.Logger interface type MCPLogger struct{} -// NewMCPLogger 创建 MCP 日志适配器 +// NewMCPLogger creates MCP log adapter func NewMCPLogger() *MCPLogger { return &MCPLogger{} } diff --git a/main.go b/main.go index 4809b4b0..620aea30 100644 --- a/main.go +++ b/main.go @@ -19,40 +19,40 @@ import ( ) func main() { - // 加载 .env 环境变量 + // Load .env environment variables _ = godotenv.Load() - // 初始化日志 + // Initialize logger logger.Init(nil) logger.Info("╔════════════════════════════════════════════════════════════╗") - logger.Info("║ 🤖 AI多模型交易系统 - 支持 DeepSeek & Qwen ║") + logger.Info("║ 🤖 AI Multi-Model Trading System - DeepSeek & Qwen ║") logger.Info("╚════════════════════════════════════════════════════════════╝") - // 初始化全局配置(从 .env 加载) + // Initialize global configuration (loaded from .env) config.Init() cfg := config.Get() - logger.Info("✅ 配置加载完成") + logger.Info("✅ Configuration loaded") - // 初始化数据库 + // Initialize database dbPath := "data.db" if len(os.Args) > 1 { dbPath = os.Args[1] } - logger.Infof("📋 初始化数据库: %s", dbPath) + logger.Infof("📋 Initializing database: %s", dbPath) st, err := store.New(dbPath) if err != nil { - logger.Fatalf("❌ 初始化数据库失败: %v", err) + logger.Fatalf("❌ Failed to initialize database: %v", err) } defer st.Close() backtest.UseDatabase(st.DB()) - // 初始化加密服务 - logger.Info("🔐 初始化加密服务...") + // Initialize encryption service + logger.Info("🔐 Initializing encryption service...") cryptoService, err := crypto.NewCryptoService() if err != nil { - logger.Fatalf("❌ 初始化加密服务失败: %v", err) + logger.Fatalf("❌ Failed to initialize encryption service: %v", err) } encryptFunc := func(plaintext string) string { if plaintext == "" { @@ -60,7 +60,7 @@ func main() { } encrypted, err := cryptoService.EncryptForStorage(plaintext) if err != nil { - logger.Warnf("⚠️ 加密失败: %v", err) + logger.Warnf("⚠️ Encryption failed: %v", err) return plaintext } return encrypted @@ -74,83 +74,83 @@ func main() { } decrypted, err := cryptoService.DecryptFromStorage(encrypted) if err != nil { - logger.Warnf("⚠️ 解密失败: %v", err) + logger.Warnf("⚠️ Decryption failed: %v", err) return encrypted } return decrypted } st.SetCryptoFuncs(encryptFunc, decryptFunc) - logger.Info("✅ 加密服务初始化成功") + logger.Info("✅ Encryption service initialized successfully") - // 设置 JWT 密钥 + // Set JWT secret auth.SetJWTSecret(cfg.JWTSecret) - logger.Info("🔑 JWT 密钥已设置") + logger.Info("🔑 JWT secret configured") - // 创建 TraderManager 与 BacktestManager + // Create TraderManager and BacktestManager traderManager := manager.NewTraderManager() mcpClient := newSharedMCPClient() backtestManager := backtest.NewManager(mcpClient) if err := backtestManager.RestoreRuns(); err != nil { - logger.Warnf("⚠️ 恢复历史回测失败: %v", err) + logger.Warnf("⚠️ Failed to restore backtest history: %v", err) } - // 从数据库加载所有交易员到内存 + // Load all traders from database to memory if err := traderManager.LoadTradersFromStore(st); err != nil { - logger.Fatalf("❌ 加载交易员失败: %v", err) + logger.Fatalf("❌ Failed to load traders: %v", err) } - // 显示加载的交易员信息 + // Display loaded trader information traders, err := st.Trader().List("default") if err != nil { - logger.Fatalf("❌ 获取交易员列表失败: %v", err) + logger.Fatalf("❌ Failed to get trader list: %v", err) } - logger.Info("🤖 数据库中的AI交易员配置:") + logger.Info("🤖 AI Trader Configurations in Database:") if len(traders) == 0 { - logger.Info(" (无交易员配置,请通过Web管理界面创建)") + logger.Info(" (No trader configurations, please create via Web interface)") } else { for _, t := range traders { - status := "❌ 已停止" + status := "❌ Stopped" if t.IsRunning { - status = "✅ 运行中" + status = "✅ Running" } - logger.Infof(" • %s [%s] %s - AI模型: %s, 交易所: %s", + logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s", t.Name, t.ID[:8], status, t.AIModelID, t.ExchangeID) } } - // 启动 WebSocket 行情监控(获取所有 USDT 永续合约的行情数据) + // Start WebSocket market monitor (get market data for all USDT perpetual contracts) go market.NewWSMonitor(150).Start(nil) - logger.Info("📊 WebSocket 行情监控已启动") + logger.Info("📊 WebSocket market monitor started") - // 启动API服务器 + // Start API server server := api.NewServer(traderManager, st, cryptoService, backtestManager, cfg.APIServerPort) go func() { if err := server.Start(); err != nil { - logger.Fatalf("❌ API服务器启动失败: %v", err) + logger.Fatalf("❌ Failed to start API server: %v", err) } }() - // 等待中断信号 + // Wait for interrupt signal quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - logger.Info("✅ 系统启动完成,等待交易指令...") - logger.Info("📌 提示: 使用 Ctrl+C 停止系统") + logger.Info("✅ System started successfully, waiting for trading commands...") + logger.Info("📌 Tip: Use Ctrl+C to stop the system") <-quit - logger.Info("📴 收到停止信号,正在关闭系统...") + logger.Info("📴 Shutdown signal received, closing system...") - // 停止所有交易员 + // Stop all traders traderManager.StopAll() - logger.Info("✅ 系统已安全关闭") + logger.Info("✅ System shut down safely") } -// newSharedMCPClient 创建共享的 MCP AI 客户端(用于回测) +// newSharedMCPClient creates a shared MCP AI client (for backtesting) func newSharedMCPClient() mcp.AIClient { apiKey := os.Getenv("DEEPSEEK_API_KEY") if apiKey == "" { - logger.Warn("⚠️ DEEPSEEK_API_KEY 未设置,AI 功能将不可用") + logger.Warn("⚠️ DEEPSEEK_API_KEY not set, AI features will be unavailable") return nil } return mcp.NewDeepSeekClient() diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 994fa389..60dd392c 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -11,21 +11,21 @@ import ( "time" ) -// CompetitionCache 竞赛数据缓存 +// CompetitionCache competition data cache type CompetitionCache struct { data map[string]interface{} timestamp time.Time mu sync.RWMutex } -// TraderManager 管理多个trader实例 +// TraderManager manages multiple trader instances type TraderManager struct { traders map[string]*trader.AutoTrader // key: trader ID competitionCache *CompetitionCache mu sync.RWMutex } -// NewTraderManager 创建trader管理器 +// NewTraderManager creates a trader manager func NewTraderManager() *TraderManager { return &TraderManager{ traders: make(map[string]*trader.AutoTrader), @@ -35,19 +35,19 @@ func NewTraderManager() *TraderManager { } } -// GetTrader 获取指定ID的trader +// GetTrader retrieves a trader by ID func (tm *TraderManager) GetTrader(id string) (*trader.AutoTrader, error) { tm.mu.RLock() defer tm.mu.RUnlock() t, exists := tm.traders[id] if !exists { - return nil, fmt.Errorf("trader ID '%s' 不存在", id) + return nil, fmt.Errorf("trader ID '%s' does not exist", id) } return t, nil } -// GetAllTraders 获取所有trader +// GetAllTraders retrieves all traders func (tm *TraderManager) GetAllTraders() map[string]*trader.AutoTrader { tm.mu.RLock() defer tm.mu.RUnlock() @@ -59,7 +59,7 @@ func (tm *TraderManager) GetAllTraders() map[string]*trader.AutoTrader { return result } -// GetTraderIDs 获取所有trader ID列表 +// GetTraderIDs retrieves all trader IDs func (tm *TraderManager) GetTraderIDs() []string { tm.mu.RLock() defer tm.mu.RUnlock() @@ -71,43 +71,43 @@ func (tm *TraderManager) GetTraderIDs() []string { return ids } -// StartAll 启动所有trader +// StartAll starts all traders func (tm *TraderManager) StartAll() { tm.mu.RLock() defer tm.mu.RUnlock() - logger.Info("🚀 启动所有Trader...") + logger.Info("🚀 Starting all traders...") for id, t := range tm.traders { go func(traderID string, at *trader.AutoTrader) { - logger.Infof("▶️ 启动 %s...", at.GetName()) + logger.Infof("▶️ Starting %s...", at.GetName()) if err := at.Run(); err != nil { - logger.Infof("❌ %s 运行错误: %v", at.GetName(), err) + logger.Infof("❌ %s runtime error: %v", at.GetName(), err) } }(id, t) } } -// StopAll 停止所有trader +// StopAll stops all traders func (tm *TraderManager) StopAll() { tm.mu.RLock() defer tm.mu.RUnlock() - logger.Info("⏹ 停止所有Trader...") + logger.Info("⏹ Stopping all traders...") for _, t := range tm.traders { t.Stop() } } -// AutoStartRunningTraders 自动启动数据库中标记为运行中的交易员 +// AutoStartRunningTraders automatically starts traders marked as running in the database func (tm *TraderManager) AutoStartRunningTraders(st *store.Store) { - // 先获取所有交易员配置(一次性查询) + // Get all trader configurations (single query) traderList, err := st.Trader().ListAll() if err != nil { - logger.Infof("⚠️ 获取交易员列表失败: %v", err) + logger.Infof("⚠️ Failed to get trader list: %v", err) return } - // 构建运行中交易员的ID集合 + // Build set of running trader IDs runningTraderIDs := make(map[string]bool) for _, traderCfg := range traderList { if traderCfg.IsRunning { @@ -116,7 +116,7 @@ func (tm *TraderManager) AutoStartRunningTraders(st *store.Store) { } if len(runningTraderIDs) == 0 { - logger.Info("📋 没有需要自动恢复的交易员") + logger.Info("📋 No traders to auto-restore") return } @@ -127,9 +127,9 @@ func (tm *TraderManager) AutoStartRunningTraders(st *store.Store) { for id, t := range tm.traders { if runningTraderIDs[id] { go func(traderID string, at *trader.AutoTrader) { - logger.Infof("▶️ 自动恢复启动 %s...", at.GetName()) + logger.Infof("▶️ Auto-restoring %s...", at.GetName()) if err := at.Run(); err != nil { - logger.Infof("❌ %s 运行错误: %v", at.GetName(), err) + logger.Infof("❌ %s runtime error: %v", at.GetName(), err) } }(id, t) startedCount++ @@ -137,11 +137,11 @@ func (tm *TraderManager) AutoStartRunningTraders(st *store.Store) { } if startedCount > 0 { - logger.Infof("✓ 自动恢复启动了 %d 个交易员", startedCount) + logger.Infof("✓ Auto-restored %d traders", startedCount) } } -// GetComparisonData 获取对比数据 +// GetComparisonData retrieves comparison data func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) { tm.mu.RLock() defer tm.mu.RUnlock() @@ -178,38 +178,38 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) { return comparison, nil } -// GetCompetitionData 获取竞赛数据(全平台所有交易员) +// GetCompetitionData retrieves competition data (all traders across platform) func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { - // 检查缓存是否有效(30秒内) + // Check if cache is valid (within 30 seconds) tm.competitionCache.mu.RLock() if time.Since(tm.competitionCache.timestamp) < 30*time.Second && len(tm.competitionCache.data) > 0 { - // 返回缓存数据 + // Return cached data cachedData := make(map[string]interface{}) for k, v := range tm.competitionCache.data { cachedData[k] = v } tm.competitionCache.mu.RUnlock() - logger.Infof("📋 返回竞赛数据缓存 (缓存时间: %.1fs)", time.Since(tm.competitionCache.timestamp).Seconds()) + logger.Infof("📋 Returning competition data cache (cache age: %.1fs)", time.Since(tm.competitionCache.timestamp).Seconds()) return cachedData, nil } tm.competitionCache.mu.RUnlock() tm.mu.RLock() - // 获取所有交易员列表 + // Get all trader list allTraders := make([]*trader.AutoTrader, 0, len(tm.traders)) for id, t := range tm.traders { allTraders = append(allTraders, t) - logger.Infof("📋 竞赛数据包含交易员: %s (%s)", t.GetName(), id) + logger.Infof("📋 Competition data includes trader: %s (%s)", t.GetName(), id) } tm.mu.RUnlock() - logger.Infof("🔄 重新获取竞赛数据,交易员数量: %d", len(allTraders)) + logger.Infof("🔄 Refreshing competition data, trader count: %d", len(allTraders)) - // 并发获取交易员数据 + // Concurrently fetch trader data traders := tm.getConcurrentTraderData(allTraders) - // 按收益率排序(降序) + // Sort by profit rate (descending) sort.Slice(traders, func(i, j int) bool { pnlPctI, okI := traders[i]["total_pnl_pct"].(float64) pnlPctJ, okJ := traders[j]["total_pnl_pct"].(float64) @@ -222,7 +222,7 @@ func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { return pnlPctI > pnlPctJ }) - // 限制返回前50名 + // Limit to top 50 totalCount := len(traders) limit := 50 if len(traders) > limit { @@ -232,9 +232,9 @@ func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { comparison := make(map[string]interface{}) comparison["traders"] = traders comparison["count"] = len(traders) - comparison["total_count"] = totalCount // 总交易员数量 + comparison["total_count"] = totalCount // Total number of traders - // 更新缓存 + // Update cache tm.competitionCache.mu.Lock() tm.competitionCache.data = comparison tm.competitionCache.timestamp = time.Now() @@ -243,24 +243,24 @@ func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { return comparison, nil } -// getConcurrentTraderData 并发获取多个交易员的数据 +// getConcurrentTraderData concurrently fetches data for multiple traders func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) []map[string]interface{} { type traderResult struct { index int data map[string]interface{} } - // 创建结果通道 + // Create result channel resultChan := make(chan traderResult, len(traders)) - // 并发获取每个交易员的数据 + // Concurrently fetch data for each trader for i, t := range traders { go func(index int, trader *trader.AutoTrader) { - // 设置单个交易员的超时时间为3秒 + // Set timeout to 3 seconds for single trader ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - // 使用通道来实现超时控制 + // Use channel for timeout control accountChan := make(chan map[string]interface{}, 1) errorChan := make(chan error, 1) @@ -278,7 +278,7 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ select { case account := <-accountChan: - // 成功获取账户信息 + // Successfully got account info traderData = map[string]interface{}{ "trader_id": trader.GetID(), "trader_name": trader.GetName(), @@ -293,8 +293,8 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ "system_prompt_template": trader.GetSystemPromptTemplate(), } case err := <-errorChan: - // 获取账户信息失败 - logger.Infof("⚠️ 获取交易员 %s 账户信息失败: %v", trader.GetID(), err) + // Failed to get account info + logger.Infof("⚠️ Failed to get account info for trader %s: %v", trader.GetID(), err) traderData = map[string]interface{}{ "trader_id": trader.GetID(), "trader_name": trader.GetName(), @@ -307,11 +307,11 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ "margin_used_pct": 0.0, "is_running": status["is_running"], "system_prompt_template": trader.GetSystemPromptTemplate(), - "error": "账户数据获取失败", + "error": "Failed to get account data", } case <-ctx.Done(): - // 超时 - logger.Infof("⏰ 获取交易员 %s 账户信息超时", trader.GetID()) + // Timeout + logger.Infof("⏰ Timeout getting account info for trader %s", trader.GetID()) traderData = map[string]interface{}{ "trader_id": trader.GetID(), "trader_name": trader.GetName(), @@ -324,7 +324,7 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ "margin_used_pct": 0.0, "is_running": status["is_running"], "system_prompt_template": trader.GetSystemPromptTemplate(), - "error": "获取超时", + "error": "Request timeout", } } @@ -332,7 +332,7 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ }(i, t) } - // 收集所有结果 + // Collect all results results := make([]map[string]interface{}, len(traders)) for i := 0; i < len(traders); i++ { result := <-resultChan @@ -342,21 +342,21 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ return results } -// GetTopTradersData 获取前5名交易员数据(用于表现对比) +// GetTopTradersData retrieves top 5 traders data (for performance comparison) func (tm *TraderManager) GetTopTradersData() (map[string]interface{}, error) { - // 复用竞赛数据缓存,因为前5名是从全部数据中筛选出来的 + // Reuse competition data cache, as top 5 is filtered from all data competitionData, err := tm.GetCompetitionData() if err != nil { return nil, err } - // 从竞赛数据中提取前5名 + // Extract top 5 from competition data allTraders, ok := competitionData["traders"].([]map[string]interface{}) if !ok { - return nil, fmt.Errorf("竞赛数据格式错误") + return nil, fmt.Errorf("invalid competition data format") } - // 限制返回前5名 + // Limit to top 5 limit := 5 topTraders := allTraders if len(allTraders) > limit { @@ -372,53 +372,53 @@ func (tm *TraderManager) GetTopTradersData() (map[string]interface{}, error) { } -// RemoveTrader 从内存中移除指定的trader(不影响数据库) -// 用于更新trader配置时强制重新加载 +// RemoveTrader removes a trader from memory (does not affect database) +// Used to force reload when updating trader configuration func (tm *TraderManager) RemoveTrader(traderID string) { tm.mu.Lock() defer tm.mu.Unlock() if _, exists := tm.traders[traderID]; exists { delete(tm.traders, traderID) - logger.Infof("✓ Trader %s 已从内存中移除", traderID) + logger.Infof("✓ Trader %s removed from memory", traderID) } } -// LoadUserTradersFromStore 为特定用户从store加载交易员到内存 +// LoadUserTradersFromStore loads traders from store for a specific user to memory func (tm *TraderManager) LoadUserTradersFromStore(st *store.Store, userID string) error { tm.mu.Lock() defer tm.mu.Unlock() - // 获取指定用户的所有交易员 + // Get all traders for the specified user traders, err := st.Trader().List(userID) if err != nil { - return fmt.Errorf("获取用户 %s 的交易员列表失败: %w", userID, err) + return fmt.Errorf("failed to get trader list for user %s: %w", userID, err) } - logger.Infof("📋 为用户 %s 加载交易员配置: %d 个", userID, len(traders)) + logger.Infof("📋 Loading trader configurations for user %s: %d traders", userID, len(traders)) - // 获取AI模型和交易所列表(在循环外只查询一次) + // Get AI model and exchange lists (query only once outside loop) aiModels, err := st.AIModel().List(userID) if err != nil { - logger.Infof("⚠️ 获取用户 %s 的AI模型配置失败: %v", userID, err) - return fmt.Errorf("获取AI模型配置失败: %w", err) + logger.Infof("⚠️ Failed to get AI model config for user %s: %v", userID, err) + return fmt.Errorf("failed to get AI model config: %w", err) } exchanges, err := st.Exchange().List(userID) if err != nil { - logger.Infof("⚠️ 获取用户 %s 的交易所配置失败: %v", userID, err) - return fmt.Errorf("获取交易所配置失败: %w", err) + logger.Infof("⚠️ Failed to get exchange config for user %s: %v", userID, err) + return fmt.Errorf("failed to get exchange config: %w", err) } - // 为每个交易员加载配置 + // Load configuration for each trader for _, traderCfg := range traders { - // 检查是否已经加载过这个交易员 + // Check if this trader is already loaded if _, exists := tm.traders[traderCfg.ID]; exists { - logger.Infof("⚠️ 交易员 %s 已经加载,跳过", traderCfg.Name) + logger.Infof("⚠️ Trader %s already loaded, skipping", traderCfg.Name) continue } - // 从已查询的列表中查找AI模型配置 + // Find AI model config from already queried list var aiModelCfg *store.AIModel for _, model := range aiModels { if model.ID == traderCfg.AIModelID { @@ -436,16 +436,16 @@ func (tm *TraderManager) LoadUserTradersFromStore(st *store.Store, userID string } if aiModelCfg == nil { - logger.Infof("⚠️ 交易员 %s 的AI模型 %s 不存在,跳过", traderCfg.Name, traderCfg.AIModelID) + logger.Infof("⚠️ AI model %s for trader %s does not exist, skipping", traderCfg.AIModelID, traderCfg.Name) continue } if !aiModelCfg.Enabled { - logger.Infof("⚠️ 交易员 %s 的AI模型 %s 未启用,跳过", traderCfg.Name, traderCfg.AIModelID) + logger.Infof("⚠️ AI model %s for trader %s is not enabled, skipping", traderCfg.AIModelID, traderCfg.Name) continue } - // 从已查询的列表中查找交易所配置 + // Find exchange config from already queried list var exchangeCfg *store.Exchange for _, exchange := range exchanges { if exchange.ID == traderCfg.ExchangeID { @@ -455,95 +455,95 @@ func (tm *TraderManager) LoadUserTradersFromStore(st *store.Store, userID string } if exchangeCfg == nil { - logger.Infof("⚠️ 交易员 %s 的交易所 %s 不存在,跳过", traderCfg.Name, traderCfg.ExchangeID) + logger.Infof("⚠️ Exchange %s for trader %s does not exist, skipping", traderCfg.ExchangeID, traderCfg.Name) continue } if !exchangeCfg.Enabled { - logger.Infof("⚠️ 交易员 %s 的交易所 %s 未启用,跳过", traderCfg.Name, traderCfg.ExchangeID) + logger.Infof("⚠️ Exchange %s for trader %s is not enabled, skipping", traderCfg.ExchangeID, traderCfg.Name) continue } - // 使用现有的方法加载交易员 - logger.Infof("📦 正在加载交易员 %s (AI模型: %s, 交易所: %s, 策略ID: %s)", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID, traderCfg.StrategyID) + // Use existing method to load trader + logger.Infof("📦 Loading trader %s (AI Model: %s, Exchange: %s, Strategy ID: %s)", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID, traderCfg.StrategyID) err = tm.addTraderFromStore(traderCfg, aiModelCfg, exchangeCfg, st) if err != nil { - logger.Infof("❌ 加载交易员 %s 失败: %v", traderCfg.Name, err) + logger.Infof("❌ Failed to load trader %s: %v", traderCfg.Name, err) } } return nil } -// LoadTradersFromStore 从store加载所有交易员到内存(新版API) +// LoadTradersFromStore loads all traders from store to memory (new API) func (tm *TraderManager) LoadTradersFromStore(st *store.Store) error { tm.mu.Lock() defer tm.mu.Unlock() - // 获取所有用户 + // Get all users userIDs, err := st.User().GetAllIDs() if err != nil { - return fmt.Errorf("获取用户列表失败: %w", err) + return fmt.Errorf("failed to get user list: %w", err) } - logger.Infof("📋 发现 %d 个用户,开始加载所有交易员配置...", len(userIDs)) + logger.Infof("📋 Found %d users, loading all trader configurations...", len(userIDs)) var allTraders []*store.Trader for _, userID := range userIDs { - // 获取每个用户的交易员 + // Get traders for each user traders, err := st.Trader().List(userID) if err != nil { - logger.Infof("⚠️ 获取用户 %s 的交易员失败: %v", userID, err) + logger.Infof("⚠️ Failed to get traders for user %s: %v", userID, err) continue } - logger.Infof("📋 用户 %s: %d 个交易员", userID, len(traders)) + logger.Infof("📋 User %s: %d traders", userID, len(traders)) allTraders = append(allTraders, traders...) } - logger.Infof("📋 总共加载 %d 个交易员配置", len(allTraders)) + logger.Infof("📋 Total loaded trader configurations: %d", len(allTraders)) - // 为每个交易员获取AI模型和交易所配置 + // Get AI model and exchange configs for each trader for _, traderCfg := range allTraders { - // 获取AI模型配置 + // Get AI model config aiModels, err := st.AIModel().List(traderCfg.UserID) if err != nil { - logger.Infof("⚠️ 获取AI模型配置失败: %v", err) + logger.Infof("⚠️ Failed to get AI model config: %v", err) continue } var aiModelCfg *store.AIModel - // 优先精确匹配 model.ID + // Prioritize exact match on model.ID for _, model := range aiModels { if model.ID == traderCfg.AIModelID { aiModelCfg = model break } } - // 如果没有精确匹配,尝试匹配 provider(兼容旧数据) + // If no exact match, try matching provider (for backward compatibility) if aiModelCfg == nil { for _, model := range aiModels { if model.Provider == traderCfg.AIModelID { aiModelCfg = model - logger.Infof("⚠️ 交易员 %s 使用旧版 provider 匹配: %s -> %s", traderCfg.Name, traderCfg.AIModelID, model.ID) + logger.Infof("⚠️ Trader %s using legacy provider match: %s -> %s", traderCfg.Name, traderCfg.AIModelID, model.ID) break } } } if aiModelCfg == nil { - logger.Infof("⚠️ 交易员 %s 的AI模型 %s 不存在,跳过", traderCfg.Name, traderCfg.AIModelID) + logger.Infof("⚠️ AI model %s for trader %s does not exist, skipping", traderCfg.AIModelID, traderCfg.Name) continue } if !aiModelCfg.Enabled { - logger.Infof("⚠️ 交易员 %s 的AI模型 %s 未启用,跳过", traderCfg.Name, traderCfg.AIModelID) + logger.Infof("⚠️ AI model %s for trader %s is not enabled, skipping", traderCfg.AIModelID, traderCfg.Name) continue } - // 获取交易所配置 + // Get exchange config exchanges, err := st.Exchange().List(traderCfg.UserID) if err != nil { - logger.Infof("⚠️ 获取交易所配置失败: %v", err) + logger.Infof("⚠️ Failed to get exchange config: %v", err) continue } @@ -556,51 +556,51 @@ func (tm *TraderManager) LoadTradersFromStore(st *store.Store) error { } if exchangeCfg == nil { - logger.Infof("⚠️ 交易员 %s 的交易所 %s 不存在,跳过", traderCfg.Name, traderCfg.ExchangeID) + logger.Infof("⚠️ Exchange %s for trader %s does not exist, skipping", traderCfg.ExchangeID, traderCfg.Name) continue } if !exchangeCfg.Enabled { - logger.Infof("⚠️ 交易员 %s 的交易所 %s 未启用,跳过", traderCfg.Name, traderCfg.ExchangeID) + logger.Infof("⚠️ Exchange %s for trader %s is not enabled, skipping", traderCfg.ExchangeID, traderCfg.Name) continue } - // 添加到TraderManager(coinPoolURL/oiTopURL 已从策略配置中获取) + // Add to TraderManager (coinPoolURL/oiTopURL already obtained from strategy config) err = tm.addTraderFromStore(traderCfg, aiModelCfg, exchangeCfg, st) if err != nil { - logger.Infof("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err) + logger.Infof("❌ Failed to add trader %s: %v", traderCfg.Name, err) continue } } - logger.Infof("✓ 成功加载 %d 个交易员到内存", len(tm.traders)) + logger.Infof("✓ Successfully loaded %d traders to memory", len(tm.traders)) return nil } -// addTraderFromStore 内部方法:从store配置添加交易员 +// addTraderFromStore internal method: adds trader from store configuration func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg *store.AIModel, exchangeCfg *store.Exchange, st *store.Store) error { if _, exists := tm.traders[traderCfg.ID]; exists { - return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID) + return fmt.Errorf("trader ID '%s' already exists", traderCfg.ID) } - // 加载策略配置(必须有策略) + // Load strategy config (must have strategy) var strategyConfig *store.StrategyConfig if traderCfg.StrategyID != "" { strategy, err := st.Strategy().Get(traderCfg.UserID, traderCfg.StrategyID) if err != nil { - return fmt.Errorf("交易员 %s 的策略 %s 加载失败: %w", traderCfg.Name, traderCfg.StrategyID, err) + return fmt.Errorf("failed to load strategy %s for trader %s: %w", traderCfg.StrategyID, traderCfg.Name, err) } - // 解析 JSON 配置 + // Parse JSON config strategyConfig, err = strategy.ParseConfig() if err != nil { - return fmt.Errorf("交易员 %s 的策略配置解析失败: %w", traderCfg.Name, err) + return fmt.Errorf("failed to parse strategy config for trader %s: %w", traderCfg.Name, err) } - logger.Infof("✓ 交易员 %s 加载策略配置: %s", traderCfg.Name, strategy.Name) + logger.Infof("✓ Trader %s loaded strategy config: %s", traderCfg.Name, strategy.Name) } else { - return fmt.Errorf("交易员 %s 未配置策略", traderCfg.Name) + return fmt.Errorf("trader %s has no strategy configured", traderCfg.Name) } - // 构建AutoTraderConfig(coinPoolURL/oiTopURL 从策略配置获取,在 StrategyEngine 中使用) + // Build AutoTraderConfig (coinPoolURL/oiTopURL obtained from strategy config, used in StrategyEngine) traderConfig := trader.AutoTraderConfig{ ID: traderCfg.ID, Name: traderCfg.Name, @@ -621,7 +621,7 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg StrategyConfig: strategyConfig, } - // 根据交易所类型设置API密钥 + // Set API keys based on exchange type switch exchangeCfg.ID { case "binance": traderConfig.BinanceAPIKey = exchangeCfg.APIKey @@ -646,31 +646,31 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg traderConfig.LighterTestnet = exchangeCfg.Testnet } - // 根据AI模型设置API密钥 + // Set API keys based on AI model if aiModelCfg.Provider == "qwen" { traderConfig.QwenKey = aiModelCfg.APIKey } else if aiModelCfg.Provider == "deepseek" { traderConfig.DeepSeekKey = aiModelCfg.APIKey } - // 创建trader实例 + // Create trader instance at, err := trader.NewAutoTrader(traderConfig, st, traderCfg.UserID) if err != nil { - return fmt.Errorf("创建trader失败: %w", err) + return fmt.Errorf("failed to create trader: %w", err) } - // 设置自定义prompt(如果有) + // Set custom prompt (if exists) if traderCfg.CustomPrompt != "" { at.SetCustomPrompt(traderCfg.CustomPrompt) at.SetOverrideBasePrompt(traderCfg.OverrideBasePrompt) if traderCfg.OverrideBasePrompt { - logger.Infof("✓ 已设置自定义交易策略prompt (覆盖基础prompt)") + logger.Infof("✓ Set custom trading strategy prompt (overriding base prompt)") } else { - logger.Infof("✓ 已设置自定义交易策略prompt (补充基础prompt)") + logger.Infof("✓ Set custom trading strategy prompt (supplementing base prompt)") } } tm.traders[traderCfg.ID] = at - logger.Infof("✓ Trader '%s' (%s + %s) 已加载到内存", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID) + logger.Infof("✓ Trader '%s' (%s + %s) loaded to memory", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID) return nil } diff --git a/manager/trader_manager_test.go b/manager/trader_manager_test.go index c5344d95..6d3f0bc5 100644 --- a/manager/trader_manager_test.go +++ b/manager/trader_manager_test.go @@ -4,51 +4,51 @@ import ( "testing" ) -// TestRemoveTrader 测试从内存中移除trader +// TestRemoveTrader tests removing trader from memory func TestRemoveTrader(t *testing.T) { tm := NewTraderManager() - // 创建一个模拟的 trader 并添加到 map + // Create a mock trader and add it to map traderID := "test-trader-123" - tm.traders[traderID] = nil // 使用 nil 作为占位符,实际测试中只需验证删除逻辑 + tm.traders[traderID] = nil // Use nil as placeholder, only need to verify deletion logic in test - // 验证 trader 存在 + // Verify trader exists if _, exists := tm.traders[traderID]; !exists { - t.Fatal("trader 应该存在于 map 中") + t.Fatal("trader should exist in map") } - // 调用 RemoveTrader + // Call RemoveTrader tm.RemoveTrader(traderID) - // 验证 trader 已被移除 + // Verify trader has been removed if _, exists := tm.traders[traderID]; exists { - t.Error("trader 应该已从 map 中移除") + t.Error("trader should be removed from map") } } -// TestRemoveTrader_NonExistent 测试移除不存在的trader不会报错 +// TestRemoveTrader_NonExistent tests that removing non-existent trader doesn't error func TestRemoveTrader_NonExistent(t *testing.T) { tm := NewTraderManager() - // 尝试移除不存在的 trader,不应该 panic + // Trying to remove non-existent trader should not panic defer func() { if r := recover(); r != nil { - t.Errorf("移除不存在的 trader 不应该 panic: %v", r) + t.Errorf("removing non-existent trader should not panic: %v", r) } }() tm.RemoveTrader("non-existent-trader") } -// TestRemoveTrader_Concurrent 测试并发移除trader的安全性 +// TestRemoveTrader_Concurrent tests concurrent removal of trader safety func TestRemoveTrader_Concurrent(t *testing.T) { tm := NewTraderManager() traderID := "test-trader-concurrent" - // 添加 trader + // Add trader tm.traders[traderID] = nil - // 并发调用 RemoveTrader + // Concurrently call RemoveTrader done := make(chan bool, 10) for i := 0; i < 10; i++ { go func() { @@ -57,31 +57,31 @@ func TestRemoveTrader_Concurrent(t *testing.T) { }() } - // 等待所有 goroutine 完成 + // Wait for all goroutines to complete for i := 0; i < 10; i++ { <-done } - // 验证 trader 已被移除 + // Verify trader has been removed if _, exists := tm.traders[traderID]; exists { - t.Error("trader 应该已从 map 中移除") + t.Error("trader should be removed from map") } } -// TestGetTrader_AfterRemove 测试移除后获取trader返回错误 +// TestGetTrader_AfterRemove tests that getting trader after removal returns error func TestGetTrader_AfterRemove(t *testing.T) { tm := NewTraderManager() traderID := "test-trader-get" - // 添加 trader + // Add trader tm.traders[traderID] = nil - // 移除 trader + // Remove trader tm.RemoveTrader(traderID) - // 尝试获取已移除的 trader + // Try to get removed trader _, err := tm.GetTrader(traderID) if err == nil { - t.Error("获取已移除的 trader 应该返回错误") + t.Error("getting removed trader should return error") } } diff --git a/market/api_client.go b/market/api_client.go index 3b9c268e..47ccff4d 100644 --- a/market/api_client.go +++ b/market/api_client.go @@ -26,7 +26,7 @@ func NewAPIClient() *APIClient { hookRes := hook.HookExec[hook.SetHttpClientResult](hook.SET_HTTP_CLIENT, client) if hookRes != nil && hookRes.Error() == nil { - log.Printf("使用Hook设置的HTTP客户端") + log.Printf("Using HTTP client set by Hook") client = hookRes.GetResult() } @@ -83,7 +83,7 @@ func (c *APIClient) GetKlines(symbol, interval string, limit int) ([]Kline, erro var klineResponses []KlineResponse err = json.Unmarshal(body, &klineResponses) if err != nil { - log.Printf("获取K线数据失败,响应内容: %s", string(body)) + log.Printf("Failed to get K-line data, response content: %s", string(body)) return nil, err } @@ -91,7 +91,7 @@ func (c *APIClient) GetKlines(symbol, interval string, limit int) ([]Kline, erro for _, kr := range klineResponses { kline, err := parseKline(kr) if err != nil { - log.Printf("解析K线数据失败: %v", err) + log.Printf("Failed to parse K-line data: %v", err) continue } klines = append(klines, kline) @@ -107,7 +107,7 @@ func parseKline(kr KlineResponse) (Kline, error) { return kline, fmt.Errorf("invalid kline data") } - // 解析各个字段 + // Parse each field kline.OpenTime = int64(kr[0].(float64)) kline.Open, _ = strconv.ParseFloat(kr[1].(string), 64) kline.High, _ = strconv.ParseFloat(kr[2].(string), 64) diff --git a/market/combined_streams.go b/market/combined_streams.go index 801d423e..31ad085c 100644 --- a/market/combined_streams.go +++ b/market/combined_streams.go @@ -17,7 +17,7 @@ type CombinedStreamsClient struct { subscribers map[string]chan []byte reconnect bool done chan struct{} - batchSize int // 每批订阅的流数量 + batchSize int // Number of streams per batch subscription } func NewCombinedStreamsClient(batchSize int) *CombinedStreamsClient { @@ -34,29 +34,29 @@ func (c *CombinedStreamsClient) Connect() error { HandshakeTimeout: 10 * time.Second, } - // 组合流使用不同的端点 + // Combined streams use a different endpoint conn, _, err := dialer.Dial("wss://fstream.binance.com/stream", nil) if err != nil { - return fmt.Errorf("组合流WebSocket连接失败: %v", err) + return fmt.Errorf("Combined stream WebSocket connection failed: %v", err) } c.mu.Lock() c.conn = conn c.mu.Unlock() - log.Println("组合流WebSocket连接成功") + log.Println("Combined stream WebSocket connected successfully") go c.readMessages() return nil } -// BatchSubscribeKlines 批量订阅K线 +// BatchSubscribeKlines subscribes to K-lines in batches func (c *CombinedStreamsClient) BatchSubscribeKlines(symbols []string, interval string) error { - // 将symbols分批处理 + // Split symbols into batches batches := c.splitIntoBatches(symbols, c.batchSize) for i, batch := range batches { - log.Printf("订阅第 %d 批, 数量: %d", i+1, len(batch)) + log.Printf("Subscribing batch %d, count: %d", i+1, len(batch)) streams := make([]string, len(batch)) for j, symbol := range batch { @@ -64,10 +64,10 @@ func (c *CombinedStreamsClient) BatchSubscribeKlines(symbols []string, interval } if err := c.subscribeStreams(streams); err != nil { - return fmt.Errorf("第 %d 批订阅失败: %v", i+1, err) + return fmt.Errorf("Batch %d subscription failed: %v", i+1, err) } - // 批次间延迟,避免被限制 + // Delay between batches to avoid rate limiting if i < len(batches)-1 { time.Sleep(100 * time.Millisecond) } @@ -76,7 +76,7 @@ func (c *CombinedStreamsClient) BatchSubscribeKlines(symbols []string, interval return nil } -// splitIntoBatches 将切片分成指定大小的批次 +// splitIntoBatches splits a slice into batches of specified size func (c *CombinedStreamsClient) splitIntoBatches(symbols []string, batchSize int) [][]string { var batches [][]string @@ -91,7 +91,7 @@ func (c *CombinedStreamsClient) splitIntoBatches(symbols []string, batchSize int return batches } -// subscribeStreams 订阅多个流 +// subscribeStreams subscribes to multiple streams func (c *CombinedStreamsClient) subscribeStreams(streams []string) error { subscribeMsg := map[string]interface{}{ "method": "SUBSCRIBE", @@ -103,10 +103,10 @@ func (c *CombinedStreamsClient) subscribeStreams(streams []string) error { defer c.mu.RUnlock() if c.conn == nil { - return fmt.Errorf("WebSocket未连接") + return fmt.Errorf("WebSocket not connected") } - log.Printf("订阅流: %v", streams) + log.Printf("Subscribing to streams: %v", streams) return c.conn.WriteJSON(subscribeMsg) } @@ -127,7 +127,7 @@ func (c *CombinedStreamsClient) readMessages() { _, message, err := conn.ReadMessage() if err != nil { - log.Printf("读取组合流消息失败: %v", err) + log.Printf("Failed to read combined stream message: %v", err) c.handleReconnect() return } @@ -144,7 +144,7 @@ func (c *CombinedStreamsClient) handleCombinedMessage(message []byte) { } if err := json.Unmarshal(message, &combinedMsg); err != nil { - log.Printf("解析组合消息失败: %v", err) + log.Printf("Failed to parse combined message: %v", err) return } @@ -156,7 +156,7 @@ func (c *CombinedStreamsClient) handleCombinedMessage(message []byte) { select { case ch <- combinedMsg.Data: default: - log.Printf("订阅者通道已满: %s", combinedMsg.Stream) + log.Printf("Subscriber channel is full: %s", combinedMsg.Stream) } } } @@ -174,11 +174,11 @@ func (c *CombinedStreamsClient) handleReconnect() { return } - log.Println("组合流尝试重新连接...") + log.Println("Combined stream attempting to reconnect...") time.Sleep(3 * time.Second) if err := c.Connect(); err != nil { - log.Printf("组合流重新连接失败: %v", err) + log.Printf("Combined stream reconnection failed: %v", err) go c.handleReconnect() } } diff --git a/market/data.go b/market/data.go index c60e024e..981be85a 100644 --- a/market/data.go +++ b/market/data.go @@ -12,8 +12,8 @@ import ( "time" ) -// FundingRateCache 资金费率缓存结构 -// Binance Funding Rate 每 8 小时才更新一次,使用 1 小时缓存可显著减少 API 调用 +// FundingRateCache is the funding rate cache structure +// Binance Funding Rate only updates every 8 hours, using 1-hour cache can significantly reduce API calls type FundingRateCache struct { Rate float64 UpdatedAt time.Time @@ -24,16 +24,16 @@ var ( frCacheTTL = 1 * time.Hour ) -// Get 获取指定代币的市场数据 +// Get retrieves market data for the specified token func Get(symbol string) (*Data, error) { var klines3m, klines4h []Kline var err error - // 标准化symbol + // Normalize symbol symbol = Normalize(symbol) - // 获取3分钟K线数据 (最近10个) - klines3m, err = WSMonitorCli.GetCurrentKlines(symbol, "3m") // 多获取一些用于计算 + // Get 3-minute K-line data (latest 10) + klines3m, err = WSMonitorCli.GetCurrentKlines(symbol, "3m") // Get more for calculation if err != nil { - return nil, fmt.Errorf("获取3分钟K线失败: %v", err) + return nil, fmt.Errorf("Failed to get 3-minute K-line: %v", err) } // Data staleness detection: Prevent DOGEUSDT-style price freeze issues @@ -42,37 +42,37 @@ func Get(symbol string) (*Data, error) { return nil, fmt.Errorf("%s data is stale, possible cache failure", symbol) } - // 获取4小时K线数据 (最近10个) - klines4h, err = WSMonitorCli.GetCurrentKlines(symbol, "4h") // 多获取用于计算指标 + // Get 4-hour K-line data (latest 10) + klines4h, err = WSMonitorCli.GetCurrentKlines(symbol, "4h") // Get more for indicator calculation if err != nil { - return nil, fmt.Errorf("获取4小时K线失败: %v", err) + return nil, fmt.Errorf("Failed to get 4-hour K-line: %v", err) } - // 检查数据是否为空 + // Check if data is empty if len(klines3m) == 0 { - return nil, fmt.Errorf("3分钟K线数据为空") + return nil, fmt.Errorf("3-minute K-line data is empty") } if len(klines4h) == 0 { - return nil, fmt.Errorf("4小时K线数据为空") + return nil, fmt.Errorf("4-hour K-line data is empty") } - // 计算当前指标 (基于3分钟最新数据) + // Calculate current indicators (based on 3-minute latest data) currentPrice := klines3m[len(klines3m)-1].Close currentEMA20 := calculateEMA(klines3m, 20) currentMACD := calculateMACD(klines3m) currentRSI7 := calculateRSI(klines3m, 7) - // 计算价格变化百分比 - // 1小时价格变化 = 20个3分钟K线前的价格 + // Calculate price change percentage + // 1-hour price change = price from 20 3-minute K-lines ago priceChange1h := 0.0 - if len(klines3m) >= 21 { // 至少需要21根K线 (当前 + 20根前) + if len(klines3m) >= 21 { // Need at least 21 K-lines (current + 20 previous) price1hAgo := klines3m[len(klines3m)-21].Close if price1hAgo > 0 { priceChange1h = ((currentPrice - price1hAgo) / price1hAgo) * 100 } } - // 4小时价格变化 = 1个4小时K线前的价格 + // 4-hour price change = price from 1 4-hour K-line ago priceChange4h := 0.0 if len(klines4h) >= 2 { price4hAgo := klines4h[len(klines4h)-2].Close @@ -81,20 +81,20 @@ func Get(symbol string) (*Data, error) { } } - // 获取OI数据 + // Get OI data oiData, err := getOpenInterestData(symbol) if err != nil { - // OI失败不影响整体,使用默认值 + // OI failure doesn't affect overall result, use default values oiData = &OIData{Latest: 0, Average: 0} } - // 获取Funding Rate + // Get Funding Rate fundingRate, _ := getFundingRate(symbol) - // 计算日内系列数据 + // Calculate intraday series data intradayData := calculateIntradaySeries(klines3m) - // 计算长期数据 + // Calculate longer-term data longerTermData := calculateLongerTermData(klines4h) return &Data{ @@ -112,23 +112,23 @@ func Get(symbol string) (*Data, error) { }, nil } -// GetWithTimeframes 获取指定多个时间周期的市场数据 -// timeframes: 时间周期列表,如 ["5m", "15m", "1h", "4h"] -// primaryTimeframe: 主时间周期(用于计算当前指标),默认使用 timeframes[0] -// count: 每个时间周期的 K 线数量 +// GetWithTimeframes retrieves market data for specified multiple timeframes +// timeframes: list of timeframes, e.g. ["5m", "15m", "1h", "4h"] +// primaryTimeframe: primary timeframe (used for calculating current indicators), defaults to timeframes[0] +// count: number of K-lines for each timeframe func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe string, count int) (*Data, error) { symbol = Normalize(symbol) if len(timeframes) == 0 { - return nil, fmt.Errorf("至少需要一个时间周期") + return nil, fmt.Errorf("at least one timeframe is required") } - // 如果未指定主周期,使用第一个 + // If primary timeframe is not specified, use the first one if primaryTimeframe == "" { primaryTimeframe = timeframes[0] } - // 确保主周期在列表中 + // Ensure primary timeframe is in the list hasPrimary := false for _, tf := range timeframes { if tf == primaryTimeframe { @@ -140,36 +140,36 @@ func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe stri timeframes = append([]string{primaryTimeframe}, timeframes...) } - // 存储所有时间周期的数据 + // Store data for all timeframes timeframeData := make(map[string]*TimeframeSeriesData) var primaryKlines []Kline - // 获取每个时间周期的 K 线数据 + // Get K-line data for each timeframe for _, tf := range timeframes { klines, err := WSMonitorCli.GetCurrentKlines(symbol, tf) if err != nil { - logger.Infof("⚠️ 获取 %s %s K线失败: %v", symbol, tf, err) + logger.Infof("⚠️ Failed to get %s %s K-line: %v", symbol, tf, err) continue } if len(klines) == 0 { - logger.Infof("⚠️ %s %s K线数据为空", symbol, tf) + logger.Infof("⚠️ %s %s K-line data is empty", symbol, tf) continue } - // 保存主周期的 K 线用于计算基础指标 + // Save primary timeframe K-lines for calculating base indicators if tf == primaryTimeframe { primaryKlines = klines } - // 计算该时间周期的系列数据 + // Calculate series data for this timeframe seriesData := calculateTimeframeSeries(klines, tf) timeframeData[tf] = seriesData } - // 如果主周期数据为空,返回错误 + // If primary timeframe data is empty, return error if len(primaryKlines) == 0 { - return nil, fmt.Errorf("主时间周期 %s K线数据为空", primaryTimeframe) + return nil, fmt.Errorf("Primary timeframe %s K-line data is empty", primaryTimeframe) } // Data staleness detection @@ -178,23 +178,23 @@ func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe stri return nil, fmt.Errorf("%s data is stale, possible cache failure", symbol) } - // 计算当前指标 (基于主周期最新数据) + // Calculate current indicators (based on primary timeframe latest data) currentPrice := primaryKlines[len(primaryKlines)-1].Close currentEMA20 := calculateEMA(primaryKlines, 20) currentMACD := calculateMACD(primaryKlines) currentRSI7 := calculateRSI(primaryKlines, 7) - // 计算价格变化 - priceChange1h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 60) // 1小时 - priceChange4h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 240) // 4小时 + // Calculate price changes + priceChange1h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 60) // 1 hour + priceChange4h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 240) // 4 hours - // 获取OI数据 + // Get OI data oiData, err := getOpenInterestData(symbol) if err != nil { oiData = &OIData{Latest: 0, Average: 0} } - // 获取Funding Rate + // Get Funding Rate fundingRate, _ := getFundingRate(symbol) return &Data{ @@ -211,7 +211,7 @@ func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe stri }, nil } -// calculateTimeframeSeries 计算单个时间周期的系列数据 +// calculateTimeframeSeries calculates series data for a single timeframe func calculateTimeframeSeries(klines []Kline, timeframe string) *TimeframeSeriesData { data := &TimeframeSeriesData{ Timeframe: timeframe, @@ -224,7 +224,7 @@ func calculateTimeframeSeries(klines []Kline, timeframe string) *TimeframeSeries Volume: make([]float64, 0, 10), } - // 获取最近10个数据点 + // Get latest 10 data points start := len(klines) - 10 if start < 0 { start = 0 @@ -234,25 +234,25 @@ func calculateTimeframeSeries(klines []Kline, timeframe string) *TimeframeSeries data.MidPrices = append(data.MidPrices, klines[i].Close) data.Volume = append(data.Volume, klines[i].Volume) - // 计算每个点的 EMA20 + // Calculate EMA20 for each point if i >= 19 { ema20 := calculateEMA(klines[:i+1], 20) data.EMA20Values = append(data.EMA20Values, ema20) } - // 计算每个点的 EMA50 + // Calculate EMA50 for each point if i >= 49 { ema50 := calculateEMA(klines[:i+1], 50) data.EMA50Values = append(data.EMA50Values, ema50) } - // 计算每个点的 MACD + // Calculate MACD for each point if i >= 25 { macd := calculateMACD(klines[:i+1]) data.MACDValues = append(data.MACDValues, macd) } - // 计算每个点的 RSI + // Calculate RSI for each point if i >= 7 { rsi7 := calculateRSI(klines[:i+1], 7) data.RSI7Values = append(data.RSI7Values, rsi7) @@ -263,25 +263,25 @@ func calculateTimeframeSeries(klines []Kline, timeframe string) *TimeframeSeries } } - // 计算 ATR14 + // Calculate ATR14 data.ATR14 = calculateATR(klines, 14) return data } -// calculatePriceChangeByBars 根据时间周期计算需要回溯多少根 K 线来计算价格变化 +// calculatePriceChangeByBars calculates how many K-lines to look back for price change based on timeframe func calculatePriceChangeByBars(klines []Kline, timeframe string, targetMinutes int) float64 { if len(klines) < 2 { return 0 } - // 解析时间周期为分钟数 + // Parse timeframe to minutes tfMinutes := parseTimeframeToMinutes(timeframe) if tfMinutes <= 0 { return 0 } - // 计算需要回溯多少根 K 线 + // Calculate how many K-lines to look back barsBack := targetMinutes / tfMinutes if barsBack < 1 { barsBack = 1 @@ -300,7 +300,7 @@ func calculatePriceChangeByBars(klines []Kline, timeframe string, targetMinutes return 0 } -// parseTimeframeToMinutes 将时间周期字符串解析为分钟数 +// parseTimeframeToMinutes parses timeframe string to minutes func parseTimeframeToMinutes(tf string) int { switch tf { case "1m": @@ -336,20 +336,20 @@ func parseTimeframeToMinutes(tf string) int { } } -// calculateEMA 计算EMA +// calculateEMA calculates EMA func calculateEMA(klines []Kline, period int) float64 { if len(klines) < period { return 0 } - // 计算SMA作为初始EMA + // Calculate SMA as initial EMA sum := 0.0 for i := 0; i < period; i++ { sum += klines[i].Close } ema := sum / float64(period) - // 计算EMA + // Calculate EMA multiplier := 2.0 / float64(period+1) for i := period; i < len(klines); i++ { ema = (klines[i].Close-ema)*multiplier + ema @@ -358,13 +358,13 @@ func calculateEMA(klines []Kline, period int) float64 { return ema } -// calculateMACD 计算MACD +// calculateMACD calculates MACD func calculateMACD(klines []Kline) float64 { if len(klines) < 26 { return 0 } - // 计算12期和26期EMA + // Calculate 12-period and 26-period EMA ema12 := calculateEMA(klines, 12) ema26 := calculateEMA(klines, 26) @@ -372,7 +372,7 @@ func calculateMACD(klines []Kline) float64 { return ema12 - ema26 } -// calculateRSI 计算RSI +// calculateRSI calculates RSI func calculateRSI(klines []Kline, period int) float64 { if len(klines) <= period { return 0 @@ -381,7 +381,7 @@ func calculateRSI(klines []Kline, period int) float64 { gains := 0.0 losses := 0.0 - // 计算初始平均涨跌幅 + // Calculate initial average gain/loss for i := 1; i <= period; i++ { change := klines[i].Close - klines[i-1].Close if change > 0 { @@ -394,7 +394,7 @@ func calculateRSI(klines []Kline, period int) float64 { avgGain := gains / float64(period) avgLoss := losses / float64(period) - // 使用Wilder平滑方法计算后续RSI + // Use Wilder smoothing method to calculate subsequent RSI for i := period + 1; i < len(klines); i++ { change := klines[i].Close - klines[i-1].Close if change > 0 { @@ -416,7 +416,7 @@ func calculateRSI(klines []Kline, period int) float64 { return rsi } -// calculateATR 计算ATR +// calculateATR calculates ATR func calculateATR(klines []Kline, period int) float64 { if len(klines) <= period { return 0 @@ -435,14 +435,14 @@ func calculateATR(klines []Kline, period int) float64 { trs[i] = math.Max(tr1, math.Max(tr2, tr3)) } - // 计算初始ATR + // Calculate initial ATR sum := 0.0 for i := 1; i <= period; i++ { sum += trs[i] } atr := sum / float64(period) - // Wilder平滑 + // Wilder smoothing for i := period + 1; i < len(klines); i++ { atr = (atr*float64(period-1) + trs[i]) / float64(period) } @@ -450,7 +450,7 @@ func calculateATR(klines []Kline, period int) float64 { return atr } -// calculateIntradaySeries 计算日内系列数据 +// calculateIntradaySeries calculates intraday series data func calculateIntradaySeries(klines []Kline) *IntradayData { data := &IntradayData{ MidPrices: make([]float64, 0, 10), @@ -461,7 +461,7 @@ func calculateIntradaySeries(klines []Kline) *IntradayData { Volume: make([]float64, 0, 10), } - // 获取最近10个数据点 + // Get latest 10 data points start := len(klines) - 10 if start < 0 { start = 0 @@ -471,19 +471,19 @@ func calculateIntradaySeries(klines []Kline) *IntradayData { data.MidPrices = append(data.MidPrices, klines[i].Close) data.Volume = append(data.Volume, klines[i].Volume) - // 计算每个点的EMA20 + // Calculate EMA20 for each point if i >= 19 { ema20 := calculateEMA(klines[:i+1], 20) data.EMA20Values = append(data.EMA20Values, ema20) } - // 计算每个点的MACD + // Calculate MACD for each point if i >= 25 { macd := calculateMACD(klines[:i+1]) data.MACDValues = append(data.MACDValues, macd) } - // 计算每个点的RSI + // Calculate RSI for each point if i >= 7 { rsi7 := calculateRSI(klines[:i+1], 7) data.RSI7Values = append(data.RSI7Values, rsi7) @@ -494,31 +494,31 @@ func calculateIntradaySeries(klines []Kline) *IntradayData { } } - // 计算3m ATR14 + // Calculate 3m ATR14 data.ATR14 = calculateATR(klines, 14) return data } -// calculateLongerTermData 计算长期数据 +// calculateLongerTermData calculates longer-term data func calculateLongerTermData(klines []Kline) *LongerTermData { data := &LongerTermData{ MACDValues: make([]float64, 0, 10), RSI14Values: make([]float64, 0, 10), } - // 计算EMA + // Calculate EMA data.EMA20 = calculateEMA(klines, 20) data.EMA50 = calculateEMA(klines, 50) - // 计算ATR + // Calculate ATR data.ATR3 = calculateATR(klines, 3) data.ATR14 = calculateATR(klines, 14) - // 计算成交量 + // Calculate volume if len(klines) > 0 { data.CurrentVolume = klines[len(klines)-1].Volume - // 计算平均成交量 + // Calculate average volume sum := 0.0 for _, k := range klines { sum += k.Volume @@ -526,7 +526,7 @@ func calculateLongerTermData(klines []Kline) *LongerTermData { data.AverageVolume = sum / float64(len(klines)) } - // 计算MACD和RSI序列 + // Calculate MACD and RSI series start := len(klines) - 10 if start < 0 { start = 0 @@ -546,7 +546,7 @@ func calculateLongerTermData(klines []Kline) *LongerTermData { return data } -// getOpenInterestData 获取OI数据 +// getOpenInterestData retrieves OI data func getOpenInterestData(symbol string) (*OIData, error) { url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/openInterest?symbol=%s", symbol) @@ -576,23 +576,23 @@ func getOpenInterestData(symbol string) (*OIData, error) { return &OIData{ Latest: oi, - Average: oi * 0.999, // 近似平均值 + Average: oi * 0.999, // Approximate average }, nil } -// getFundingRate 获取资金费率(优化:使用 1 小时缓存) +// getFundingRate retrieves funding rate (optimized: uses 1-hour cache) func getFundingRate(symbol string) (float64, error) { - // 检查缓存(有效期 1 小时) - // Funding Rate 每 8 小时才更新,1 小时缓存非常合理 + // Check cache (1-hour validity) + // Funding Rate only updates every 8 hours, 1-hour cache is very reasonable if cached, ok := fundingRateMap.Load(symbol); ok { cache := cached.(*FundingRateCache) if time.Since(cache.UpdatedAt) < frCacheTTL { - // 缓存命中,直接返回 + // Cache hit, return directly return cache.Rate, nil } } - // 缓存过期或不存在,调用 API + // Cache expired or doesn't exist, call API url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/premiumIndex?symbol=%s", symbol) apiClient := NewAPIClient() @@ -623,7 +623,7 @@ func getFundingRate(symbol string) (float64, error) { rate, _ := strconv.ParseFloat(result.LastFundingRate, 64) - // 更新缓存 + // Update cache fundingRateMap.Store(symbol, &FundingRateCache{ Rate: rate, UpdatedAt: time.Now(), @@ -632,11 +632,11 @@ func getFundingRate(symbol string) (float64, error) { return rate, nil } -// Format 格式化输出市场数据 +// Format formats and outputs market data func Format(data *Data) string { var sb strings.Builder - // 使用动态精度格式化价格 + // Format price with dynamic precision priceStr := formatPriceWithDynamicPrecision(data.CurrentPrice) sb.WriteString(fmt.Sprintf("current_price = %s, current_ema20 = %.3f, current_macd = %.3f, current_rsi (7 period) = %.3f\n\n", priceStr, data.CurrentEMA20, data.CurrentMACD, data.CurrentRSI7)) @@ -645,7 +645,7 @@ func Format(data *Data) string { data.Symbol)) if data.OpenInterest != nil { - // 使用动态精度格式化 OI 数据 + // Format OI data with dynamic precision oiLatestStr := formatPriceWithDynamicPrecision(data.OpenInterest.Latest) oiAverageStr := formatPriceWithDynamicPrecision(data.OpenInterest.Average) sb.WriteString(fmt.Sprintf("Open Interest: Latest: %s Average: %s\n\n", @@ -705,9 +705,9 @@ func Format(data *Data) string { } } - // 多时间周期数据(新增) + // Multi-timeframe data (new) if len(data.TimeframeData) > 0 { - // 按时间周期排序输出 + // Output sorted by timeframe 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 { @@ -720,7 +720,7 @@ func Format(data *Data) string { return sb.String() } -// formatTimeframeData 格式化单个时间周期的数据 +// formatTimeframeData formats data for a single timeframe func formatTimeframeData(sb *strings.Builder, data *TimeframeSeriesData) { if len(data.MidPrices) > 0 { sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidPrices))) @@ -753,38 +753,38 @@ func formatTimeframeData(sb *strings.Builder, data *TimeframeSeriesData) { sb.WriteString(fmt.Sprintf("ATR (14‑period): %.3f\n\n", data.ATR14)) } -// formatPriceWithDynamicPrecision 根据价格区间动态选择精度 -// 这样可以完美支持从超低价 meme coin (< 0.0001) 到 BTC/ETH 的所有币种 +// formatPriceWithDynamicPrecision dynamically selects precision based on price range +// This perfectly supports all coins from ultra-low price meme coins (< 0.0001) to BTC/ETH func formatPriceWithDynamicPrecision(price float64) string { switch { case price < 0.0001: - // 超低价 meme coin: 1000SATS, 1000WHY, DOGS - // 0.00002070 → "0.00002070" (8位小数) + // Ultra-low price meme coins: 1000SATS, 1000WHY, DOGS + // 0.00002070 → "0.00002070" (8 decimal places) return fmt.Sprintf("%.8f", price) case price < 0.001: - // 低价 meme coin: NEIRO, HMSTR, HOT, NOT - // 0.00015060 → "0.000151" (6位小数) + // Low price meme coins: NEIRO, HMSTR, HOT, NOT + // 0.00015060 → "0.000151" (6 decimal places) return fmt.Sprintf("%.6f", price) case price < 0.01: - // 中低价币: PEPE, SHIB, MEME - // 0.00556800 → "0.005568" (6位小数) + // Mid-low price coins: PEPE, SHIB, MEME + // 0.00556800 → "0.005568" (6 decimal places) return fmt.Sprintf("%.6f", price) case price < 1.0: - // 低价币: ASTER, DOGE, ADA, TRX - // 0.9954 → "0.9954" (4位小数) + // Low price coins: ASTER, DOGE, ADA, TRX + // 0.9954 → "0.9954" (4 decimal places) return fmt.Sprintf("%.4f", price) case price < 100: - // 中价币: SOL, AVAX, LINK, MATIC - // 23.4567 → "23.4567" (4位小数) + // Mid price coins: SOL, AVAX, LINK, MATIC + // 23.4567 → "23.4567" (4 decimal places) return fmt.Sprintf("%.4f", price) default: - // 高价币: BTC, ETH (节省 Token) - // 45678.9123 → "45678.91" (2位小数) + // High price coins: BTC, ETH (save tokens) + // 45678.9123 → "45678.91" (2 decimal places) return fmt.Sprintf("%.2f", price) } } -// formatFloatSlice 格式化float64切片为字符串(使用动态精度) +// formatFloatSlice formats float64 slice to string (using dynamic precision) func formatFloatSlice(values []float64) string { strValues := make([]string, len(values)) for i, v := range values { @@ -793,7 +793,7 @@ func formatFloatSlice(values []float64) string { return "[" + strings.Join(strValues, ", ") + "]" } -// Normalize 标准化symbol,确保是USDT交易对 +// Normalize normalizes symbol, ensures it's a USDT trading pair func Normalize(symbol string) string { symbol = strings.ToUpper(symbol) if strings.HasSuffix(symbol, "USDT") { @@ -802,7 +802,7 @@ func Normalize(symbol string) string { return symbol + "USDT" } -// parseFloat 解析float值 +// parseFloat parses float value func parseFloat(v interface{}) (float64, error) { switch val := v.(type) { case string: @@ -818,7 +818,7 @@ func parseFloat(v interface{}) (float64, error) { } } -// BuildDataFromKlines 根据预加载的K线序列构造市场数据快照(用于回测/模拟)。 +// BuildDataFromKlines constructs market data snapshot from preloaded K-line series (for backtesting/simulation). func BuildDataFromKlines(symbol string, primary []Kline, longer []Kline) (*Data, error) { if len(primary) == 0 { return nil, fmt.Errorf("primary series is empty") diff --git a/market/data_test.go b/market/data_test.go index 984e727d..b20a336c 100644 --- a/market/data_test.go +++ b/market/data_test.go @@ -5,11 +5,11 @@ import ( "testing" ) -// generateTestKlines 生成测试用的 K线数据 +// generateTestKlines generates test K-line data func generateTestKlines(count int) []Kline { klines := make([]Kline, count) for i := 0; i < count; i++ { - // 生成模拟的价格数据,有一定的波动 + // Generate simulated price data with some fluctuation basePrice := 100.0 variance := float64(i%10) * 0.5 open := basePrice + variance @@ -19,7 +19,7 @@ func generateTestKlines(count int) []Kline { volume := 1000.0 + float64(i*100) klines[i] = Kline{ - OpenTime: int64(i * 180000), // 3分钟间隔 + OpenTime: int64(i * 180000), // 3-minute interval Open: open, High: high, Low: low, @@ -31,7 +31,7 @@ func generateTestKlines(count int) []Kline { return klines } -// TestCalculateIntradaySeries_VolumeCollection 测试 Volume 数据收集 +// TestCalculateIntradaySeries_VolumeCollection tests Volume data collection func TestCalculateIntradaySeries_VolumeCollection(t *testing.T) { tests := []struct { name string @@ -39,24 +39,24 @@ func TestCalculateIntradaySeries_VolumeCollection(t *testing.T) { expectedVolLen int }{ { - name: "正常情况 - 20个K线", + name: "Normal case - 20 K-lines", klineCount: 20, - expectedVolLen: 10, // 应该收集最近10个 + expectedVolLen: 10, // Should collect latest 10 }, { - name: "刚好10个K线", + name: "Exactly 10 K-lines", klineCount: 10, expectedVolLen: 10, }, { - name: "少于10个K线", + name: "Less than 10 K-lines", klineCount: 5, - expectedVolLen: 5, // 应该返回所有5个 + expectedVolLen: 5, // Should return all 5 }, { - name: "超过10个K线", + name: "More than 10 K-lines", klineCount: 30, - expectedVolLen: 10, // 应该只返回最近10个 + expectedVolLen: 10, // Should only return latest 10 }, } @@ -73,21 +73,21 @@ func TestCalculateIntradaySeries_VolumeCollection(t *testing.T) { t.Errorf("Volume length = %d, want %d", len(data.Volume), tt.expectedVolLen) } - // 验证 Volume 数据正确性 + // Verify Volume data correctness if len(data.Volume) > 0 { - // 计算期望的起始索引 + // Calculate expected start index start := tt.klineCount - 10 if start < 0 { start = 0 } - // 验证第一个 Volume 值 + // Verify first Volume value expectedFirstVolume := klines[start].Volume if data.Volume[0] != expectedFirstVolume { t.Errorf("First volume = %.2f, want %.2f", data.Volume[0], expectedFirstVolume) } - // 验证最后一个 Volume 值 + // Verify last Volume value expectedLastVolume := klines[tt.klineCount-1].Volume lastVolume := data.Volume[len(data.Volume)-1] if lastVolume != expectedLastVolume { @@ -98,7 +98,7 @@ func TestCalculateIntradaySeries_VolumeCollection(t *testing.T) { } } -// TestCalculateIntradaySeries_VolumeValues 测试 Volume 值的正确性 +// TestCalculateIntradaySeries_VolumeValues tests Volume value correctness func TestCalculateIntradaySeries_VolumeValues(t *testing.T) { klines := []Kline{ {Close: 100.0, Volume: 1000.0, High: 101.0, Low: 99.0, Open: 100.0}, @@ -128,7 +128,7 @@ func TestCalculateIntradaySeries_VolumeValues(t *testing.T) { } } -// TestCalculateIntradaySeries_ATR14 测试 ATR14 计算 +// TestCalculateIntradaySeries_ATR14 tests ATR14 calculation func TestCalculateIntradaySeries_ATR14(t *testing.T) { tests := []struct { name string @@ -137,27 +137,27 @@ func TestCalculateIntradaySeries_ATR14(t *testing.T) { expectNonZero bool }{ { - name: "足够数据 - 20个K线", + name: "Sufficient data - 20 K-lines", klineCount: 20, expectNonZero: true, }, { - name: "刚好15个K线(ATR14需要至少15个)", + name: "Exactly 15 K-lines (ATR14 requires at least 15)", klineCount: 15, expectNonZero: true, }, { - name: "数据不足 - 14个K线", + name: "Insufficient data - 14 K-lines", klineCount: 14, expectZero: true, }, { - name: "数据不足 - 10个K线", + name: "Insufficient data - 10 K-lines", klineCount: 10, expectZero: true, }, { - name: "数据不足 - 5个K线", + name: "Insufficient data - 5 K-lines", klineCount: 5, expectZero: true, }, @@ -183,7 +183,7 @@ func TestCalculateIntradaySeries_ATR14(t *testing.T) { } } -// TestCalculateATR 测试 ATR 计算函数 +// TestCalculateATR tests ATR calculation function func TestCalculateATR(t *testing.T) { tests := []struct { name string @@ -192,7 +192,7 @@ func TestCalculateATR(t *testing.T) { expectZero bool }{ { - name: "正常计算 - 足够数据", + name: "Normal calculation - sufficient data", klines: []Kline{ {High: 102.0, Low: 100.0, Close: 101.0}, {High: 103.0, Low: 101.0, Close: 102.0}, @@ -214,7 +214,7 @@ func TestCalculateATR(t *testing.T) { expectZero: false, }, { - name: "数据不足 - 等于period", + name: "Insufficient data - equal to period", klines: []Kline{ {High: 102.0, Low: 100.0, Close: 101.0}, {High: 103.0, Low: 101.0, Close: 102.0}, @@ -223,7 +223,7 @@ func TestCalculateATR(t *testing.T) { expectZero: true, }, { - name: "数据不足 - 少于period", + name: "Insufficient data - less than period", klines: []Kline{ {High: 102.0, Low: 100.0, Close: 101.0}, }, @@ -249,9 +249,9 @@ func TestCalculateATR(t *testing.T) { } } -// TestCalculateATR_TrueRange 测试 ATR 的 True Range 计算正确性 +// TestCalculateATR_TrueRange tests ATR True Range calculation correctness func TestCalculateATR_TrueRange(t *testing.T) { - // 创建一个简单的测试用例,手动计算期望的 ATR + // Create a simple test case, manually calculate expected ATR klines := []Kline{ {High: 50.0, Low: 48.0, Close: 49.0}, // TR = 2.0 {High: 51.0, Low: 49.0, Close: 50.0}, // TR = max(2.0, 2.0, 1.0) = 2.0 @@ -262,28 +262,28 @@ func TestCalculateATR_TrueRange(t *testing.T) { atr := calculateATR(klines, 3) - // 期望的计算: + // Expected calculation: // TR[1] = max(51-49, |51-49|, |49-49|) = 2.0 // TR[2] = max(52-50, |52-50|, |50-50|) = 2.0 // TR[3] = max(53-51, |53-51|, |51-51|) = 2.0 - // 初始 ATR = (2.0 + 2.0 + 2.0) / 3 = 2.0 + // Initial ATR = (2.0 + 2.0 + 2.0) / 3 = 2.0 // TR[4] = max(54-52, |54-52|, |52-52|) = 2.0 - // 平滑 ATR = (2.0*2 + 2.0) / 3 = 2.0 + // Smoothed ATR = (2.0*2 + 2.0) / 3 = 2.0 expectedATR := 2.0 - tolerance := 0.01 // 允许小的浮点误差 + tolerance := 0.01 // Allow small floating point error if math.Abs(atr-expectedATR) > tolerance { t.Errorf("calculateATR() = %.3f, want approximately %.3f", atr, expectedATR) } } -// TestCalculateIntradaySeries_ConsistencyWithOtherIndicators 测试 Volume 和其他指标的一致性 +// TestCalculateIntradaySeries_ConsistencyWithOtherIndicators tests Volume and other indicators consistency func TestCalculateIntradaySeries_ConsistencyWithOtherIndicators(t *testing.T) { klines := generateTestKlines(30) data := calculateIntradaySeries(klines) - // 所有数组应该存在 + // All arrays should exist if data.MidPrices == nil { t.Error("MidPrices should not be nil") } @@ -291,13 +291,13 @@ func TestCalculateIntradaySeries_ConsistencyWithOtherIndicators(t *testing.T) { t.Error("Volume should not be nil") } - // MidPrices 和 Volume 应该有相同的长度(都是最近10个) + // MidPrices and Volume should have the same length (both latest 10) if len(data.MidPrices) != len(data.Volume) { t.Errorf("MidPrices length (%d) should equal Volume length (%d)", len(data.MidPrices), len(data.Volume)) } - // 所有 Volume 值应该大于 0 + // All Volume values should be > 0 for i, vol := range data.Volume { if vol <= 0 { t.Errorf("Volume[%d] = %.2f, should be > 0", i, vol) @@ -305,7 +305,7 @@ func TestCalculateIntradaySeries_ConsistencyWithOtherIndicators(t *testing.T) { } } -// TestCalculateIntradaySeries_EmptyKlines 测试空 K线数据 +// TestCalculateIntradaySeries_EmptyKlines tests empty K-line data func TestCalculateIntradaySeries_EmptyKlines(t *testing.T) { klines := []Kline{} data := calculateIntradaySeries(klines) @@ -314,7 +314,7 @@ func TestCalculateIntradaySeries_EmptyKlines(t *testing.T) { t.Fatal("calculateIntradaySeries should not return nil for empty klines") } - // 所有切片应该为空 + // All slices should be empty if len(data.MidPrices) != 0 { t.Errorf("MidPrices length = %d, want 0", len(data.MidPrices)) } @@ -322,13 +322,13 @@ func TestCalculateIntradaySeries_EmptyKlines(t *testing.T) { t.Errorf("Volume length = %d, want 0", len(data.Volume)) } - // ATR14 应该为 0(数据不足) + // ATR14 should be 0 (insufficient data) if data.ATR14 != 0 { t.Errorf("ATR14 = %.3f, want 0", data.ATR14) } } -// TestCalculateIntradaySeries_VolumePrecision 测试 Volume 精度保持 +// TestCalculateIntradaySeries_VolumePrecision tests Volume precision preservation func TestCalculateIntradaySeries_VolumePrecision(t *testing.T) { klines := []Kline{ {Close: 100.0, Volume: 1234.5678, High: 101.0, Low: 99.0}, diff --git a/market/historical.go b/market/historical.go index e2563f01..9d401b5d 100644 --- a/market/historical.go +++ b/market/historical.go @@ -13,7 +13,7 @@ const ( binanceMaxKlineLimit = 1500 ) -// GetKlinesRange 拉取指定时间范围内的 K 线序列(闭区间),返回按时间升序排列的数据。 +// GetKlinesRange fetches K-line series within specified time range (closed interval), returns data sorted by time in ascending order. func GetKlinesRange(symbol string, timeframe string, start, end time.Time) ([]Kline, error) { symbol = Normalize(symbol) normTF, err := NormalizeTimeframe(timeframe) @@ -94,7 +94,7 @@ func GetKlinesRange(symbol string, timeframe string, start, end time.Time) ([]Kl last := batch[len(batch)-1] cursor = last.CloseTime + 1 - // 若返回数量少于请求上限,说明已到达末尾,可提前退出。 + // If returned quantity is less than request limit, reached the end, can exit early. if len(batch) < binanceMaxKlineLimit { break } diff --git a/market/monitor.go b/market/monitor.go index 717c96fe..d140da4c 100644 --- a/market/monitor.go +++ b/market/monitor.go @@ -15,24 +15,24 @@ type WSMonitor struct { symbols []string featuresMap sync.Map alertsChan chan Alert - klineDataMap3m sync.Map // 存储每个交易对的K线历史数据 - klineDataMap4h sync.Map // 存储每个交易对的K线历史数据 - tickerDataMap sync.Map // 存储每个交易对的ticker数据 + klineDataMap3m sync.Map // Store K-line historical data for each trading pair + klineDataMap4h sync.Map // Store K-line historical data for each trading pair + tickerDataMap sync.Map // Store ticker data for each trading pair batchSize int - filterSymbols sync.Map // 使用sync.Map来存储需要监控的币种和其状态 - symbolStats sync.Map // 存储币种统计信息 - FilterSymbol []string //经过筛选的币种 + filterSymbols sync.Map // Use sync.Map to store monitored coins and their status + symbolStats sync.Map // Store symbol statistics + FilterSymbol []string // Filtered symbols } type SymbolStats struct { LastActiveTime time.Time AlertCount int VolumeSpikeCount int LastAlertTime time.Time - Score float64 // 综合评分 + Score float64 // Composite score } var WSMonitorCli *WSMonitor -var subKlineTime = []string{"3m", "4h"} // 管理订阅流的K线周期 +var subKlineTime = []string{"3m", "4h"} // Manage K-line periods for subscription streams func NewWSMonitor(batchSize int) *WSMonitor { WSMonitorCli = &WSMonitor{ @@ -45,16 +45,16 @@ func NewWSMonitor(batchSize int) *WSMonitor { } func (m *WSMonitor) Initialize(coins []string) error { - log.Println("初始化WebSocket监控器...") - // 获取交易对信息 + log.Println("Initializing WebSocket monitor...") + // Get trading pair information apiClient := NewAPIClient() - // 如果不指定交易对,则使用market市场的所有交易对币种 + // If trading pairs are not specified, use all trading pairs from the market if len(coins) == 0 { exchangeInfo, err := apiClient.GetExchangeInfo() if err != nil { return err } - // 筛选永续合约交易对 --仅测试时使用 + // Filter perpetual contract trading pairs -- only use for testing //exchangeInfo.Symbols = exchangeInfo.Symbols[0:2] for _, symbol := range exchangeInfo.Symbols { if symbol.Status == "TRADING" && symbol.ContractType == "PERPETUAL" && strings.ToUpper(symbol.Symbol[len(symbol.Symbol)-4:]) == "USDT" { @@ -66,10 +66,10 @@ func (m *WSMonitor) Initialize(coins []string) error { m.symbols = coins } - log.Printf("找到 %d 个交易对", len(m.symbols)) - // 初始化历史数据 + log.Printf("Found %d trading pairs", len(m.symbols)) + // Initialize historical data if err := m.initializeHistoricalData(); err != nil { - log.Printf("初始化历史数据失败: %v", err) + log.Printf("Failed to initialize historical data: %v", err) } return nil @@ -79,7 +79,7 @@ func (m *WSMonitor) initializeHistoricalData() error { apiClient := NewAPIClient() var wg sync.WaitGroup - semaphore := make(chan struct{}, 5) // 限制并发数 + semaphore := make(chan struct{}, 5) // Limit concurrency for _, symbol := range m.symbols { wg.Add(1) @@ -89,25 +89,25 @@ func (m *WSMonitor) initializeHistoricalData() error { defer wg.Done() defer func() { <-semaphore }() - // 获取历史K线数据 + // Get historical K-line data klines, err := apiClient.GetKlines(s, "3m", 100) if err != nil { - log.Printf("获取 %s 历史数据失败: %v", s, err) + log.Printf("Failed to get %s historical data: %v", s, err) return } if len(klines) > 0 { m.klineDataMap3m.Store(s, klines) - log.Printf("已加载 %s 的历史K线数据-3m: %d 条", s, len(klines)) + log.Printf("Loaded %s historical K-line data-3m: %d entries", s, len(klines)) } - // 获取历史K线数据 + // Get historical K-line data klines4h, err := apiClient.GetKlines(s, "4h", 100) if err != nil { - log.Printf("获取 %s 历史数据失败: %v", s, err) + log.Printf("Failed to get %s historical data: %v", s, err) return } if len(klines4h) > 0 { m.klineDataMap4h.Store(s, klines4h) - log.Printf("已加载 %s 的历史K线数据-4h: %d 条", s, len(klines4h)) + log.Printf("Loaded %s historical K-line data-4h: %d entries", s, len(klines4h)) } }(symbol) } @@ -117,28 +117,28 @@ func (m *WSMonitor) initializeHistoricalData() error { } func (m *WSMonitor) Start(coins []string) { - log.Printf("启动WebSocket实时监控...") - // 初始化交易对 + log.Printf("Starting WebSocket real-time monitoring...") + // Initialize trading pairs err := m.Initialize(coins) if err != nil { - log.Printf("❌ 初始化币种失败: %v", err) + log.Printf("❌ Failed to initialize coins: %v", err) return } err = m.combinedClient.Connect() if err != nil { - log.Printf("❌ 批量订阅流失败: %v", err) + log.Printf("❌ Failed to batch subscribe to streams: %v", err) return } - // 订阅所有交易对 + // Subscribe to all trading pairs err = m.subscribeAll() if err != nil { - log.Printf("❌ 订阅币种交易对失败: %v", err) + log.Printf("❌ Failed to subscribe to coin trading pairs: %v", err) return } } -// subscribeSymbol 注册监听 +// subscribeSymbol registers listener func (m *WSMonitor) subscribeSymbol(symbol, st string) []string { var streams []string stream := fmt.Sprintf("%s@kline_%s", strings.ToLower(symbol), st) @@ -149,8 +149,8 @@ func (m *WSMonitor) subscribeSymbol(symbol, st string) []string { return streams } func (m *WSMonitor) subscribeAll() error { - // 执行批量订阅 - log.Println("开始订阅所有交易对...") + // Execute batch subscription + log.Println("Starting to subscribe to all trading pairs...") for _, symbol := range m.symbols { for _, st := range subKlineTime { m.subscribeSymbol(symbol, st) @@ -159,11 +159,11 @@ func (m *WSMonitor) subscribeAll() error { for _, st := range subKlineTime { err := m.combinedClient.BatchSubscribeKlines(m.symbols, st) if err != nil { - log.Printf("❌ 订阅 %s K线失败: %v", st, err) + log.Printf("❌ Failed to subscribe to %s K-line: %v", st, err) return err } } - log.Println("所有交易对订阅完成") + log.Println("All trading pair subscriptions completed") return nil } @@ -171,7 +171,7 @@ func (m *WSMonitor) handleKlineData(symbol string, ch <-chan []byte, _time strin for data := range ch { var klineData KlineWSData if err := json.Unmarshal(data, &klineData); err != nil { - log.Printf("解析Kline数据失败: %v", err) + log.Printf("Failed to parse Kline data: %v", err) continue } m.processKlineUpdate(symbol, klineData, _time) @@ -190,7 +190,7 @@ func (m *WSMonitor) getKlineDataMap(_time string) *sync.Map { return klineDataMap } func (m *WSMonitor) processKlineUpdate(symbol string, wsData KlineWSData, _time string) { - // 转换WebSocket数据为Kline结构 + // Convert WebSocket data to Kline structure kline := Kline{ OpenTime: wsData.Kline.StartTime, CloseTime: wsData.Kline.CloseTime, @@ -205,22 +205,22 @@ func (m *WSMonitor) processKlineUpdate(symbol string, wsData KlineWSData, _time kline.QuoteVolume, _ = parseFloat(wsData.Kline.QuoteVolume) kline.TakerBuyBaseVolume, _ = parseFloat(wsData.Kline.TakerBuyBaseVolume) kline.TakerBuyQuoteVolume, _ = parseFloat(wsData.Kline.TakerBuyQuoteVolume) - // 更新K线数据 + // Update K-line data var klineDataMap = m.getKlineDataMap(_time) value, exists := klineDataMap.Load(symbol) var klines []Kline if exists { klines = value.([]Kline) - // 检查是否是新的K线 + // Check if it's a new K-line if len(klines) > 0 && klines[len(klines)-1].OpenTime == kline.OpenTime { - // 更新当前K线 + // Update current K-line klines[len(klines)-1] = kline } else { - // 添加新K线 + // Add new K-line klines = append(klines, kline) - // 保持数据长度 + // Maintain data length if len(klines) > 100 { klines = klines[1:] } @@ -233,34 +233,34 @@ func (m *WSMonitor) processKlineUpdate(symbol string, wsData KlineWSData, _time } func (m *WSMonitor) GetCurrentKlines(symbol string, duration string) ([]Kline, error) { - // 对每一个进来的symbol检测是否存在内类 是否的话就订阅它 + // Check if each incoming symbol exists internally, if not subscribe to it value, exists := m.getKlineDataMap(duration).Load(symbol) if !exists { - // 如果Ws数据未初始化完成时,单独使用api获取 - 兼容性代码 (防止在未初始化完成是,已经有交易员运行) + // If WS data is not initialized, use API separately - compatibility code (prevents trader from running when not initialized) apiClient := NewAPIClient() klines, err := apiClient.GetKlines(symbol, duration, 100) if err != nil { - return nil, fmt.Errorf("获取%v分钟K线失败: %v", duration, err) + return nil, fmt.Errorf("Failed to get %v-minute K-line: %v", duration, err) } - // 动态缓存进缓存 + // Dynamically cache into cache m.getKlineDataMap(duration).Store(strings.ToUpper(symbol), klines) - // 订阅 WebSocket 流 + // Subscribe to WebSocket stream subStr := m.subscribeSymbol(symbol, duration) subErr := m.combinedClient.subscribeStreams(subStr) - log.Printf("动态订阅流: %v", subStr) + log.Printf("Dynamic subscription to stream: %v", subStr) if subErr != nil { - log.Printf("警告: 动态订阅%v分钟K线失败: %v (使用API数据)", duration, subErr) + log.Printf("Warning: Failed to dynamically subscribe to %v-minute K-line: %v (using API data)", duration, subErr) } - // ✅ FIX: 返回深拷贝而非引用 + // ✅ FIX: Return deep copy instead of reference result := make([]Kline, len(klines)) copy(result, klines) return result, nil } - // ✅ FIX: 返回深拷贝而非引用,避免并发竞态条件 + // ✅ FIX: Return deep copy instead of reference, avoid concurrent race conditions klines := value.([]Kline) result := make([]Kline, len(klines)) copy(result, klines) diff --git a/market/timeframe.go b/market/timeframe.go index 9424b49d..752e441e 100644 --- a/market/timeframe.go +++ b/market/timeframe.go @@ -7,7 +7,7 @@ import ( "time" ) -// supportedTimeframes 定义支持的时间周期与其对应的分钟数。 +// supportedTimeframes defines supported timeframes and their corresponding durations. var supportedTimeframes = map[string]time.Duration{ "1m": time.Minute, "3m": 3 * time.Minute, @@ -22,7 +22,7 @@ var supportedTimeframes = map[string]time.Duration{ "1d": 24 * time.Hour, } -// NormalizeTimeframe 规范化传入的时间周期字符串(大小写、不带空格),并校验是否受支持。 +// NormalizeTimeframe normalizes the incoming timeframe string (case-insensitive, no spaces), and validates if it's supported. func NormalizeTimeframe(tf string) (string, error) { trimmed := strings.TrimSpace(strings.ToLower(tf)) if trimmed == "" { @@ -34,7 +34,7 @@ func NormalizeTimeframe(tf string) (string, error) { return trimmed, nil } -// TFDuration 返回给定周期对应的时间长度。 +// TFDuration returns the time duration corresponding to the given timeframe. func TFDuration(tf string) (time.Duration, error) { norm, err := NormalizeTimeframe(tf) if err != nil { @@ -43,7 +43,7 @@ func TFDuration(tf string) (time.Duration, error) { return supportedTimeframes[norm], nil } -// MustNormalizeTimeframe 与 NormalizeTimeframe 类似,但在不受支持时 panic。 +// MustNormalizeTimeframe is similar to NormalizeTimeframe, but panics when unsupported. func MustNormalizeTimeframe(tf string) string { norm, err := NormalizeTimeframe(tf) if err != nil { @@ -52,7 +52,7 @@ func MustNormalizeTimeframe(tf string) string { return norm } -// SupportedTimeframes 返回所有受支持的时间周期(已排序的切片)。 +// SupportedTimeframes returns all supported timeframes (sorted slice). func SupportedTimeframes() []string { keys := make([]string, 0, len(supportedTimeframes)) for k := range supportedTimeframes { diff --git a/market/types.go b/market/types.go index 2c00e102..dee19c95 100644 --- a/market/types.go +++ b/market/types.go @@ -2,12 +2,12 @@ package market import "time" -// Data 市场数据结构 +// Data market data structure type Data struct { Symbol string CurrentPrice float64 - PriceChange1h float64 // 1小时价格变化百分比 - PriceChange4h float64 // 4小时价格变化百分比 + PriceChange1h float64 // 1-hour price change percentage + PriceChange4h float64 // 4-hour price change percentage CurrentEMA20 float64 CurrentMACD float64 CurrentRSI7 float64 @@ -15,30 +15,30 @@ type Data struct { FundingRate float64 IntradaySeries *IntradayData LongerTermContext *LongerTermData - // 多时间周期数据(新增) + // Multi-timeframe data (new) TimeframeData map[string]*TimeframeSeriesData `json:"timeframe_data,omitempty"` } -// TimeframeSeriesData 单个时间周期的序列数据 +// TimeframeSeriesData series data for a single timeframe type TimeframeSeriesData struct { - Timeframe string `json:"timeframe"` // 时间周期标识,如 "5m", "15m", "1h" - MidPrices []float64 `json:"mid_prices"` // 价格序列 - EMA20Values []float64 `json:"ema20_values"` // EMA20 序列 - EMA50Values []float64 `json:"ema50_values"` // EMA50 序列 - MACDValues []float64 `json:"macd_values"` // MACD 序列 - RSI7Values []float64 `json:"rsi7_values"` // RSI7 序列 - RSI14Values []float64 `json:"rsi14_values"` // RSI14 序列 - Volume []float64 `json:"volume"` // 成交量序列 + Timeframe string `json:"timeframe"` // Timeframe identifier, e.g. "5m", "15m", "1h" + MidPrices []float64 `json:"mid_prices"` // Price series + EMA20Values []float64 `json:"ema20_values"` // EMA20 series + EMA50Values []float64 `json:"ema50_values"` // EMA50 series + MACDValues []float64 `json:"macd_values"` // MACD series + RSI7Values []float64 `json:"rsi7_values"` // RSI7 series + RSI14Values []float64 `json:"rsi14_values"` // RSI14 series + Volume []float64 `json:"volume"` // Volume series ATR14 float64 `json:"atr14"` // ATR14 } -// OIData Open Interest数据 +// OIData Open Interest data type OIData struct { Latest float64 Average float64 } -// IntradayData 日内数据(3分钟间隔) +// IntradayData intraday data (3-minute interval) type IntradayData struct { MidPrices []float64 EMA20Values []float64 @@ -49,7 +49,7 @@ type IntradayData struct { ATR14 float64 } -// LongerTermData 长期数据(4小时时间框架) +// LongerTermData longer-term data (4-hour timeframe) type LongerTermData struct { EMA20 float64 EMA50 float64 @@ -61,7 +61,7 @@ type LongerTermData struct { RSI14Values []float64 } -// Binance API 响应结构 +// Binance API response structure type ExchangeInfo struct { Symbols []SymbolInfo `json:"symbols"` } @@ -105,7 +105,7 @@ type Ticker24hr struct { QuoteVolume string `json:"quoteVolume"` } -// 特征数据结构 +// SymbolFeatures feature data structure type SymbolFeatures struct { Symbol string `json:"symbol"` Timestamp time.Time `json:"timestamp"` @@ -126,7 +126,7 @@ type SymbolFeatures struct { PositionInRange float64 `json:"position_in_range"` } -// 警报数据结构 +// Alert alert data structure type Alert struct { Type string `json:"type"` Symbol string `json:"symbol"` @@ -150,10 +150,10 @@ type AlertThresholds struct { RSIOversold float64 `json:"rsi_oversold"` } type CleanupConfig struct { - InactiveTimeout time.Duration `json:"inactive_timeout"` // 不活跃超时时间 - MinScoreThreshold float64 `json:"min_score_threshold"` // 最低评分阈值 - NoAlertTimeout time.Duration `json:"no_alert_timeout"` // 无警报超时时间 - CheckInterval time.Duration `json:"check_interval"` // 检查间隔 + InactiveTimeout time.Duration `json:"inactive_timeout"` // Inactive timeout duration + MinScoreThreshold float64 `json:"min_score_threshold"` // Minimum score threshold + NoAlertTimeout time.Duration `json:"no_alert_timeout"` // No alert timeout duration + CheckInterval time.Duration `json:"check_interval"` // Check interval } var config = Config{ diff --git a/market/websocket_client.go b/market/websocket_client.go index ce151691..d51d5e91 100644 --- a/market/websocket_client.go +++ b/market/websocket_client.go @@ -83,16 +83,16 @@ func (w *WSClient) Connect() error { conn, _, err := dialer.Dial("wss://ws-fapi.binance.com/ws-fapi/v1", nil) if err != nil { - return fmt.Errorf("WebSocket连接失败: %v", err) + return fmt.Errorf("WebSocket connection failed: %v", err) } w.mu.Lock() w.conn = conn w.mu.Unlock() - log.Println("WebSocket连接成功") + log.Println("WebSocket connected successfully") - // 启动消息读取循环 + // Start message reading loop go w.readMessages() return nil @@ -124,7 +124,7 @@ func (w *WSClient) subscribe(stream string) error { defer w.mu.RUnlock() if w.conn == nil { - return fmt.Errorf("WebSocket未连接") + return fmt.Errorf("WebSocket not connected") } err := w.conn.WriteJSON(subscribeMsg) @@ -132,7 +132,7 @@ func (w *WSClient) subscribe(stream string) error { return err } - log.Printf("订阅流: %s", stream) + log.Printf("Subscribing to stream: %s", stream) return nil } @@ -153,7 +153,7 @@ func (w *WSClient) readMessages() { _, message, err := conn.ReadMessage() if err != nil { - log.Printf("读取WebSocket消息失败: %v", err) + log.Printf("Failed to read WebSocket message: %v", err) w.handleReconnect() return } @@ -166,7 +166,7 @@ func (w *WSClient) readMessages() { func (w *WSClient) handleMessage(message []byte) { var wsMsg WSMessage if err := json.Unmarshal(message, &wsMsg); err != nil { - // 可能是其他格式的消息 + // Might be a different message format return } @@ -178,7 +178,7 @@ func (w *WSClient) handleMessage(message []byte) { select { case ch <- wsMsg.Data: default: - log.Printf("订阅者通道已满: %s", wsMsg.Stream) + log.Printf("Subscriber channel is full: %s", wsMsg.Stream) } } } @@ -188,11 +188,11 @@ func (w *WSClient) handleReconnect() { return } - log.Println("尝试重新连接...") + log.Println("Attempting to reconnect...") time.Sleep(3 * time.Second) if err := w.Connect(); err != nil { - log.Printf("重新连接失败: %v", err) + log.Printf("Reconnection failed: %v", err) go w.handleReconnect() } } @@ -223,7 +223,7 @@ func (w *WSClient) Close() { w.conn = nil } - // 关闭所有订阅者通道 + // Close all subscriber channels for stream, ch := range w.subscribers { close(ch) delete(w.subscribers, stream) diff --git a/mcp/client.go b/mcp/client.go index ab34f402..f696a87f 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -28,65 +28,65 @@ var ( "connection refused", "temporary failure", "no such host", - "stream error", // HTTP/2 stream 错误 - "INTERNAL_ERROR", // 服务端内部错误 + "stream error", // HTTP/2 stream error + "INTERNAL_ERROR", // Server internal error } ) -// Client AI API配置 +// Client AI API configuration type Client struct { Provider string APIKey string BaseURL string Model string - UseFullURL bool // 是否使用完整URL(不添加/chat/completions) - MaxTokens int // AI响应的最大token数 + UseFullURL bool // Whether to use full URL (without appending /chat/completions) + MaxTokens int // Maximum tokens for AI response httpClient *http.Client - logger Logger // 日志器(可替换) - config *Config // 配置对象(保存所有配置) + logger Logger // Logger (replaceable) + config *Config // Config object (stores all configurations) - // hooks 用于实现动态分派(多态) - // 当 DeepSeekClient 嵌入 Client 时,hooks 指向 DeepSeekClient - // 这样 call() 中调用的方法会自动分派到子类重写的版本 + // hooks are used to implement dynamic dispatch (polymorphism) + // When DeepSeekClient embeds Client, hooks point to DeepSeekClient + // This way methods called in call() are automatically dispatched to the overridden version in subclass hooks clientHooks } -// New 创建默认客户端(向前兼容) +// New creates default client (backward compatible) // -// Deprecated: 推荐使用 NewClient(...opts) 以获得更好的灵活性 +// Deprecated: Recommend using NewClient(...opts) for better flexibility func New() AIClient { return NewClient() } -// NewClient 创建客户端(支持选项模式) +// NewClient creates client (supports options pattern) // -// 使用示例: -// // 基础用法(向前兼容) +// Usage examples: +// // Basic usage (backward compatible) // client := mcp.NewClient() // -// // 自定义日志 +// // Custom logger // client := mcp.NewClient(mcp.WithLogger(customLogger)) // -// // 自定义超时 +// // Custom timeout // client := mcp.NewClient(mcp.WithTimeout(60*time.Second)) // -// // 组合多个选项 +// // Combine multiple options // client := mcp.NewClient( // mcp.WithDeepSeekConfig("sk-xxx"), // mcp.WithLogger(customLogger), // mcp.WithTimeout(60*time.Second), // ) func NewClient(opts ...ClientOption) AIClient { - // 1. 创建默认配置 + // 1. Create default config cfg := DefaultConfig() - // 2. 应用用户选项 + // 2. Apply user options for _, opt := range opts { opt(cfg) } - // 3. 创建客户端实例 + // 3. Create client instance client := &Client{ Provider: cfg.Provider, APIKey: cfg.APIKey, @@ -99,25 +99,25 @@ func NewClient(opts ...ClientOption) AIClient { config: cfg, } - // 4. 设置默认 Provider(如果未设置) + // 4. Set default Provider (if not set) if client.Provider == "" { client.Provider = ProviderDeepSeek client.BaseURL = DefaultDeepSeekBaseURL client.Model = DefaultDeepSeekModel } - // 5. 设置 hooks 指向自己 + // 5. Set hooks to point to self client.hooks = client return client } -// SetCustomAPI 设置自定义OpenAI兼容API +// SetCustomAPI sets custom OpenAI-compatible API func (client *Client) SetAPIKey(apiKey, apiURL, customModel string) { client.Provider = ProviderCustom client.APIKey = apiKey - // 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions) + // Check if URL ends with #, if so use full URL (without appending /chat/completions) if strings.HasSuffix(apiURL, "#") { client.BaseURL = strings.TrimSuffix(apiURL, "#") client.UseFullURL = true @@ -133,45 +133,45 @@ func (client *Client) SetTimeout(timeout time.Duration) { client.httpClient.Timeout = timeout } -// CallWithMessages 模板方法 - 固定的重试流程(不可重写) +// CallWithMessages template method - fixed retry flow (cannot be overridden) func (client *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) { if client.APIKey == "" { - return "", fmt.Errorf("AI API密钥未设置,请先调用 SetAPIKey") + return "", fmt.Errorf("AI API key not set, please call SetAPIKey first") } - // 固定的重试流程 + // Fixed retry flow var lastErr error maxRetries := client.config.MaxRetries for attempt := 1; attempt <= maxRetries; attempt++ { if attempt > 1 { - client.logger.Warnf("⚠️ AI API调用失败,正在重试 (%d/%d)...", attempt, maxRetries) + client.logger.Warnf("⚠️ AI API call failed, retrying (%d/%d)...", attempt, maxRetries) } - // 调用固定的单次调用流程 + // Call the fixed single-call flow result, err := client.hooks.call(systemPrompt, userPrompt) if err == nil { if attempt > 1 { - client.logger.Infof("✓ AI API重试成功") + client.logger.Infof("✓ AI API retry succeeded") } return result, nil } lastErr = err - // 通过 hooks 判断是否可重试(支持子类自定义重试策略) + // Check if error is retryable via hooks (supports custom retry strategy in subclass) if !client.hooks.isRetryableError(err) { return "", err } - // 重试前等待 + // Wait before retry if attempt < maxRetries { waitTime := client.config.RetryWaitBase * time.Duration(attempt) - client.logger.Infof("⏳ 等待%v后重试...", waitTime) + client.logger.Infof("⏳ Waiting %v before retry...", waitTime) time.Sleep(waitTime) } } - return "", fmt.Errorf("重试%d次后仍然失败: %w", maxRetries, lastErr) + return "", fmt.Errorf("still failed after %d retries: %w", maxRetries, lastErr) } func (client *Client) setAuthHeader(reqHeader http.Header) { @@ -179,27 +179,27 @@ func (client *Client) setAuthHeader(reqHeader http.Header) { } func (client *Client) buildMCPRequestBody(systemPrompt, userPrompt string) map[string]any { - // 构建 messages 数组 + // Build messages array messages := []map[string]string{} - // 如果有 system prompt,添加 system message + // If system prompt exists, add system message if systemPrompt != "" { messages = append(messages, map[string]string{ "role": "system", "content": systemPrompt, }) } - // 添加 user message + // Add user message messages = append(messages, map[string]string{ "role": "user", "content": userPrompt, }) - // 构建请求体 + // Build request body requestBody := map[string]interface{}{ "model": client.Model, "messages": messages, - "temperature": client.config.Temperature, // 使用配置的 temperature + "temperature": client.config.Temperature, // Use configured temperature "max_tokens": client.MaxTokens, } return requestBody @@ -209,7 +209,7 @@ func (client *Client) buildMCPRequestBody(systemPrompt, userPrompt string) map[s func (client *Client) marshalRequestBody(requestBody map[string]any) ([]byte, error) { jsonData, err := json.Marshal(requestBody) if err != nil { - return nil, fmt.Errorf("序列化请求失败: %w", err) + return nil, fmt.Errorf("failed to serialize request: %w", err) } return jsonData, nil } @@ -224,11 +224,11 @@ func (client *Client) parseMCPResponse(body []byte) (string, error) { } if err := json.Unmarshal(body, &result); err != nil { - return "", fmt.Errorf("解析响应失败: %w", err) + return "", fmt.Errorf("failed to parse response: %w", err) } if len(result.Choices) == 0 { - return "", fmt.Errorf("API返回空响应") + return "", fmt.Errorf("API returned empty response") } return result.Choices[0].Message.Content, nil @@ -250,59 +250,59 @@ func (client *Client) buildRequest(url string, jsonData []byte) (*http.Request, req.Header.Set("Content-Type", "application/json") - // 通过 hooks 设置认证头(支持子类重写) + // Set auth header via hooks (supports overriding in subclass) client.hooks.setAuthHeader(req.Header) return req, nil } -// call 单次调用AI API(固定流程,不可重写) +// call single AI API call (fixed flow, cannot be overridden) func (client *Client) call(systemPrompt, userPrompt string) (string, error) { - // 打印当前 AI 配置 + // Print current AI configuration client.logger.Infof("📡 [%s] Request AI Server: BaseURL: %s", client.String(), client.BaseURL) client.logger.Debugf("[%s] UseFullURL: %v", client.String(), client.UseFullURL) if len(client.APIKey) > 8 { client.logger.Debugf("[%s] API Key: %s...%s", client.String(), client.APIKey[:4], client.APIKey[len(client.APIKey)-4:]) } - // Step 1: 构建请求体(通过 hooks 实现动态分派) + // Step 1: Build request body (via hooks for dynamic dispatch) requestBody := client.hooks.buildMCPRequestBody(systemPrompt, userPrompt) - // Step 2: 序列化请求体(通过 hooks 实现动态分派) + // Step 2: Serialize request body (via hooks for dynamic dispatch) jsonData, err := client.hooks.marshalRequestBody(requestBody) if err != nil { return "", err } - // Step 3: 构建 URL(通过 hooks 实现动态分派) + // Step 3: Build URL (via hooks for dynamic dispatch) url := client.hooks.buildUrl() - client.logger.Infof("📡 [MCP %s] 请求 URL: %s", client.String(), url) + client.logger.Infof("📡 [MCP %s] Request URL: %s", client.String(), url) - // Step 4: 创建 HTTP 请求(固定逻辑) + // Step 4: Create HTTP request (fixed logic) req, err := client.hooks.buildRequest(url, jsonData) if err != nil { - return "", fmt.Errorf("创建请求失败: %w", err) + return "", fmt.Errorf("failed to create request: %w", err) } - // Step 5: 发送 HTTP 请求(固定逻辑) + // Step 5: Send HTTP request (fixed logic) resp, err := client.httpClient.Do(req) if err != nil { - return "", fmt.Errorf("发送请求失败: %w", err) + return "", fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() - // Step 6: 读取响应体(固定逻辑) + // Step 6: Read response body (fixed logic) body, err := io.ReadAll(resp.Body) if err != nil { - return "", fmt.Errorf("读取响应失败: %w", err) + return "", fmt.Errorf("failed to read response: %w", err) } - // Step 7: 检查 HTTP 状态码(固定逻辑) + // Step 7: Check HTTP status code (fixed logic) if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("API返回错误 (status %d): %s", resp.StatusCode, string(body)) + return "", fmt.Errorf("API returned error (status %d): %s", resp.StatusCode, string(body)) } - // Step 8: 解析响应(通过 hooks 实现动态分派) + // Step 8: Parse response (via hooks for dynamic dispatch) result, err := client.hooks.parseMCPResponse(body) if err != nil { return "", fmt.Errorf("fail to parse AI server response: %w", err) @@ -316,10 +316,10 @@ func (client *Client) String() string { client.Provider, client.Model) } -// isRetryableError 判断错误是否可重试(网络错误、超时等) +// isRetryableError determines if error is retryable (network errors, timeouts, etc.) func (client *Client) isRetryableError(err error) bool { errStr := err.Error() - // 网络错误、超时、EOF等可以重试 + // Network errors, timeouts, EOF, etc. can be retried for _, retryable := range client.config.RetryableErrors { if strings.Contains(errStr, retryable) { return true @@ -329,18 +329,18 @@ func (client *Client) isRetryableError(err error) bool { } // ============================================================ -// 构建器模式 API(高级功能) +// Builder Pattern API (Advanced Features) // ============================================================ -// CallWithRequest 使用 Request 对象调用 AI API(支持高级功能) +// CallWithRequest calls AI API using Request object (supports advanced features) // -// 此方法支持: -// - 多轮对话历史 -// - 精细参数控制(temperature、top_p、penalties 等) +// This method supports: +// - Multi-turn conversation history +// - Fine-grained parameter control (temperature, top_p, penalties, etc.) // - Function Calling / Tools -// - 流式响应(未来支持) +// - Streaming response (future support) // -// 使用示例: +// Usage example: // request := NewRequestBuilder(). // WithSystemPrompt("You are helpful"). // WithUserPrompt("Hello"). @@ -349,93 +349,93 @@ func (client *Client) isRetryableError(err error) bool { // result, err := client.CallWithRequest(request) func (client *Client) CallWithRequest(req *Request) (string, error) { if client.APIKey == "" { - return "", fmt.Errorf("AI API密钥未设置,请先调用 SetAPIKey") + return "", fmt.Errorf("AI API key not set, please call SetAPIKey first") } - // 如果 Request 中没有设置 Model,使用 Client 的 Model + // If Model is not set in Request, use Client's Model if req.Model == "" { req.Model = client.Model } - // 固定的重试流程 + // Fixed retry flow var lastErr error maxRetries := client.config.MaxRetries for attempt := 1; attempt <= maxRetries; attempt++ { if attempt > 1 { - client.logger.Warnf("⚠️ AI API调用失败,正在重试 (%d/%d)...", attempt, maxRetries) + client.logger.Warnf("⚠️ AI API call failed, retrying (%d/%d)...", attempt, maxRetries) } - // 调用单次请求 + // Call single request result, err := client.callWithRequest(req) if err == nil { if attempt > 1 { - client.logger.Infof("✓ AI API重试成功") + client.logger.Infof("✓ AI API retry succeeded") } return result, nil } lastErr = err - // 判断是否可重试 + // Check if error is retryable if !client.hooks.isRetryableError(err) { return "", err } - // 重试前等待 + // Wait before retry if attempt < maxRetries { waitTime := client.config.RetryWaitBase * time.Duration(attempt) - client.logger.Infof("⏳ 等待%v后重试...", waitTime) + client.logger.Infof("⏳ Waiting %v before retry...", waitTime) time.Sleep(waitTime) } } - return "", fmt.Errorf("重试%d次后仍然失败: %w", maxRetries, lastErr) + return "", fmt.Errorf("still failed after %d retries: %w", maxRetries, lastErr) } -// callWithRequest 单次调用 AI API(使用 Request 对象) +// callWithRequest single AI API call (using Request object) func (client *Client) callWithRequest(req *Request) (string, error) { - // 打印当前 AI 配置 + // Print current AI configuration client.logger.Infof("📡 [%s] Request AI Server with Builder: BaseURL: %s", client.String(), client.BaseURL) client.logger.Debugf("[%s] Messages count: %d", client.String(), len(req.Messages)) - // 构建请求体(从 Request 对象) + // Build request body (from Request object) requestBody := client.buildRequestBodyFromRequest(req) - // 序列化请求体 + // Serialize request body jsonData, err := client.hooks.marshalRequestBody(requestBody) if err != nil { return "", err } - // 构建 URL + // Build URL url := client.hooks.buildUrl() - client.logger.Infof("📡 [MCP %s] 请求 URL: %s", client.String(), url) + client.logger.Infof("📡 [MCP %s] Request URL: %s", client.String(), url) - // 创建 HTTP 请求 + // Create HTTP request httpReq, err := client.hooks.buildRequest(url, jsonData) if err != nil { - return "", fmt.Errorf("创建请求失败: %w", err) + return "", fmt.Errorf("failed to create request: %w", err) } - // 发送 HTTP 请求 + // Send HTTP request resp, err := client.httpClient.Do(httpReq) if err != nil { - return "", fmt.Errorf("发送请求失败: %w", err) + return "", fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() - // 读取响应体 + // Read response body body, err := io.ReadAll(resp.Body) if err != nil { - return "", fmt.Errorf("读取响应失败: %w", err) + return "", fmt.Errorf("failed to read response: %w", err) } - // 检查 HTTP 状态码 + // Check HTTP status code if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("API返回错误 (status %d): %s", resp.StatusCode, string(body)) + return "", fmt.Errorf("API returned error (status %d): %s", resp.StatusCode, string(body)) } - // 解析响应 + // Parse response result, err := client.hooks.parseMCPResponse(body) if err != nil { return "", fmt.Errorf("fail to parse AI server response: %w", err) @@ -444,9 +444,9 @@ func (client *Client) callWithRequest(req *Request) (string, error) { return result, nil } -// buildRequestBodyFromRequest 从 Request 对象构建请求体 +// buildRequestBodyFromRequest builds request body from Request object func (client *Client) buildRequestBodyFromRequest(req *Request) map[string]any { - // 转换 Message 为 API 格式 + // Convert Message to API format messages := make([]map[string]string, 0, len(req.Messages)) for _, msg := range req.Messages { messages = append(messages, map[string]string{ @@ -455,24 +455,24 @@ func (client *Client) buildRequestBodyFromRequest(req *Request) map[string]any { }) } - // 构建基础请求体 + // Build basic request body requestBody := map[string]interface{}{ "model": req.Model, "messages": messages, } - // 添加可选参数(只添加非 nil 的参数) + // Add optional parameters (only add non-nil parameters) if req.Temperature != nil { requestBody["temperature"] = *req.Temperature } else { - // 如果 Request 中没有设置,使用 Client 的配置 + // If not set in Request, use Client's configuration requestBody["temperature"] = client.config.Temperature } if req.MaxTokens != nil { requestBody["max_tokens"] = *req.MaxTokens } else { - // 如果 Request 中没有设置,使用 Client 的 MaxTokens + // If not set in Request, use Client's MaxTokens requestBody["max_tokens"] = client.MaxTokens } diff --git a/mcp/client_test.go b/mcp/client_test.go index 4a0e9e46..4af38703 100644 --- a/mcp/client_test.go +++ b/mcp/client_test.go @@ -8,7 +8,7 @@ import ( ) // ============================================================ -// 测试 Client 创建和配置 +// Test Client Creation and Configuration // ============================================================ func TestNewClient_Default(t *testing.T) { @@ -72,7 +72,7 @@ func TestNewClient_WithOptions(t *testing.T) { } // ============================================================ -// 测试 CallWithMessages +// Test CallWithMessages // ============================================================ func TestClient_CallWithMessages_Success(t *testing.T) { @@ -97,7 +97,7 @@ func TestClient_CallWithMessages_Success(t *testing.T) { t.Errorf("expected 'AI response content', got '%s'", result) } - // 验证请求 + // Verify request requests := mockHTTP.GetRequests() if len(requests) != 1 { t.Errorf("expected 1 request, got %d", len(requests)) @@ -123,7 +123,7 @@ func TestClient_CallWithMessages_NoAPIKey(t *testing.T) { t.Error("should error when API key is not set") } - if err.Error() != "AI API密钥未设置,请先调用 SetAPIKey" { + if err.Error() != "AI API key not set, please call SetAPIKey first" { t.Errorf("unexpected error message: %v", err) } } @@ -147,14 +147,14 @@ func TestClient_CallWithMessages_HTTPError(t *testing.T) { } // ============================================================ -// 测试重试逻辑 +// Test Retry Logic // ============================================================ func TestClient_Retry_Success(t *testing.T) { mockHTTP := NewMockHTTPClient() mockLogger := NewMockLogger() - // 模拟:第一次失败,第二次成功 + // Simulate: first call fails, second call succeeds callCount := 0 mockHTTP.ResponseFunc = func(req *http.Request) (*http.Response, error) { callCount++ @@ -174,40 +174,40 @@ func TestClient_Retry_Success(t *testing.T) { WithMaxRetries(3), ) - // 由于我们的 client 使用 hooks.call,需要特殊处理 - // 这里我们测试的是 CallWithMessages 会调用 retry 逻辑 + // Since our client uses hooks.call, need special handling + // Here we test that CallWithMessages will invoke retry logic c := client.(*Client) - // 临时修改重试等待时间为 0 以加速测试 + // Temporarily modify retry wait time to 0 to speed up test oldRetries := MaxRetryTimes MaxRetryTimes = 3 defer func() { MaxRetryTimes = oldRetries }() _, err := c.CallWithMessages("system", "user") - // 第一次失败(connection reset),第二次成功,但是响应格式不对,会失败 - // 但至少验证了重试逻辑被触发 + // First fails (connection reset), second succeeds, but response format is wrong, will fail + // But at least verify retry logic was triggered if callCount < 2 { t.Errorf("should retry, got %d calls", callCount) } - // 检查日志中是否有重试信息 + // Check if there's retry information in logs logs := mockLogger.GetLogsByLevel("WARN") hasRetryLog := false for _, log := range logs { - if log.Message == "⚠️ AI API调用失败,正在重试 (2/3)..." { + if log.Message == "⚠️ AI API call failed, retrying (2/3)..." { hasRetryLog = true break } } if !hasRetryLog && callCount >= 2 { - // 如果确实重试了,应该有警告日志 - // 但由于我们的测试设置,可能不会触发,所以这里只是检查 + // If retry was indeed attempted, there should be warning logs + // But due to our test setup, it may not trigger, so just check here t.Log("Retry was attempted") } - _ = err // 忽略错误,我们主要测试重试逻辑被触发 + _ = err // Ignore error, we mainly test retry logic was triggered } func TestClient_Retry_NonRetryableError(t *testing.T) { @@ -227,7 +227,7 @@ func TestClient_Retry_NonRetryableError(t *testing.T) { t.Error("should error") } - // 验证没有重试(因为 400 不是可重试错误) + // Verify no retry (because 400 is not a retryable error) requests := mockHTTP.GetRequests() if len(requests) != 1 { t.Errorf("should not retry for 400 error, got %d requests", len(requests)) @@ -235,7 +235,7 @@ func TestClient_Retry_NonRetryableError(t *testing.T) { } // ============================================================ -// 测试钩子方法 +// Test Hook Methods // ============================================================ func TestClient_BuildMCPRequestBody(t *testing.T) { @@ -368,7 +368,7 @@ func TestClient_IsRetryableError(t *testing.T) { } // ============================================================ -// 测试 SetTimeout +// Test SetTimeout // ============================================================ func TestClient_SetTimeout(t *testing.T) { @@ -384,7 +384,7 @@ func TestClient_SetTimeout(t *testing.T) { } // ============================================================ -// 测试 String 方法 +// Test String Method // ============================================================ func TestClient_String(t *testing.T) { @@ -404,7 +404,7 @@ func TestClient_String(t *testing.T) { } } -// 辅助函数 +// Helper function func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && findSubstring(s, substr)) } diff --git a/mcp/config.go b/mcp/config.go index d235a28d..1aebadd2 100644 --- a/mcp/config.go +++ b/mcp/config.go @@ -9,36 +9,36 @@ import ( "nofx/logger" ) -// Config 客户端配置(集中管理所有配置) +// Config client configuration (centralized management of all configurations) type Config struct { - // Provider 配置 + // Provider configuration Provider string APIKey string BaseURL string Model string - // 行为配置 + // Behavior configuration MaxTokens int Temperature float64 UseFullURL bool - // 重试配置 + // Retry configuration MaxRetries int RetryWaitBase time.Duration RetryableErrors []string - // 超时配置 + // Timeout configuration Timeout time.Duration - // 依赖注入 + // Dependency injection Logger Logger HTTPClient *http.Client } -// DefaultConfig 返回默认配置 +// DefaultConfig returns default configuration func DefaultConfig() *Config { return &Config{ - // 默认值 + // Default values MaxTokens: getEnvInt("AI_MAX_TOKENS", 2000), Temperature: MCPClientTemperature, MaxRetries: MaxRetryTimes, @@ -46,13 +46,13 @@ func DefaultConfig() *Config { Timeout: DefaultTimeout, RetryableErrors: retryableErrors, - // 默认依赖(使用全局 logger) + // Default dependencies (use global logger) Logger: logger.NewMCPLogger(), HTTPClient: &http.Client{Timeout: DefaultTimeout}, } } -// getEnvInt 从环境变量读取整数,失败则返回默认值 +// getEnvInt reads integer from environment variable, returns default value if failed func getEnvInt(key string, defaultValue int) int { if val := os.Getenv(key); val != "" { if parsed, err := strconv.Atoi(val); err == nil && parsed > 0 { @@ -62,7 +62,7 @@ func getEnvInt(key string, defaultValue int) int { return defaultValue } -// getEnvString 从环境变量读取字符串,为空则返回默认值 +// getEnvString reads string from environment variable, returns default value if empty func getEnvString(key string, defaultValue string) string { if val := os.Getenv(key); val != "" { return val diff --git a/mcp/config_usage_test.go b/mcp/config_usage_test.go index 0972cb20..c0e984f8 100644 --- a/mcp/config_usage_test.go +++ b/mcp/config_usage_test.go @@ -11,49 +11,49 @@ import ( ) // ============================================================ -// 测试 Config 字段真正被使用(验证问题2修复) +// Test Config Fields Are Actually Used (Verify Issue 2 Fix) // ============================================================ func TestConfig_MaxRetries_IsUsed(t *testing.T) { mockHTTP := NewMockHTTPClient() mockLogger := NewMockLogger() - // 设置 HTTP 客户端返回错误 + // Set HTTP client to return error callCount := 0 mockHTTP.ResponseFunc = func(req *http.Request) (*http.Response, error) { callCount++ return nil, errors.New("connection reset") } - // 创建客户端并设置自定义重试次数为 5 + // Create client and set custom retry count to 5 client := NewClient( WithHTTPClient(mockHTTP.ToHTTPClient()), WithLogger(mockLogger), WithAPIKey("sk-test-key"), - WithMaxRetries(5), // ✅ 设置重试5次 + WithMaxRetries(5), // Set to retry 5 times ) - // 调用 API(应该失败) + // Call API (should fail) _, err := client.CallWithMessages("system", "user") if err == nil { t.Error("should error") } - // 验证确实重试了5次(而不是默认的3次) + // Verify indeed retried 5 times (not the default 3 times) if callCount != 5 { t.Errorf("expected 5 retry attempts (from WithMaxRetries(5)), got %d", callCount) } - // 验证日志中显示正确的重试次数 + // Verify logs show correct retry count logs := mockLogger.GetLogsByLevel("WARN") - expectedWarningCount := 4 // 第2、3、4、5次重试时会打印警告 + expectedWarningCount := 4 // Warnings will be printed on 2nd, 3rd, 4th, 5th retry actualWarningCount := 0 for _, log := range logs { - if log.Message == "⚠️ AI API调用失败,正在重试 (2/5)..." || - log.Message == "⚠️ AI API调用失败,正在重试 (3/5)..." || - log.Message == "⚠️ AI API调用失败,正在重试 (4/5)..." || - log.Message == "⚠️ AI API调用失败,正在重试 (5/5)..." { + if log.Message == "⚠️ AI API call failed, retrying (2/5)..." || + log.Message == "⚠️ AI API call failed, retrying (3/5)..." || + log.Message == "⚠️ AI API call failed, retrying (4/5)..." || + log.Message == "⚠️ AI API call failed, retrying (5/5)..." { actualWarningCount++ } } @@ -73,20 +73,20 @@ func TestConfig_Temperature_IsUsed(t *testing.T) { customTemperature := 0.8 - // 创建客户端并设置自定义 temperature + // Create client and set custom temperature client := NewClient( WithHTTPClient(mockHTTP.ToHTTPClient()), WithLogger(mockLogger), WithAPIKey("sk-test-key"), - WithTemperature(customTemperature), // ✅ 设置自定义 temperature + WithTemperature(customTemperature), // Set custom temperature ) c := client.(*Client) - // 构建请求体 + // Build request body requestBody := c.buildMCPRequestBody("system", "user") - // 验证 temperature 字段 + // Verify temperature field temp, ok := requestBody["temperature"].(float64) if !ok { t.Fatal("temperature should be float64") @@ -96,26 +96,26 @@ func TestConfig_Temperature_IsUsed(t *testing.T) { t.Errorf("expected temperature %f (from WithTemperature), got %f", customTemperature, temp) } - // 也可以通过实际 HTTP 请求验证 + // Can also verify through actual HTTP request _, err := client.CallWithMessages("system", "user") if err != nil { t.Fatalf("should not error: %v", err) } - // 检查发送的请求体 + // Check sent request body requests := mockHTTP.GetRequests() if len(requests) != 1 { t.Fatalf("expected 1 request, got %d", len(requests)) } - // 解析请求体 + // Parse request body var body map[string]interface{} decoder := json.NewDecoder(requests[0].Body) if err := decoder.Decode(&body); err != nil { t.Fatalf("failed to decode request body: %v", err) } - // 验证 temperature + // Verify temperature if body["temperature"] != customTemperature { t.Errorf("expected temperature %f in HTTP request, got %v", customTemperature, body["temperature"]) } @@ -125,18 +125,18 @@ func TestConfig_RetryWaitBase_IsUsed(t *testing.T) { mockHTTP := NewMockHTTPClient() mockLogger := NewMockLogger() - // 设置成功响应(在 ResponseFunc 之前) + // Set success response (before ResponseFunc) mockHTTP.SetSuccessResponse("AI response") - // 设置 HTTP 客户端前2次返回错误,第3次成功 + // Set HTTP client to return error first 2 times, success on 3rd time callCount := 0 - successResponse := mockHTTP.Response // 保存成功响应字符串 + successResponse := mockHTTP.Response // Save success response string mockHTTP.ResponseFunc = func(req *http.Request) (*http.Response, error) { callCount++ if callCount <= 2 { return nil, errors.New("timeout exceeded") } - // 第3次返回成功响应 + // 3rd time return success response return &http.Response{ StatusCode: 200, Body: io.NopCloser(bytes.NewBufferString(successResponse)), @@ -144,27 +144,27 @@ func TestConfig_RetryWaitBase_IsUsed(t *testing.T) { }, nil } - // 设置自定义重试等待基数为 1 秒(而不是默认的 2 秒) + // Set custom retry wait base to 1 second (instead of default 2 seconds) customWaitBase := 1 * time.Second client := NewClient( WithHTTPClient(mockHTTP.ToHTTPClient()), WithLogger(mockLogger), WithAPIKey("sk-test-key"), - WithRetryWaitBase(customWaitBase), // ✅ 设置自定义等待时间 + WithRetryWaitBase(customWaitBase), // Set custom wait time WithMaxRetries(3), ) - // 记录开始时间 + // Record start time start := time.Now() - // 调用 API + // Call API _, err := client.CallWithMessages("system", "user") - // 记录结束时间 + // Record end time elapsed := time.Since(start) - // 第3次成功,但前面失败了2次 + // 3rd time succeeds, but failed 2 times before if err != nil { t.Fatalf("should succeed on 3rd attempt, got error: %v", err) } @@ -173,10 +173,10 @@ func TestConfig_RetryWaitBase_IsUsed(t *testing.T) { t.Errorf("expected 3 attempts, got %d", callCount) } - // 验证等待时间 - // 第1次失败后等待 1s (customWaitBase * 1) - // 第2次失败后等待 2s (customWaitBase * 2) - // 总等待时间应该约为 3s (允许一些误差) + // Verify wait time + // After 1st failure wait 1s (customWaitBase * 1) + // After 2nd failure wait 2s (customWaitBase * 2) + // Total wait time should be about 3s (allow some error) expectedWait := 3 * time.Second tolerance := 200 * time.Millisecond @@ -189,7 +189,7 @@ func TestConfig_RetryableErrors_IsUsed(t *testing.T) { mockHTTP := NewMockHTTPClient() mockLogger := NewMockLogger() - // 自定义可重试错误列表(只包含 "custom error") + // Custom retryable error list (only contains "custom error") customRetryableErrors := []string{"custom error"} client := NewClient( @@ -200,7 +200,7 @@ func TestConfig_RetryableErrors_IsUsed(t *testing.T) { c := client.(*Client) - // 修改 config 的 RetryableErrors(暂时没有 WithRetryableErrors 选项) + // Modify config's RetryableErrors (no WithRetryableErrors option yet) c.config.RetryableErrors = customRetryableErrors tests := []struct { @@ -236,14 +236,14 @@ func TestConfig_RetryableErrors_IsUsed(t *testing.T) { } // ============================================================ -// 测试默认值 +// Test Default Values // ============================================================ func TestConfig_DefaultValues(t *testing.T) { client := NewClient() c := client.(*Client) - // 验证默认值 + // Verify default values if c.config.MaxRetries != 3 { t.Errorf("default MaxRetries should be 3, got %d", c.config.MaxRetries) } diff --git a/mcp/deepseek_client.go b/mcp/deepseek_client.go index 2a124c8b..5972c806 100644 --- a/mcp/deepseek_client.go +++ b/mcp/deepseek_client.go @@ -14,45 +14,45 @@ type DeepSeekClient struct { *Client } -// NewDeepSeekClient 创建 DeepSeek 客户端(向前兼容) +// NewDeepSeekClient creates DeepSeek client (backward compatible) // -// Deprecated: 推荐使用 NewDeepSeekClientWithOptions 以获得更好的灵活性 +// Deprecated: Recommend using NewDeepSeekClientWithOptions for better flexibility func NewDeepSeekClient() AIClient { return NewDeepSeekClientWithOptions() } -// NewDeepSeekClientWithOptions 创建 DeepSeek 客户端(支持选项模式) +// NewDeepSeekClientWithOptions creates DeepSeek client (supports options pattern) // -// 使用示例: -// // 基础用法 +// Usage examples: +// // Basic usage // client := mcp.NewDeepSeekClientWithOptions() // -// // 自定义配置 +// // Custom configuration // client := mcp.NewDeepSeekClientWithOptions( // mcp.WithAPIKey("sk-xxx"), // mcp.WithLogger(customLogger), // mcp.WithTimeout(60*time.Second), // ) func NewDeepSeekClientWithOptions(opts ...ClientOption) AIClient { - // 1. 创建 DeepSeek 预设选项 + // 1. Create DeepSeek preset options deepseekOpts := []ClientOption{ WithProvider(ProviderDeepSeek), WithModel(DefaultDeepSeekModel), WithBaseURL(DefaultDeepSeekBaseURL), } - // 2. 合并用户选项(用户选项优先级更高) + // 2. Merge user options (user options have higher priority) allOpts := append(deepseekOpts, opts...) - // 3. 创建基础客户端 + // 3. Create base client baseClient := NewClient(allOpts...).(*Client) - // 4. 创建 DeepSeek 客户端 + // 4. Create DeepSeek client dsClient := &DeepSeekClient{ Client: baseClient, } - // 5. 设置 hooks 指向 DeepSeekClient(实现动态分派) + // 5. Set hooks to point to DeepSeekClient (implement dynamic dispatch) baseClient.hooks = dsClient return dsClient @@ -66,15 +66,15 @@ func (dsClient *DeepSeekClient) SetAPIKey(apiKey string, customURL string, custo } if customURL != "" { dsClient.BaseURL = customURL - dsClient.logger.Infof("🔧 [MCP] DeepSeek 使用自定义 BaseURL: %s", customURL) + dsClient.logger.Infof("🔧 [MCP] DeepSeek using custom BaseURL: %s", customURL) } else { - dsClient.logger.Infof("🔧 [MCP] DeepSeek 使用默认 BaseURL: %s", dsClient.BaseURL) + dsClient.logger.Infof("🔧 [MCP] DeepSeek using default BaseURL: %s", dsClient.BaseURL) } if customModel != "" { dsClient.Model = customModel - dsClient.logger.Infof("🔧 [MCP] DeepSeek 使用自定义 Model: %s", customModel) + dsClient.logger.Infof("🔧 [MCP] DeepSeek using custom Model: %s", customModel) } else { - dsClient.logger.Infof("🔧 [MCP] DeepSeek 使用默认 Model: %s", dsClient.Model) + dsClient.logger.Infof("🔧 [MCP] DeepSeek using default Model: %s", dsClient.Model) } } diff --git a/mcp/deepseek_client_test.go b/mcp/deepseek_client_test.go index 8be91d52..afcd3b81 100644 --- a/mcp/deepseek_client_test.go +++ b/mcp/deepseek_client_test.go @@ -6,7 +6,7 @@ import ( ) // ============================================================ -// 测试 DeepSeekClient 创建和配置 +// Test DeepSeekClient Creation and Configuration // ============================================================ func TestNewDeepSeekClient_Default(t *testing.T) { @@ -16,13 +16,13 @@ func TestNewDeepSeekClient_Default(t *testing.T) { t.Fatal("client should not be nil") } - // 类型断言检查 + // Type assertion check dsClient, ok := client.(*DeepSeekClient) if !ok { t.Fatal("client should be *DeepSeekClient") } - // 验证默认值 + // Verify default values if dsClient.Provider != ProviderDeepSeek { t.Errorf("Provider should be '%s', got '%s'", ProviderDeepSeek, dsClient.Provider) } @@ -58,7 +58,7 @@ func TestNewDeepSeekClientWithOptions(t *testing.T) { dsClient := client.(*DeepSeekClient) - // 验证自定义选项被应用 + // Verify custom options are applied if dsClient.logger != mockLogger { t.Error("logger should be set from option") } @@ -75,7 +75,7 @@ func TestNewDeepSeekClientWithOptions(t *testing.T) { t.Error("MaxTokens should be 4000") } - // 验证 DeepSeek 默认值仍然保留 + // Verify DeepSeek default values are retained if dsClient.Provider != ProviderDeepSeek { t.Errorf("Provider should still be '%s'", ProviderDeepSeek) } @@ -86,7 +86,7 @@ func TestNewDeepSeekClientWithOptions(t *testing.T) { } // ============================================================ -// 测试 SetAPIKey +// Test SetAPIKey // ============================================================ func TestDeepSeekClient_SetAPIKey(t *testing.T) { @@ -97,20 +97,20 @@ func TestDeepSeekClient_SetAPIKey(t *testing.T) { dsClient := client.(*DeepSeekClient) - // 测试设置 API Key(默认 URL 和 Model) + // Test setting API Key (default URL and Model) dsClient.SetAPIKey("sk-test-key-12345678", "", "") if dsClient.APIKey != "sk-test-key-12345678" { t.Errorf("APIKey should be 'sk-test-key-12345678', got '%s'", dsClient.APIKey) } - // 验证日志记录 + // Verify logging logs := mockLogger.GetLogsByLevel("INFO") if len(logs) == 0 { t.Error("should have logged API key setting") } - // 验证 BaseURL 和 Model 保持默认 + // Verify BaseURL and Model remain default if dsClient.BaseURL != DefaultDeepSeekBaseURL { t.Error("BaseURL should remain default") } @@ -135,11 +135,11 @@ func TestDeepSeekClient_SetAPIKey_WithCustomURL(t *testing.T) { t.Errorf("BaseURL should be '%s', got '%s'", customURL, dsClient.BaseURL) } - // 验证日志记录 + // Verify logging logs := mockLogger.GetLogsByLevel("INFO") hasCustomURLLog := false for _, log := range logs { - if log.Format == "🔧 [MCP] DeepSeek 使用自定义 BaseURL: %s" { + if log.Format == "🔧 [MCP] DeepSeek using custom BaseURL: %s" { hasCustomURLLog = true break } @@ -165,11 +165,11 @@ func TestDeepSeekClient_SetAPIKey_WithCustomModel(t *testing.T) { t.Errorf("Model should be '%s', got '%s'", customModel, dsClient.Model) } - // 验证日志记录 + // Verify logging logs := mockLogger.GetLogsByLevel("INFO") hasCustomModelLog := false for _, log := range logs { - if log.Format == "🔧 [MCP] DeepSeek 使用自定义 Model: %s" { + if log.Format == "🔧 [MCP] DeepSeek using custom Model: %s" { hasCustomModelLog = true break } @@ -181,7 +181,7 @@ func TestDeepSeekClient_SetAPIKey_WithCustomModel(t *testing.T) { } // ============================================================ -// 测试集成功能 +// Test Integration Features // ============================================================ func TestDeepSeekClient_CallWithMessages_Success(t *testing.T) { @@ -205,7 +205,7 @@ func TestDeepSeekClient_CallWithMessages_Success(t *testing.T) { t.Errorf("expected 'DeepSeek AI response', got '%s'", result) } - // 验证请求 + // Verify request requests := mockHTTP.GetRequests() if len(requests) != 1 { t.Fatalf("expected 1 request, got %d", len(requests)) @@ -213,19 +213,19 @@ func TestDeepSeekClient_CallWithMessages_Success(t *testing.T) { req := requests[0] - // 验证 URL + // Verify URL expectedURL := DefaultDeepSeekBaseURL + "/chat/completions" if req.URL.String() != expectedURL { t.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL.String()) } - // 验证 Authorization header + // Verify Authorization header authHeader := req.Header.Get("Authorization") if authHeader != "Bearer sk-test-key" { t.Errorf("expected 'Bearer sk-test-key', got '%s'", authHeader) } - // 验证 Content-Type + // Verify Content-Type if req.Header.Get("Content-Type") != "application/json" { t.Error("Content-Type should be application/json") } @@ -242,7 +242,7 @@ func TestDeepSeekClient_Timeout(t *testing.T) { t.Errorf("expected timeout 30s, got %v", dsClient.httpClient.Timeout) } - // 测试 SetTimeout + // Test SetTimeout client.SetTimeout(60 * time.Second) if dsClient.httpClient.Timeout != 60*time.Second { @@ -251,19 +251,19 @@ func TestDeepSeekClient_Timeout(t *testing.T) { } // ============================================================ -// 测试 hooks 机制 +// Test hooks Mechanism // ============================================================ func TestDeepSeekClient_HooksIntegration(t *testing.T) { client := NewDeepSeekClientWithOptions() dsClient := client.(*DeepSeekClient) - // 验证 hooks 指向 dsClient 自己(实现多态) + // Verify hooks point to dsClient itself (implements polymorphism) if dsClient.hooks != dsClient { t.Error("hooks should point to dsClient for polymorphism") } - // 验证 buildUrl 使用 DeepSeek 配置 + // Verify buildUrl uses DeepSeek configuration url := dsClient.buildUrl() expectedURL := DefaultDeepSeekBaseURL + "/chat/completions" if url != expectedURL { diff --git a/mcp/examples_test.go b/mcp/examples_test.go index 2aa20829..78125421 100644 --- a/mcp/examples_test.go +++ b/mcp/examples_test.go @@ -9,21 +9,21 @@ import ( ) // ============================================================ -// 示例 1: 基础用法(向前兼容) +// Example 1: Basic Usage (Backward Compatible) // ============================================================ func Example_backward_compatible() { - // ✅ 旧代码继续工作,无需修改 + // Old code continues to work without modification client := mcp.New() client.SetAPIKey("sk-xxx", "https://api.custom.com", "gpt-4") - // 使用 + // Usage result, _ := client.CallWithMessages("system prompt", "user prompt") fmt.Println(result) } func Example_deepseek_backward_compatible() { - // ✅ DeepSeek 旧代码继续工作 + // DeepSeek old code continues to work client := mcp.NewDeepSeekClient() client.SetAPIKey("sk-xxx", "", "") @@ -32,19 +32,19 @@ func Example_deepseek_backward_compatible() { } // ============================================================ -// 示例 2: 新的推荐用法(选项模式) +// Example 2: New Recommended Usage (Options Pattern) // ============================================================ func Example_new_client_basic() { - // 使用默认配置 + // Use default configuration client := mcp.NewClient() - // 使用 DeepSeek + // Use DeepSeek client = mcp.NewClient( mcp.WithDeepSeekConfig("sk-xxx"), ) - // 使用 Qwen + // Use Qwen client = mcp.NewClient( mcp.WithQwenConfig("sk-xxx"), ) @@ -53,7 +53,7 @@ func Example_new_client_basic() { } func Example_new_client_with_options() { - // 组合多个选项 + // Combine multiple options client := mcp.NewClient( mcp.WithDeepSeekConfig("sk-xxx"), mcp.WithTimeout(60*time.Second), @@ -67,10 +67,10 @@ func Example_new_client_with_options() { } // ============================================================ -// 示例 3: 自定义日志器 +// Example 3: Custom Logger // ============================================================ -// CustomLogger 自定义日志器示例 +// CustomLogger custom logger example type CustomLogger struct{} func (l *CustomLogger) Debugf(format string, args ...any) { @@ -90,7 +90,7 @@ func (l *CustomLogger) Errorf(format string, args ...any) { } func Example_custom_logger() { - // 使用自定义日志器 + // Use custom logger customLogger := &CustomLogger{} client := mcp.NewClient( @@ -103,7 +103,7 @@ func Example_custom_logger() { } func Example_no_logger_for_testing() { - // 测试时禁用日志 + // Disable logging during testing client := mcp.NewClient( mcp.WithLogger(mcp.NewNoopLogger()), ) @@ -113,16 +113,16 @@ func Example_no_logger_for_testing() { } // ============================================================ -// 示例 4: 自定义 HTTP 客户端 +// Example 4: Custom HTTP Client // ============================================================ func Example_custom_http_client() { - // 自定义 HTTP 客户端(添加代理、TLS等) + // Custom HTTP client (add proxy, TLS, etc.) customHTTP := &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, - // 自定义 TLS、连接池等 + // Custom TLS, connection pool, etc. }, } @@ -136,16 +136,16 @@ func Example_custom_http_client() { } // ============================================================ -// 示例 5: DeepSeek 客户端(新 API) +// Example 5: DeepSeek Client (New API) // ============================================================ func Example_deepseek_new_api() { - // 基础用法 + // Basic usage client := mcp.NewDeepSeekClientWithOptions( mcp.WithAPIKey("sk-xxx"), ) - // 高级用法 + // Advanced usage client = mcp.NewDeepSeekClientWithOptions( mcp.WithAPIKey("sk-xxx"), mcp.WithLogger(&CustomLogger{}), @@ -158,16 +158,16 @@ func Example_deepseek_new_api() { } // ============================================================ -// 示例 6: Qwen 客户端(新 API) +// Example 6: Qwen Client (New API) // ============================================================ func Example_qwen_new_api() { - // 基础用法 + // Basic usage client := mcp.NewQwenClientWithOptions( mcp.WithAPIKey("sk-xxx"), ) - // 高级用法 + // Advanced usage client = mcp.NewQwenClientWithOptions( mcp.WithAPIKey("sk-xxx"), mcp.WithLogger(&CustomLogger{}), @@ -179,18 +179,18 @@ func Example_qwen_new_api() { } // ============================================================ -// 示例 7: 在 trader/auto_trader.go 中的迁移示例 +// Example 7: Migration Example in trader/auto_trader.go // ============================================================ func Example_trader_migration() { - // === 旧代码(继续工作)=== + // Old code (continues to work) oldStyleClient := func(apiKey, customURL, customModel string) mcp.AIClient { client := mcp.NewDeepSeekClient() client.SetAPIKey(apiKey, customURL, customModel) return client } - // === 新代码(推荐)=== + // New code (recommended) newStyleClient := func(apiKey, customURL, customModel string) mcp.AIClient { opts := []mcp.ClientOption{ mcp.WithAPIKey(apiKey), @@ -207,37 +207,37 @@ func Example_trader_migration() { return mcp.NewDeepSeekClientWithOptions(opts...) } - // 两种方式都能工作 + // Both approaches work _ = oldStyleClient("sk-xxx", "", "") _ = newStyleClient("sk-xxx", "", "") } // ============================================================ -// 示例 8: 测试场景 +// Example 8: Testing Scenarios // ============================================================ -// MockHTTPClient Mock HTTP 客户端 +// MockHTTPClient Mock HTTP client type MockHTTPClient struct { Response string } func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { - // 返回预设的响应 + // Return preset response return &http.Response{ StatusCode: 200, - Body: nil, // 实际测试中需要实现 + Body: nil, // Need to implement in actual tests }, nil } func Example_testing_with_mock() { - // 测试时使用 Mock + // Use Mock during testing // mockHTTP := &MockHTTPClient{ // Response: `{"choices":[{"message":{"content":"test response"}}]}`, // } client := mcp.NewClient( - // mcp.WithHTTPClient(mockHTTP), // 实际测试中使用 mockHTTP - mcp.WithLogger(mcp.NewNoopLogger()), // 禁用日志 + // mcp.WithHTTPClient(mockHTTP), // Use mockHTTP in actual tests + mcp.WithLogger(mcp.NewNoopLogger()), // Disable logging ) result, _ := client.CallWithMessages("system", "user") @@ -245,20 +245,20 @@ func Example_testing_with_mock() { } // ============================================================ -// 示例 9: 环境特定配置 +// Example 9: Environment-Specific Configuration // ============================================================ func Example_environment_specific() { - // 开发环境:详细日志 + // Development environment: detailed logging devClient := mcp.NewClient( mcp.WithDeepSeekConfig("sk-xxx"), - mcp.WithLogger(&CustomLogger{}), // 详细日志 + mcp.WithLogger(&CustomLogger{}), // Detailed logging ) - // 生产环境:结构化日志 + 超时保护 + // Production environment: structured logging + timeout protection prodClient := mcp.NewClient( mcp.WithDeepSeekConfig("sk-xxx"), - // mcp.WithLogger(&ZapLogger{}), // 生产级日志 + // mcp.WithLogger(&ZapLogger{}), // Production-grade logging mcp.WithTimeout(30*time.Second), mcp.WithMaxRetries(3), ) @@ -268,11 +268,11 @@ func Example_environment_specific() { } // ============================================================ -// 示例 10: 完整实战示例 +// Example 10: Complete Real-World Example // ============================================================ func Example_real_world_usage() { - // 创建带有完整配置的客户端 + // Create client with complete configuration client := mcp.NewDeepSeekClientWithOptions( mcp.WithAPIKey("sk-xxxxxxxxxx"), mcp.WithTimeout(60*time.Second), @@ -282,9 +282,9 @@ func Example_real_world_usage() { mcp.WithLogger(&CustomLogger{}), ) - // 使用客户端 - systemPrompt := "你是一个专业的量化交易顾问" - userPrompt := "分析 BTC 当前走势" + // Use client + systemPrompt := "You are a professional quantitative trading advisor" + userPrompt := "Analyze current BTC trend" result, err := client.CallWithMessages(systemPrompt, userPrompt) if err != nil { @@ -292,5 +292,5 @@ func Example_real_world_usage() { return } - fmt.Printf("AI 响应: %s\n", result) + fmt.Printf("AI response: %s\n", result) } diff --git a/mcp/interface.go b/mcp/interface.go index e155ac01..696b03ba 100644 --- a/mcp/interface.go +++ b/mcp/interface.go @@ -5,18 +5,18 @@ import ( "time" ) -// AIClient AI客户端公开接口(给外部使用) +// AIClient public AI client interface (for external use) type AIClient interface { SetAPIKey(apiKey string, customURL string, customModel string) SetTimeout(timeout time.Duration) CallWithMessages(systemPrompt, userPrompt string) (string, error) - CallWithRequest(req *Request) (string, error) // 构建器模式 API(支持高级功能) + CallWithRequest(req *Request) (string, error) // Builder pattern API (supports advanced features) } -// clientHooks 内部钩子接口(用于子类重写特定步骤) -// 这些方法只在包内部使用,实现动态分派 +// clientHooks internal hook interface (for subclass to override specific steps) +// These methods are only used inside the package to implement dynamic dispatch type clientHooks interface { - // 可被子类重写的钩子方法 + // Hook methods that can be overridden by subclass call(systemPrompt, userPrompt string) (string, error) diff --git a/mcp/logger.go b/mcp/logger.go index e12aa206..b565206d 100644 --- a/mcp/logger.go +++ b/mcp/logger.go @@ -1,8 +1,8 @@ package mcp -// Logger 日志接口(抽象依赖) -// 使用 Printf 风格的方法名,方便集成 logrus、zap 等主流日志库 -// 默认使用全局 logger 包(见 mcp/config.go) +// Logger interface (abstract dependency) +// Uses Printf-style method names for easy integration with mainstream logging libraries like logrus, zap, etc. +// Default uses global logger package (see mcp/config.go) type Logger interface { Debugf(format string, args ...any) Infof(format string, args ...any) @@ -10,7 +10,7 @@ type Logger interface { Errorf(format string, args ...any) } -// noopLogger 空日志实现(测试时使用) +// noopLogger no-op logger implementation (used in tests) type noopLogger struct{} func (l *noopLogger) Debugf(format string, args ...any) {} @@ -18,7 +18,7 @@ func (l *noopLogger) Infof(format string, args ...any) {} func (l *noopLogger) Warnf(format string, args ...any) {} func (l *noopLogger) Errorf(format string, args ...any) {} -// NewNoopLogger 创建空日志器(测试使用) +// NewNoopLogger creates no-op logger (for testing) func NewNoopLogger() Logger { return &noopLogger{} } diff --git a/mcp/mock_test.go b/mcp/mock_test.go index 6c39b099..1f93042d 100644 --- a/mcp/mock_test.go +++ b/mcp/mock_test.go @@ -13,19 +13,19 @@ import ( // Mock Logger // ============================================================ -// MockLogger Mock 日志器(用于测试) +// MockLogger Mock logger (for testing) type MockLogger struct { mu sync.Mutex Logs []LogEntry - Enabled bool // 是否启用日志记录 + Enabled bool // Whether logging is enabled } -// LogEntry 日志条目 +// LogEntry log entry type LogEntry struct { Level string Format string Args []any - Message string // 格式化后的消息 + Message string // Formatted message } func NewMockLogger() *MockLogger { @@ -68,14 +68,14 @@ func (m *MockLogger) log(level, format string, args ...any) { }) } -// GetLogs 获取所有日志 +// GetLogs gets all logs func (m *MockLogger) GetLogs() []LogEntry { m.mu.Lock() defer m.mu.Unlock() return append([]LogEntry{}, m.Logs...) } -// GetLogsByLevel 获取指定级别的日志 +// GetLogsByLevel gets logs by specified level func (m *MockLogger) GetLogsByLevel(level string) []LogEntry { m.mu.Lock() defer m.mu.Unlock() @@ -89,14 +89,14 @@ func (m *MockLogger) GetLogsByLevel(level string) []LogEntry { return result } -// Clear 清空日志 +// Clear clears all logs func (m *MockLogger) Clear() { m.mu.Lock() defer m.mu.Unlock() m.Logs = make([]LogEntry, 0) } -// HasLog 检查是否包含指定消息 +// HasLog checks if contains specified message func (m *MockLogger) HasLog(level, message string) bool { m.mu.Lock() defer m.mu.Unlock() @@ -110,20 +110,20 @@ func (m *MockLogger) HasLog(level, message string) bool { } // ============================================================ -// Mock HTTP Client (实现 http.RoundTripper) +// Mock HTTP Client (implements http.RoundTripper) // ============================================================ -// MockHTTPClient Mock HTTP 客户端(实现 http.RoundTripper) +// MockHTTPClient Mock HTTP client (implements http.RoundTripper) type MockHTTPClient struct { mu sync.Mutex - // 配置 + // Configuration Response string StatusCode int Error error - ResponseFunc func(req *http.Request) (*http.Response, error) // 自定义响应函数 + ResponseFunc func(req *http.Request) (*http.Response, error) // Custom response function - // 记录 + // Recording Requests []*http.Request } @@ -134,32 +134,32 @@ func NewMockHTTPClient() *MockHTTPClient { } } -// ToHTTPClient 转换为 http.Client +// ToHTTPClient converts to http.Client func (m *MockHTTPClient) ToHTTPClient() *http.Client { return &http.Client{ Transport: m, } } -// RoundTrip 实现 http.RoundTripper 接口 +// RoundTrip implements http.RoundTripper interface func (m *MockHTTPClient) RoundTrip(req *http.Request) (*http.Response, error) { m.mu.Lock() defer m.mu.Unlock() - // 记录请求 + // Record request m.Requests = append(m.Requests, req) - // 如果有自定义响应函数,使用它 + // If custom response function exists, use it if m.ResponseFunc != nil { return m.ResponseFunc(req) } - // 如果设置了错误,返回错误 + // If error is set, return error if m.Error != nil { return nil, m.Error } - // 返回模拟响应 + // Return mock response resp := &http.Response{ StatusCode: m.StatusCode, Body: io.NopCloser(bytes.NewBufferString(m.Response)), @@ -169,14 +169,14 @@ func (m *MockHTTPClient) RoundTrip(req *http.Request) (*http.Response, error) { return resp, nil } -// GetRequests 获取所有请求 +// GetRequests gets all requests func (m *MockHTTPClient) GetRequests() []*http.Request { m.mu.Lock() defer m.mu.Unlock() return append([]*http.Request{}, m.Requests...) } -// GetLastRequest 获取最后一次请求 +// GetLastRequest gets last request func (m *MockHTTPClient) GetLastRequest() *http.Request { m.mu.Lock() defer m.mu.Unlock() @@ -187,14 +187,14 @@ func (m *MockHTTPClient) GetLastRequest() *http.Request { return m.Requests[len(m.Requests)-1] } -// Reset 重置状态 +// Reset resets state func (m *MockHTTPClient) Reset() { m.mu.Lock() defer m.mu.Unlock() m.Requests = make([]*http.Request, 0) } -// SetSuccessResponse 设置成功响应 +// SetSuccessResponse sets success response func (m *MockHTTPClient) SetSuccessResponse(content string) { m.mu.Lock() defer m.mu.Unlock() @@ -204,7 +204,7 @@ func (m *MockHTTPClient) SetSuccessResponse(content string) { m.Error = nil } -// SetErrorResponse 设置错误响应 +// SetErrorResponse sets error response func (m *MockHTTPClient) SetErrorResponse(statusCode int, message string) { m.mu.Lock() defer m.mu.Unlock() @@ -214,7 +214,7 @@ func (m *MockHTTPClient) SetErrorResponse(statusCode int, message string) { m.Error = nil } -// SetNetworkError 设置网络错误 +// SetNetworkError sets network error func (m *MockHTTPClient) SetNetworkError(err error) { m.mu.Lock() defer m.mu.Unlock() @@ -223,10 +223,10 @@ func (m *MockHTTPClient) SetNetworkError(err error) { } // ============================================================ -// Mock Client Hooks (用于测试钩子机制) +// Mock Client Hooks (for testing hook mechanism) // ============================================================ -// MockClientHooks Mock 客户端钩子 +// MockClientHooks Mock client hooks type MockClientHooks struct { BuildRequestBodyCalled int BuildUrlCalled int @@ -235,7 +235,7 @@ type MockClientHooks struct { ParseResponseCalled int IsRetryableErrorCalled int - // 自定义返回值 + // Custom return values BuildUrlFunc func() string ParseResponseFunc func([]byte) (string, error) IsRetryableErrorFunc func(error) bool diff --git a/mcp/options.go b/mcp/options.go index c460224f..3e962627 100644 --- a/mcp/options.go +++ b/mcp/options.go @@ -5,16 +5,16 @@ import ( "time" ) -// ClientOption 客户端选项函数(Functional Options 模式) +// ClientOption client option function (Functional Options pattern) type ClientOption func(*Config) // ============================================================ -// 依赖注入选项 +// Dependency Injection Options // ============================================================ -// WithLogger 设置自定义日志器 +// WithLogger sets custom logger // -// 使用示例: +// Usage example: // client := mcp.NewClient(mcp.WithLogger(customLogger)) func WithLogger(logger Logger) ClientOption { return func(c *Config) { @@ -22,9 +22,9 @@ func WithLogger(logger Logger) ClientOption { } } -// WithHTTPClient 设置自定义 HTTP 客户端 +// WithHTTPClient sets custom HTTP client // -// 使用示例: +// Usage example: // httpClient := &http.Client{Timeout: 60 * time.Second} // client := mcp.NewClient(mcp.WithHTTPClient(httpClient)) func WithHTTPClient(client *http.Client) ClientOption { @@ -34,12 +34,12 @@ func WithHTTPClient(client *http.Client) ClientOption { } // ============================================================ -// 超时和重试选项 +// Timeout and Retry Options // ============================================================ -// WithTimeout 设置请求超时时间 +// WithTimeout sets request timeout duration // -// 使用示例: +// Usage example: // client := mcp.NewClient(mcp.WithTimeout(60 * time.Second)) func WithTimeout(timeout time.Duration) ClientOption { return func(c *Config) { @@ -48,9 +48,9 @@ func WithTimeout(timeout time.Duration) ClientOption { } } -// WithMaxRetries 设置最大重试次数 +// WithMaxRetries sets maximum retry count // -// 使用示例: +// Usage example: // client := mcp.NewClient(mcp.WithMaxRetries(5)) func WithMaxRetries(maxRetries int) ClientOption { return func(c *Config) { @@ -58,9 +58,9 @@ func WithMaxRetries(maxRetries int) ClientOption { } } -// WithRetryWaitBase 设置重试等待基础时长 +// WithRetryWaitBase sets base retry wait duration // -// 使用示例: +// Usage example: // client := mcp.NewClient(mcp.WithRetryWaitBase(3 * time.Second)) func WithRetryWaitBase(waitTime time.Duration) ClientOption { return func(c *Config) { @@ -69,12 +69,12 @@ func WithRetryWaitBase(waitTime time.Duration) ClientOption { } // ============================================================ -// AI 参数选项 +// AI Parameter Options // ============================================================ -// WithMaxTokens 设置最大 token 数 +// WithMaxTokens sets maximum token count // -// 使用示例: +// Usage example: // client := mcp.NewClient(mcp.WithMaxTokens(4000)) func WithMaxTokens(maxTokens int) ClientOption { return func(c *Config) { @@ -82,9 +82,9 @@ func WithMaxTokens(maxTokens int) ClientOption { } } -// WithTemperature 设置温度参数 +// WithTemperature sets temperature parameter // -// 使用示例: +// Usage example: // client := mcp.NewClient(mcp.WithTemperature(0.7)) func WithTemperature(temperature float64) ClientOption { return func(c *Config) { @@ -93,38 +93,38 @@ func WithTemperature(temperature float64) ClientOption { } // ============================================================ -// Provider 配置选项 +// Provider Configuration Options // ============================================================ -// WithAPIKey 设置 API Key +// WithAPIKey sets API Key func WithAPIKey(apiKey string) ClientOption { return func(c *Config) { c.APIKey = apiKey } } -// WithBaseURL 设置基础 URL +// WithBaseURL sets base URL func WithBaseURL(baseURL string) ClientOption { return func(c *Config) { c.BaseURL = baseURL } } -// WithModel 设置模型名称 +// WithModel sets model name func WithModel(model string) ClientOption { return func(c *Config) { c.Model = model } } -// WithProvider 设置提供商 +// WithProvider sets provider func WithProvider(provider string) ClientOption { return func(c *Config) { c.Provider = provider } } -// WithUseFullURL 设置是否使用完整 URL +// WithUseFullURL sets whether to use full URL func WithUseFullURL(useFullURL bool) ClientOption { return func(c *Config) { c.UseFullURL = useFullURL @@ -132,12 +132,12 @@ func WithUseFullURL(useFullURL bool) ClientOption { } // ============================================================ -// 组合选项(便捷方法) +// Combined Options (Convenience Methods) // ============================================================ -// WithDeepSeekConfig 设置 DeepSeek 配置 +// WithDeepSeekConfig sets DeepSeek configuration // -// 使用示例: +// Usage example: // client := mcp.NewClient(mcp.WithDeepSeekConfig("sk-xxx")) func WithDeepSeekConfig(apiKey string) ClientOption { return func(c *Config) { @@ -148,9 +148,9 @@ func WithDeepSeekConfig(apiKey string) ClientOption { } } -// WithQwenConfig 设置 Qwen 配置 +// WithQwenConfig sets Qwen configuration // -// 使用示例: +// Usage example: // client := mcp.NewClient(mcp.WithQwenConfig("sk-xxx")) func WithQwenConfig(apiKey string) ClientOption { return func(c *Config) { diff --git a/mcp/options_test.go b/mcp/options_test.go index 67ad5b9b..f39ee33b 100644 --- a/mcp/options_test.go +++ b/mcp/options_test.go @@ -7,7 +7,7 @@ import ( ) // ============================================================ -// 测试基础选项 +// Test Basic Options // ============================================================ func TestWithProvider(t *testing.T) { @@ -116,7 +116,7 @@ func TestWithHTTPClient(t *testing.T) { } // ============================================================ -// 测试预设配置选项 +// Test Preset Configuration Options // ============================================================ func TestWithDeepSeekConfig(t *testing.T) { @@ -162,7 +162,7 @@ func TestWithQwenConfig(t *testing.T) { } // ============================================================ -// 测试选项组合 +// Test Options Combination // ============================================================ func TestMultipleOptions(t *testing.T) { @@ -170,7 +170,7 @@ func TestMultipleOptions(t *testing.T) { cfg := DefaultConfig() - // 应用多个选项 + // Apply multiple options options := []ClientOption{ WithProvider("test-provider"), WithAPIKey("sk-test-key"), @@ -186,7 +186,7 @@ func TestMultipleOptions(t *testing.T) { opt(cfg) } - // 验证所有选项都被应用 + // Verify all options are applied if cfg.Provider != "test-provider" { t.Error("Provider should be set") } @@ -223,14 +223,14 @@ func TestMultipleOptions(t *testing.T) { func TestOptionsOverride(t *testing.T) { cfg := DefaultConfig() - // 先应用 DeepSeek 配置 + // First apply DeepSeek configuration WithDeepSeekConfig("sk-deepseek-key")(cfg) - // 然后覆盖某些选项 + // Then override some options WithModel("custom-model")(cfg) WithMaxTokens(5000)(cfg) - // 验证覆盖成功 + // Verify override succeeded if cfg.Model != "custom-model" { t.Errorf("Model should be overridden to 'custom-model', got '%s'", cfg.Model) } @@ -239,7 +239,7 @@ func TestOptionsOverride(t *testing.T) { t.Errorf("MaxTokens should be overridden to 5000, got %d", cfg.MaxTokens) } - // 验证其他 DeepSeek 配置保持不变 + // Verify other DeepSeek configurations remain unchanged if cfg.Provider != ProviderDeepSeek { t.Error("Provider should still be DeepSeek") } @@ -250,7 +250,7 @@ func TestOptionsOverride(t *testing.T) { } // ============================================================ -// 测试与客户端集成 +// Test Integration with Client // ============================================================ func TestOptionsWithNewClient(t *testing.T) { @@ -266,7 +266,7 @@ func TestOptionsWithNewClient(t *testing.T) { c := client.(*Client) - // 验证选项被正确应用到客户端 + // Verify options are correctly applied to client if c.Provider != "test-provider" { t.Error("Provider should be set from options") } @@ -299,7 +299,7 @@ func TestOptionsWithDeepSeekClient(t *testing.T) { dsClient := client.(*DeepSeekClient) - // 验证 DeepSeek 默认值 + // Verify DeepSeek default values if dsClient.Provider != ProviderDeepSeek { t.Error("Provider should be DeepSeek") } @@ -312,7 +312,7 @@ func TestOptionsWithDeepSeekClient(t *testing.T) { t.Error("Model should be DeepSeek default") } - // 验证自定义选项 + // Verify custom options if dsClient.APIKey != "sk-deepseek-key" { t.Error("APIKey should be set from options") } @@ -337,7 +337,7 @@ func TestOptionsWithQwenClient(t *testing.T) { qwenClient := client.(*QwenClient) - // 验证 Qwen 默认值 + // Verify Qwen default values if qwenClient.Provider != ProviderQwen { t.Error("Provider should be Qwen") } @@ -350,7 +350,7 @@ func TestOptionsWithQwenClient(t *testing.T) { t.Error("Model should be Qwen default") } - // 验证自定义选项 + // Verify custom options if qwenClient.APIKey != "sk-qwen-key" { t.Error("APIKey should be set from options") } diff --git a/mcp/qwen_client.go b/mcp/qwen_client.go index f790d08d..4c1b6ae3 100644 --- a/mcp/qwen_client.go +++ b/mcp/qwen_client.go @@ -14,45 +14,45 @@ type QwenClient struct { *Client } -// NewQwenClient 创建 Qwen 客户端(向前兼容) +// NewQwenClient creates Qwen client (backward compatible) // -// Deprecated: 推荐使用 NewQwenClientWithOptions 以获得更好的灵活性 +// Deprecated: Recommend using NewQwenClientWithOptions for better flexibility func NewQwenClient() AIClient { return NewQwenClientWithOptions() } -// NewQwenClientWithOptions 创建 Qwen 客户端(支持选项模式) +// NewQwenClientWithOptions creates Qwen client (supports options pattern) // -// 使用示例: -// // 基础用法 +// Usage examples: +// // Basic usage // client := mcp.NewQwenClientWithOptions() // -// // 自定义配置 +// // Custom configuration // client := mcp.NewQwenClientWithOptions( // mcp.WithAPIKey("sk-xxx"), // mcp.WithLogger(customLogger), // mcp.WithTimeout(60*time.Second), // ) func NewQwenClientWithOptions(opts ...ClientOption) AIClient { - // 1. 创建 Qwen 预设选项 + // 1. Create Qwen preset options qwenOpts := []ClientOption{ WithProvider(ProviderQwen), WithModel(DefaultQwenModel), WithBaseURL(DefaultQwenBaseURL), } - // 2. 合并用户选项(用户选项优先级更高) + // 2. Merge user options (user options have higher priority) allOpts := append(qwenOpts, opts...) - // 3. 创建基础客户端 + // 3. Create base client baseClient := NewClient(allOpts...).(*Client) - // 4. 创建 Qwen 客户端 + // 4. Create Qwen client qwenClient := &QwenClient{ Client: baseClient, } - // 5. 设置 hooks 指向 QwenClient(实现动态分派) + // 5. Set hooks to point to QwenClient (implement dynamic dispatch) baseClient.hooks = qwenClient return qwenClient @@ -66,15 +66,15 @@ func (qwenClient *QwenClient) SetAPIKey(apiKey string, customURL string, customM } if customURL != "" { qwenClient.BaseURL = customURL - qwenClient.logger.Infof("🔧 [MCP] Qwen 使用自定义 BaseURL: %s", customURL) + qwenClient.logger.Infof("🔧 [MCP] Qwen using custom BaseURL: %s", customURL) } else { - qwenClient.logger.Infof("🔧 [MCP] Qwen 使用默认 BaseURL: %s", qwenClient.BaseURL) + qwenClient.logger.Infof("🔧 [MCP] Qwen using default BaseURL: %s", qwenClient.BaseURL) } if customModel != "" { qwenClient.Model = customModel - qwenClient.logger.Infof("🔧 [MCP] Qwen 使用自定义 Model: %s", customModel) + qwenClient.logger.Infof("🔧 [MCP] Qwen using custom Model: %s", customModel) } else { - qwenClient.logger.Infof("🔧 [MCP] Qwen 使用默认 Model: %s", qwenClient.Model) + qwenClient.logger.Infof("🔧 [MCP] Qwen using default Model: %s", qwenClient.Model) } } diff --git a/mcp/qwen_client_test.go b/mcp/qwen_client_test.go index d8f0c44c..90149fc7 100644 --- a/mcp/qwen_client_test.go +++ b/mcp/qwen_client_test.go @@ -6,7 +6,7 @@ import ( ) // ============================================================ -// 测试 QwenClient 创建和配置 +// Test QwenClient Creation and Configuration // ============================================================ func TestNewQwenClient_Default(t *testing.T) { @@ -16,13 +16,13 @@ func TestNewQwenClient_Default(t *testing.T) { t.Fatal("client should not be nil") } - // 类型断言检查 + // Type assertion check qwenClient, ok := client.(*QwenClient) if !ok { t.Fatal("client should be *QwenClient") } - // 验证默认值 + // Verify default values if qwenClient.Provider != ProviderQwen { t.Errorf("Provider should be '%s', got '%s'", ProviderQwen, qwenClient.Provider) } @@ -58,7 +58,7 @@ func TestNewQwenClientWithOptions(t *testing.T) { qwenClient := client.(*QwenClient) - // 验证自定义选项被应用 + // Verify custom options are applied if qwenClient.logger != mockLogger { t.Error("logger should be set from option") } @@ -75,7 +75,7 @@ func TestNewQwenClientWithOptions(t *testing.T) { t.Error("MaxTokens should be 4000") } - // 验证 Qwen 默认值仍然保留 + // Verify Qwen default values are retained if qwenClient.Provider != ProviderQwen { t.Errorf("Provider should still be '%s'", ProviderQwen) } @@ -86,7 +86,7 @@ func TestNewQwenClientWithOptions(t *testing.T) { } // ============================================================ -// 测试 SetAPIKey +// Test SetAPIKey // ============================================================ func TestQwenClient_SetAPIKey(t *testing.T) { @@ -97,20 +97,20 @@ func TestQwenClient_SetAPIKey(t *testing.T) { qwenClient := client.(*QwenClient) - // 测试设置 API Key(默认 URL 和 Model) + // Test setting API Key (default URL and Model) qwenClient.SetAPIKey("sk-test-key-12345678", "", "") if qwenClient.APIKey != "sk-test-key-12345678" { t.Errorf("APIKey should be 'sk-test-key-12345678', got '%s'", qwenClient.APIKey) } - // 验证日志记录 + // Verify logging logs := mockLogger.GetLogsByLevel("INFO") if len(logs) == 0 { t.Error("should have logged API key setting") } - // 验证 BaseURL 和 Model 保持默认 + // Verify BaseURL and Model remain default if qwenClient.BaseURL != DefaultQwenBaseURL { t.Error("BaseURL should remain default") } @@ -135,11 +135,11 @@ func TestQwenClient_SetAPIKey_WithCustomURL(t *testing.T) { t.Errorf("BaseURL should be '%s', got '%s'", customURL, qwenClient.BaseURL) } - // 验证日志记录 + // Verify logging logs := mockLogger.GetLogsByLevel("INFO") hasCustomURLLog := false for _, log := range logs { - if log.Format == "🔧 [MCP] Qwen 使用自定义 BaseURL: %s" { + if log.Format == "🔧 [MCP] Qwen using custom BaseURL: %s" { hasCustomURLLog = true break } @@ -165,11 +165,11 @@ func TestQwenClient_SetAPIKey_WithCustomModel(t *testing.T) { t.Errorf("Model should be '%s', got '%s'", customModel, qwenClient.Model) } - // 验证日志记录 + // Verify logging logs := mockLogger.GetLogsByLevel("INFO") hasCustomModelLog := false for _, log := range logs { - if log.Format == "🔧 [MCP] Qwen 使用自定义 Model: %s" { + if log.Format == "🔧 [MCP] Qwen using custom Model: %s" { hasCustomModelLog = true break } @@ -181,7 +181,7 @@ func TestQwenClient_SetAPIKey_WithCustomModel(t *testing.T) { } // ============================================================ -// 测试集成功能 +// Test Integration Features // ============================================================ func TestQwenClient_CallWithMessages_Success(t *testing.T) { @@ -205,7 +205,7 @@ func TestQwenClient_CallWithMessages_Success(t *testing.T) { t.Errorf("expected 'Qwen AI response', got '%s'", result) } - // 验证请求 + // Verify request requests := mockHTTP.GetRequests() if len(requests) != 1 { t.Fatalf("expected 1 request, got %d", len(requests)) @@ -213,19 +213,19 @@ func TestQwenClient_CallWithMessages_Success(t *testing.T) { req := requests[0] - // 验证 URL + // Verify URL expectedURL := DefaultQwenBaseURL + "/chat/completions" if req.URL.String() != expectedURL { t.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL.String()) } - // 验证 Authorization header + // Verify Authorization header authHeader := req.Header.Get("Authorization") if authHeader != "Bearer sk-test-key" { t.Errorf("expected 'Bearer sk-test-key', got '%s'", authHeader) } - // 验证 Content-Type + // Verify Content-Type if req.Header.Get("Content-Type") != "application/json" { t.Error("Content-Type should be application/json") } @@ -242,7 +242,7 @@ func TestQwenClient_Timeout(t *testing.T) { t.Errorf("expected timeout 30s, got %v", qwenClient.httpClient.Timeout) } - // 测试 SetTimeout + // Test SetTimeout client.SetTimeout(60 * time.Second) if qwenClient.httpClient.Timeout != 60*time.Second { @@ -251,19 +251,19 @@ func TestQwenClient_Timeout(t *testing.T) { } // ============================================================ -// 测试 hooks 机制 +// Test hooks Mechanism // ============================================================ func TestQwenClient_HooksIntegration(t *testing.T) { client := NewQwenClientWithOptions() qwenClient := client.(*QwenClient) - // 验证 hooks 指向 qwenClient 自己(实现多态) + // Verify hooks point to qwenClient itself (implements polymorphism) if qwenClient.hooks != qwenClient { t.Error("hooks should point to qwenClient for polymorphism") } - // 验证 buildUrl 使用 Qwen 配置 + // Verify buildUrl uses Qwen configuration url := qwenClient.buildUrl() expectedURL := DefaultQwenBaseURL + "/chat/completions" if url != expectedURL { diff --git a/mcp/request.go b/mcp/request.go index d706dd63..3ade2d71 100644 --- a/mcp/request.go +++ b/mcp/request.go @@ -1,45 +1,45 @@ package mcp -// Message 表示一条对话消息 +// Message represents a conversation message type Message struct { Role string `json:"role"` // "system", "user", "assistant" - Content string `json:"content"` // 消息内容 + Content string `json:"content"` // Message content } -// Tool 表示 AI 可以调用的工具/函数 +// Tool represents a tool/function that AI can call type Tool struct { - Type string `json:"type"` // 通常为 "function" - Function FunctionDef `json:"function"` // 函数定义 + Type string `json:"type"` // Usually "function" + Function FunctionDef `json:"function"` // Function definition } -// FunctionDef 函数定义 +// FunctionDef function definition type FunctionDef struct { - Name string `json:"name"` // 函数名 - Description string `json:"description,omitempty"` // 函数描述 - Parameters map[string]any `json:"parameters,omitempty"` // 参数 schema (JSON Schema) + Name string `json:"name"` // Function name + Description string `json:"description,omitempty"` // Function description + Parameters map[string]any `json:"parameters,omitempty"` // Parameter schema (JSON Schema) } -// Request AI API 请求(支持高级功能) +// Request AI API request (supports advanced features) type Request struct { - // 基础字段 - Model string `json:"model"` // 模型名称 - Messages []Message `json:"messages"` // 对话消息列表 - Stream bool `json:"stream,omitempty"` // 是否流式响应 + // Basic fields + Model string `json:"model"` // Model name + Messages []Message `json:"messages"` // Conversation message list + Stream bool `json:"stream,omitempty"` // Whether to stream response - // 可选参数(用于精细控制) - Temperature *float64 `json:"temperature,omitempty"` // 温度 (0-2),控制随机性 - MaxTokens *int `json:"max_tokens,omitempty"` // 最大 token 数 - TopP *float64 `json:"top_p,omitempty"` // 核采样参数 (0-1) - FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` // 频率惩罚 (-2 to 2) - PresencePenalty *float64 `json:"presence_penalty,omitempty"` // 存在惩罚 (-2 to 2) - Stop []string `json:"stop,omitempty"` // 停止序列 + // Optional parameters (for fine-grained control) + Temperature *float64 `json:"temperature,omitempty"` // Temperature (0-2), controls randomness + MaxTokens *int `json:"max_tokens,omitempty"` // Maximum token count + TopP *float64 `json:"top_p,omitempty"` // Nucleus sampling parameter (0-1) + FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` // Frequency penalty (-2 to 2) + PresencePenalty *float64 `json:"presence_penalty,omitempty"` // Presence penalty (-2 to 2) + Stop []string `json:"stop,omitempty"` // Stop sequences - // 高级功能 - Tools []Tool `json:"tools,omitempty"` // 可用工具列表 - ToolChoice string `json:"tool_choice,omitempty"` // 工具选择策略 ("auto", "none", {"type": "function", "function": {"name": "xxx"}}) + // Advanced features + Tools []Tool `json:"tools,omitempty"` // Available tools list + ToolChoice string `json:"tool_choice,omitempty"` // Tool choice strategy ("auto", "none", {"type": "function", "function": {"name": "xxx"}}) } -// NewMessage 创建一条消息 +// NewMessage creates a message func NewMessage(role, content string) Message { return Message{ Role: role, @@ -47,7 +47,7 @@ func NewMessage(role, content string) Message { } } -// NewSystemMessage 创建系统消息 +// NewSystemMessage creates a system message func NewSystemMessage(content string) Message { return Message{ Role: "system", @@ -55,7 +55,7 @@ func NewSystemMessage(content string) Message { } } -// NewUserMessage 创建用户消息 +// NewUserMessage creates a user message func NewUserMessage(content string) Message { return Message{ Role: "user", @@ -63,7 +63,7 @@ func NewUserMessage(content string) Message { } } -// NewAssistantMessage 创建助手消息 +// NewAssistantMessage creates an assistant message func NewAssistantMessage(content string) Message { return Message{ Role: "assistant", diff --git a/mcp/request_builder.go b/mcp/request_builder.go index 4c61d6a4..293334ab 100644 --- a/mcp/request_builder.go +++ b/mcp/request_builder.go @@ -4,7 +4,7 @@ import ( "errors" ) -// RequestBuilder 请求构建器 +// RequestBuilder request builder type RequestBuilder struct { model string messages []Message @@ -19,9 +19,9 @@ type RequestBuilder struct { toolChoice string } -// NewRequestBuilder 创建请求构建器 +// NewRequestBuilder creates request builder // -// 使用示例: +// Usage example: // request := NewRequestBuilder(). // WithSystemPrompt("You are helpful"). // WithUserPrompt("Hello"). @@ -35,26 +35,26 @@ func NewRequestBuilder() *RequestBuilder { } // ============================================================ -// 模型和流式配置 +// Model and Stream Configuration // ============================================================ -// WithModel 设置模型名称 +// WithModel sets model name func (b *RequestBuilder) WithModel(model string) *RequestBuilder { b.model = model return b } -// WithStream 设置是否使用流式响应 +// WithStream sets whether to use streaming response func (b *RequestBuilder) WithStream(stream bool) *RequestBuilder { b.stream = stream return b } // ============================================================ -// 消息构建方法 +// Message Building Methods // ============================================================ -// WithSystemPrompt 添加系统提示词(便捷方法) +// WithSystemPrompt adds system prompt (convenience method) func (b *RequestBuilder) WithSystemPrompt(prompt string) *RequestBuilder { if prompt != "" { b.messages = append(b.messages, NewSystemMessage(prompt)) @@ -62,7 +62,7 @@ func (b *RequestBuilder) WithSystemPrompt(prompt string) *RequestBuilder { return b } -// WithUserPrompt 添加用户提示词(便捷方法) +// WithUserPrompt adds user prompt (convenience method) func (b *RequestBuilder) WithUserPrompt(prompt string) *RequestBuilder { if prompt != "" { b.messages = append(b.messages, NewUserMessage(prompt)) @@ -70,17 +70,17 @@ func (b *RequestBuilder) WithUserPrompt(prompt string) *RequestBuilder { return b } -// AddSystemMessage 添加系统消息 +// AddSystemMessage adds system message func (b *RequestBuilder) AddSystemMessage(content string) *RequestBuilder { return b.WithSystemPrompt(content) } -// AddUserMessage 添加用户消息 +// AddUserMessage adds user message func (b *RequestBuilder) AddUserMessage(content string) *RequestBuilder { return b.WithUserPrompt(content) } -// AddAssistantMessage 添加助手消息(用于多轮对话上下文) +// AddAssistantMessage adds assistant message (for multi-turn conversation context) func (b *RequestBuilder) AddAssistantMessage(content string) *RequestBuilder { if content != "" { b.messages = append(b.messages, NewAssistantMessage(content)) @@ -88,7 +88,7 @@ func (b *RequestBuilder) AddAssistantMessage(content string) *RequestBuilder { return b } -// AddMessage 添加自定义角色的消息 +// AddMessage adds message with custom role func (b *RequestBuilder) AddMessage(role, content string) *RequestBuilder { if content != "" { b.messages = append(b.messages, NewMessage(role, content)) @@ -96,33 +96,33 @@ func (b *RequestBuilder) AddMessage(role, content string) *RequestBuilder { return b } -// AddMessages 批量添加消息 +// AddMessages adds messages in batch func (b *RequestBuilder) AddMessages(messages ...Message) *RequestBuilder { b.messages = append(b.messages, messages...) return b } -// AddConversationHistory 添加对话历史 +// AddConversationHistory adds conversation history func (b *RequestBuilder) AddConversationHistory(history []Message) *RequestBuilder { b.messages = append(b.messages, history...) return b } -// ClearMessages 清空所有消息 +// ClearMessages clears all messages func (b *RequestBuilder) ClearMessages() *RequestBuilder { b.messages = make([]Message, 0) return b } // ============================================================ -// 参数控制方法 +// Parameter Control Methods // ============================================================ -// WithTemperature 设置温度参数 (0-2) -// 较高的温度(如 1.2)会使输出更随机,较低的温度(如 0.2)会使输出更确定 +// WithTemperature sets temperature parameter (0-2) +// Higher temperature (e.g. 1.2) makes output more random, lower temperature (e.g. 0.2) makes output more deterministic func (b *RequestBuilder) WithTemperature(t float64) *RequestBuilder { if t < 0 || t > 2 { - // 可以选择 panic 或者静默忽略,这里选择限制范围 + // Can choose to panic or silently ignore, here we choose to limit the range if t < 0 { t = 0 } @@ -134,7 +134,7 @@ func (b *RequestBuilder) WithTemperature(t float64) *RequestBuilder { return b } -// WithMaxTokens 设置最大 token 数 +// WithMaxTokens sets maximum token count func (b *RequestBuilder) WithMaxTokens(tokens int) *RequestBuilder { if tokens > 0 { b.maxTokens = &tokens @@ -142,8 +142,8 @@ func (b *RequestBuilder) WithMaxTokens(tokens int) *RequestBuilder { return b } -// WithTopP 设置 top-p 核采样参数 (0-1) -// 控制考虑的 token 范围,较小的值(如 0.1)使输出更聚焦 +// WithTopP sets top-p nucleus sampling parameter (0-1) +// Controls the range of tokens considered, smaller values (e.g. 0.1) make output more focused func (b *RequestBuilder) WithTopP(p float64) *RequestBuilder { if p >= 0 && p <= 1 { b.topP = &p @@ -151,8 +151,8 @@ func (b *RequestBuilder) WithTopP(p float64) *RequestBuilder { return b } -// WithFrequencyPenalty 设置频率惩罚 (-2 to 2) -// 正值会根据 token 在文本中出现的频率惩罚它们,减少重复 +// WithFrequencyPenalty sets frequency penalty (-2 to 2) +// Positive values penalize tokens based on their frequency in the text, reducing repetition func (b *RequestBuilder) WithFrequencyPenalty(penalty float64) *RequestBuilder { if penalty >= -2 && penalty <= 2 { b.frequencyPenalty = &penalty @@ -160,8 +160,8 @@ func (b *RequestBuilder) WithFrequencyPenalty(penalty float64) *RequestBuilder { return b } -// WithPresencePenalty 设置存在惩罚 (-2 to 2) -// 正值会根据 token 是否出现在文本中惩罚它们,增加话题多样性 +// WithPresencePenalty sets presence penalty (-2 to 2) +// Positive values penalize tokens based on whether they appear in the text, increasing topic diversity func (b *RequestBuilder) WithPresencePenalty(penalty float64) *RequestBuilder { if penalty >= -2 && penalty <= 2 { b.presencePenalty = &penalty @@ -169,14 +169,14 @@ func (b *RequestBuilder) WithPresencePenalty(penalty float64) *RequestBuilder { return b } -// WithStopSequences 设置停止序列 -// 当模型生成这些序列之一时,将停止生成 +// WithStopSequences sets stop sequences +// Model will stop generating when it generates one of these sequences func (b *RequestBuilder) WithStopSequences(sequences []string) *RequestBuilder { b.stop = sequences return b } -// AddStopSequence 添加单个停止序列 +// AddStopSequence adds a single stop sequence func (b *RequestBuilder) AddStopSequence(sequence string) *RequestBuilder { if sequence != "" { b.stop = append(b.stop, sequence) @@ -185,16 +185,16 @@ func (b *RequestBuilder) AddStopSequence(sequence string) *RequestBuilder { } // ============================================================ -// 工具/函数调用相关 +// Tool/Function Calling Related // ============================================================ -// AddTool 添加工具 +// AddTool adds a tool func (b *RequestBuilder) AddTool(tool Tool) *RequestBuilder { b.tools = append(b.tools, tool) return b } -// AddFunction 添加函数(便捷方法) +// AddFunction adds a function (convenience method) func (b *RequestBuilder) AddFunction(name, description string, parameters map[string]any) *RequestBuilder { tool := Tool{ Type: "function", @@ -208,27 +208,27 @@ func (b *RequestBuilder) AddFunction(name, description string, parameters map[st return b } -// WithToolChoice 设置工具选择策略 -// - "auto": 自动选择是否调用工具 -// - "none": 不调用工具 -// - 也可以指定特定工具: `{"type": "function", "function": {"name": "my_function"}}` +// WithToolChoice sets tool choice strategy +// - "auto": automatically choose whether to call tools +// - "none": don't call tools +// - Can also specify a specific tool: `{"type": "function", "function": {"name": "my_function"}}` func (b *RequestBuilder) WithToolChoice(choice string) *RequestBuilder { b.toolChoice = choice return b } // ============================================================ -// 构建方法 +// Build Methods // ============================================================ -// Build 构建请求对象 +// Build builds request object func (b *RequestBuilder) Build() (*Request, error) { - // 验证:至少需要一条消息 + // Validation: at least one message is required if len(b.messages) == 0 { - return nil, errors.New("至少需要一条消息") + return nil, errors.New("at least one message is required") } - // 创建请求 + // Create request req := &Request{ Model: b.model, Messages: b.messages, @@ -238,7 +238,7 @@ func (b *RequestBuilder) Build() (*Request, error) { ToolChoice: b.toolChoice, } - // 只设置非 nil 的可选参数(避免发送 0 值覆盖服务端默认值) + // Only set non-nil optional parameters (avoid sending 0 values that override server defaults) if b.temperature != nil { req.Temperature = b.temperature } @@ -258,8 +258,8 @@ func (b *RequestBuilder) Build() (*Request, error) { return req, nil } -// MustBuild 构建请求对象,如果失败则 panic -// 适用于构建过程中确定不会出错的场景 +// MustBuild builds request object, panics if failed +// Suitable for scenarios where build is guaranteed not to fail func (b *RequestBuilder) MustBuild() *Request { req, err := b.Build() if err != nil { @@ -269,10 +269,10 @@ func (b *RequestBuilder) MustBuild() *Request { } // ============================================================ -// 便捷方法:预设场景 +// Convenience Methods: Preset Scenarios // ============================================================ -// ForChat 创建用于聊天的构建器(预设合理的参数) +// ForChat creates builder for chat (preset with reasonable parameters) func ForChat() *RequestBuilder { temp := 0.7 tokens := 2000 @@ -284,7 +284,7 @@ func ForChat() *RequestBuilder { } } -// ForCodeGeneration 创建用于代码生成的构建器(低温度,更确定) +// ForCodeGeneration creates builder for code generation (low temperature, more deterministic) func ForCodeGeneration() *RequestBuilder { temp := 0.2 tokens := 2000 @@ -298,7 +298,7 @@ func ForCodeGeneration() *RequestBuilder { } } -// ForCreativeWriting 创建用于创意写作的构建器(高温度,更随机) +// ForCreativeWriting creates builder for creative writing (high temperature, more random) func ForCreativeWriting() *RequestBuilder { temp := 1.2 tokens := 4000 diff --git a/mcp/request_builder_test.go b/mcp/request_builder_test.go index be78601f..6c5de6db 100644 --- a/mcp/request_builder_test.go +++ b/mcp/request_builder_test.go @@ -6,7 +6,7 @@ import ( ) // ============================================================ -// 测试 RequestBuilder 基本功能 +// Test RequestBuilder Basic Features // ============================================================ func TestRequestBuilder_BasicUsage(t *testing.T) { @@ -39,13 +39,13 @@ func TestRequestBuilder_EmptyMessages(t *testing.T) { t.Error("Build should error when no messages") } - if err.Error() != "至少需要一条消息" { + if err.Error() != "at least one message is required" { t.Errorf("unexpected error: %v", err) } } // ============================================================ -// 测试消息构建方法 +// Test Message Building Methods // ============================================================ func TestRequestBuilder_MultipleMessages(t *testing.T) { @@ -85,7 +85,7 @@ func TestRequestBuilder_AddConversationHistory(t *testing.T) { } // ============================================================ -// 测试参数控制方法 +// Test Parameter Control Methods // ============================================================ func TestRequestBuilder_WithTemperature(t *testing.T) { @@ -165,7 +165,7 @@ func TestRequestBuilder_WithStopSequences(t *testing.T) { } // ============================================================ -// 测试工具/函数调用 +// Test Tool/Function Calling // ============================================================ func TestRequestBuilder_AddTool(t *testing.T) { @@ -229,7 +229,7 @@ func TestRequestBuilder_AddFunction(t *testing.T) { } // ============================================================ -// 测试便捷方法 +// Test Convenience Methods // ============================================================ func TestRequestBuilder_ForChat(t *testing.T) { @@ -287,7 +287,7 @@ func TestRequestBuilder_ForCreativeWriting(t *testing.T) { } // ============================================================ -// 测试 CallWithRequest 集成 +// Test CallWithRequest Integration // ============================================================ func TestClient_CallWithRequest_Success(t *testing.T) { @@ -317,25 +317,25 @@ func TestClient_CallWithRequest_Success(t *testing.T) { t.Errorf("expected 'Builder response', got '%s'", result) } - // 验证请求体 + // Verify request body requests := mockHTTP.GetRequests() if len(requests) != 1 { t.Fatalf("expected 1 request, got %d", len(requests)) } - // 解析请求体验证参数 + // Parse request body to verify parameters var body map[string]interface{} decoder := json.NewDecoder(requests[0].Body) if err := decoder.Decode(&body); err != nil { t.Fatalf("failed to decode request body: %v", err) } - // 验证 temperature + // Verify temperature if body["temperature"] != 0.8 { t.Errorf("expected temperature 0.8, got %v", body["temperature"]) } - // 验证 messages + // Verify messages messages, ok := body["messages"].([]interface{}) if !ok || len(messages) != 2 { t.Error("messages not correctly formatted") @@ -353,7 +353,7 @@ func TestClient_CallWithRequest_MultiRound(t *testing.T) { WithAPIKey("sk-test-key"), ) - // 构建多轮对话 + // Build multi-round conversation request := NewRequestBuilder(). AddSystemMessage("You are a trading advisor"). AddUserMessage("Analyze BTC"). @@ -372,7 +372,7 @@ func TestClient_CallWithRequest_MultiRound(t *testing.T) { t.Errorf("expected 'Multi-round response', got '%s'", result) } - // 验证请求体包含所有消息 + // Verify request body contains all messages requests := mockHTTP.GetRequests() var body map[string]interface{} json.NewDecoder(requests[0].Body).Decode(&body) @@ -411,7 +411,7 @@ func TestClient_CallWithRequest_WithTools(t *testing.T) { t.Fatalf("should not error: %v", err) } - // 验证请求体包含 tools + // Verify request body contains tools requests := mockHTTP.GetRequests() var body map[string]interface{} json.NewDecoder(requests[0].Body).Decode(&body) @@ -440,7 +440,7 @@ func TestClient_CallWithRequest_NoAPIKey(t *testing.T) { t.Error("should error when API key not set") } - if err.Error() != "AI API密钥未设置,请先调用 SetAPIKey" { + if err.Error() != "AI API key not set, please call SetAPIKey first" { t.Errorf("unexpected error: %v", err) } } @@ -456,7 +456,7 @@ func TestClient_CallWithRequest_UsesClientModel(t *testing.T) { WithAPIKey("sk-test-key"), ) - // Request 不设置 model,应该使用 Client 的 model + // Request does not set model, should use Client's model request := NewRequestBuilder(). WithUserPrompt("Hello"). MustBuild() @@ -467,7 +467,7 @@ func TestClient_CallWithRequest_UsesClientModel(t *testing.T) { client.CallWithRequest(request) - // 验证使用了 DeepSeek 的 model + // Verify DeepSeek's model is used requests := mockHTTP.GetRequests() var body map[string]interface{} json.NewDecoder(requests[0].Body).Decode(&body) diff --git a/pool/coin_pool.go b/pool/coin_pool.go index 72ce5c72..08169554 100644 --- a/pool/coin_pool.go +++ b/pool/coin_pool.go @@ -12,7 +12,7 @@ import ( "time" ) -// defaultMainstreamCoins 默认主流币种池(从配置文件读取) +// defaultMainstreamCoins default mainstream coin pool (read from config file) var defaultMainstreamCoins = []string{ "BTCUSDT", "ETHUSDT", @@ -24,42 +24,42 @@ var defaultMainstreamCoins = []string{ "HYPEUSDT", } -// CoinPoolConfig 币种池配置 +// CoinPoolConfig coin pool configuration type CoinPoolConfig struct { APIURL string Timeout time.Duration CacheDir string - UseDefaultCoins bool // 是否使用默认主流币种 + UseDefaultCoins bool // Whether to use default mainstream coins } var coinPoolConfig = CoinPoolConfig{ APIURL: "", - Timeout: 30 * time.Second, // 增加到30秒 + Timeout: 30 * time.Second, // Increased to 30 seconds CacheDir: "coin_pool_cache", - UseDefaultCoins: false, // 默认不使用 + UseDefaultCoins: false, // Default is not to use } -// CoinPoolCache 币种池缓存 +// CoinPoolCache coin pool cache type CoinPoolCache struct { Coins []CoinInfo `json:"coins"` FetchedAt time.Time `json:"fetched_at"` SourceType string `json:"source_type"` // "api" or "cache" } -// CoinInfo 币种信息 +// CoinInfo coin information type CoinInfo struct { - Pair string `json:"pair"` // 交易对符号(例如:BTCUSDT) - Score float64 `json:"score"` // 当前评分 - StartTime int64 `json:"start_time"` // 开始时间(Unix时间戳) - StartPrice float64 `json:"start_price"` // 开始价格 - LastScore float64 `json:"last_score"` // 最新评分 - MaxScore float64 `json:"max_score"` // 最高评分 - MaxPrice float64 `json:"max_price"` // 最高价格 - IncreasePercent float64 `json:"increase_percent"` // 涨幅百分比 - IsAvailable bool `json:"-"` // 是否可交易(内部使用) + Pair string `json:"pair"` // Trading pair symbol (e.g.: BTCUSDT) + Score float64 `json:"score"` // Current score + StartTime int64 `json:"start_time"` // Start time (Unix timestamp) + StartPrice float64 `json:"start_price"` // Start price + LastScore float64 `json:"last_score"` // Latest score + MaxScore float64 `json:"max_score"` // Highest score + MaxPrice float64 `json:"max_price"` // Highest price + IncreasePercent float64 `json:"increase_percent"` // Increase percentage + IsAvailable bool `json:"-"` // Whether tradable (internal use) } -// CoinPoolAPIResponse API返回的原始数据结构 +// CoinPoolAPIResponse raw data structure returned by API type CoinPoolAPIResponse struct { Success bool `json:"success"` Data struct { @@ -68,85 +68,85 @@ type CoinPoolAPIResponse struct { } `json:"data"` } -// SetCoinPoolAPI 设置币种池API +// SetCoinPoolAPI sets coin pool API func SetCoinPoolAPI(apiURL string) { coinPoolConfig.APIURL = apiURL } -// SetOITopAPI 设置OI Top API +// SetOITopAPI sets OI Top API func SetOITopAPI(apiURL string) { oiTopConfig.APIURL = apiURL } -// SetUseDefaultCoins 设置是否使用默认主流币种 +// SetUseDefaultCoins sets whether to use default mainstream coins func SetUseDefaultCoins(useDefault bool) { coinPoolConfig.UseDefaultCoins = useDefault } -// SetDefaultCoins 设置默认主流币种列表 +// SetDefaultCoins sets default mainstream coin list func SetDefaultCoins(coins []string) { if len(coins) > 0 { defaultMainstreamCoins = coins - log.Printf("✓ 已设置默认币种池(共%d个币种): %v", len(coins), coins) + log.Printf("✓ Default coin pool set (%d coins): %v", len(coins), coins) } } -// GetCoinPool 获取币种池列表(带重试和缓存机制) +// GetCoinPool retrieves coin pool list (with retry and cache mechanism) func GetCoinPool() ([]CoinInfo, error) { - // 优先检查是否启用默认币种列表 + // First check if default coin list is enabled if coinPoolConfig.UseDefaultCoins { - log.Printf("✓ 已启用默认主流币种列表") + log.Printf("✓ Default mainstream coin list enabled") return convertSymbolsToCoins(defaultMainstreamCoins), nil } - // 检查API URL是否配置 + // Check if API URL is configured if strings.TrimSpace(coinPoolConfig.APIURL) == "" { - log.Printf("⚠️ 未配置币种池API URL,使用默认主流币种列表") + log.Printf("⚠️ Coin pool API URL not configured, using default mainstream coin list") return convertSymbolsToCoins(defaultMainstreamCoins), nil } maxRetries := 3 var lastErr error - // 尝试从API获取 + // Try to fetch from API for attempt := 1; attempt <= maxRetries; attempt++ { if attempt > 1 { - log.Printf("⚠️ 第%d次重试获取币种池(共%d次)...", attempt, maxRetries) - time.Sleep(2 * time.Second) // 重试前等待2秒 + log.Printf("⚠️ Retry attempt %d of %d to fetch coin pool...", attempt, maxRetries) + time.Sleep(2 * time.Second) // Wait 2 seconds before retry } coins, err := fetchCoinPool() if err == nil { if attempt > 1 { - log.Printf("✓ 第%d次重试成功", attempt) + log.Printf("✓ Retry attempt %d succeeded", attempt) } - // 成功获取后保存到缓存 + // Save to cache after successful fetch if err := saveCoinPoolCache(coins); err != nil { - log.Printf("⚠️ 保存币种池缓存失败: %v", err) + log.Printf("⚠️ Failed to save coin pool cache: %v", err) } return coins, nil } lastErr = err - log.Printf("❌ 第%d次请求失败: %v", attempt, err) + log.Printf("❌ Request attempt %d failed: %v", attempt, err) } - // API获取失败,尝试使用缓存 - log.Printf("⚠️ API请求全部失败,尝试使用历史缓存数据...") + // API fetch failed, try to use cache + log.Printf("⚠️ All API requests failed, trying to use historical cache data...") cachedCoins, err := loadCoinPoolCache() if err == nil { - log.Printf("✓ 使用历史缓存数据(共%d个币种)", len(cachedCoins)) + log.Printf("✓ Using historical cache data (%d coins)", len(cachedCoins)) return cachedCoins, nil } - // 缓存也失败,使用默认主流币种 - log.Printf("⚠️ 无法加载缓存数据(最后错误: %v),使用默认主流币种列表", lastErr) + // Cache also failed, use default mainstream coins + log.Printf("⚠️ Unable to load cache data (last error: %v), using default mainstream coin list", lastErr) return convertSymbolsToCoins(defaultMainstreamCoins), nil } -// fetchCoinPool 实际执行币种池请求 +// fetchCoinPool actually executes coin pool request func fetchCoinPool() ([]CoinInfo, error) { - log.Printf("🔄 正在请求AI500币种池...") + log.Printf("🔄 Requesting AI500 coin pool...") client := &http.Client{ Timeout: coinPoolConfig.Timeout, @@ -154,48 +154,48 @@ func fetchCoinPool() ([]CoinInfo, error) { resp, err := client.Get(coinPoolConfig.APIURL) if err != nil { - return nil, fmt.Errorf("请求币种池API失败: %w", err) + return nil, fmt.Errorf("failed to request coin pool API: %w", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("读取响应失败: %w", err) + return nil, fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API返回错误 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("API returned error (status %d): %s", resp.StatusCode, string(body)) } - // 解析API响应 + // Parse API response var response CoinPoolAPIResponse if err := json.Unmarshal(body, &response); err != nil { - return nil, fmt.Errorf("JSON解析失败: %w", err) + return nil, fmt.Errorf("JSON parsing failed: %w", err) } if !response.Success { - return nil, fmt.Errorf("API返回失败状态") + return nil, fmt.Errorf("API returned failure status") } if len(response.Data.Coins) == 0 { - return nil, fmt.Errorf("币种列表为空") + return nil, fmt.Errorf("coin list is empty") } - // 设置IsAvailable标志 + // Set IsAvailable flag coins := response.Data.Coins for i := range coins { coins[i].IsAvailable = true } - log.Printf("✓ 成功获取%d个币种", len(coins)) + log.Printf("✓ Successfully fetched %d coins", len(coins)) return coins, nil } -// saveCoinPoolCache 保存币种池到缓存文件 +// saveCoinPoolCache saves coin pool to cache file func saveCoinPoolCache(coins []CoinInfo) error { - // 确保缓存目录存在 + // Ensure cache directory exists if err := os.MkdirAll(coinPoolConfig.CacheDir, 0755); err != nil { - return fmt.Errorf("创建缓存目录失败: %w", err) + return fmt.Errorf("failed to create cache directory: %w", err) } cache := CoinPoolCache{ @@ -206,43 +206,43 @@ func saveCoinPoolCache(coins []CoinInfo) error { data, err := json.MarshalIndent(cache, "", " ") if err != nil { - return fmt.Errorf("序列化缓存数据失败: %w", err) + return fmt.Errorf("failed to serialize cache data: %w", err) } cachePath := filepath.Join(coinPoolConfig.CacheDir, "latest.json") if err := ioutil.WriteFile(cachePath, data, 0644); err != nil { - return fmt.Errorf("写入缓存文件失败: %w", err) + return fmt.Errorf("failed to write cache file: %w", err) } - log.Printf("💾 已保存币种池缓存(%d个币种)", len(coins)) + log.Printf("💾 Coin pool cache saved (%d coins)", len(coins)) return nil } -// loadCoinPoolCache 从缓存文件加载币种池 +// loadCoinPoolCache loads coin pool from cache file func loadCoinPoolCache() ([]CoinInfo, error) { cachePath := filepath.Join(coinPoolConfig.CacheDir, "latest.json") - // 检查文件是否存在 + // Check if file exists if _, err := os.Stat(cachePath); os.IsNotExist(err) { - return nil, fmt.Errorf("缓存文件不存在") + return nil, fmt.Errorf("cache file does not exist") } data, err := ioutil.ReadFile(cachePath) if err != nil { - return nil, fmt.Errorf("读取缓存文件失败: %w", err) + return nil, fmt.Errorf("failed to read cache file: %w", err) } var cache CoinPoolCache if err := json.Unmarshal(data, &cache); err != nil { - return nil, fmt.Errorf("解析缓存数据失败: %w", err) + return nil, fmt.Errorf("failed to parse cache data: %w", err) } - // 检查缓存年龄 + // Check cache age cacheAge := time.Since(cache.FetchedAt) if cacheAge > 24*time.Hour { - log.Printf("⚠️ 缓存数据较旧(%.1f小时前),但仍可使用", cacheAge.Hours()) + log.Printf("⚠️ Cache data is old (%.1f hours ago), but still usable", cacheAge.Hours()) } else { - log.Printf("📂 缓存数据时间: %s(%.1f分钟前)", + log.Printf("📂 Cache data timestamp: %s (%.1f minutes ago)", cache.FetchedAt.Format("2006-01-02 15:04:05"), cacheAge.Minutes()) } @@ -250,7 +250,7 @@ func loadCoinPoolCache() ([]CoinInfo, error) { return cache.Coins, nil } -// GetAvailableCoins 获取可用的币种列表(过滤不可用的) +// GetAvailableCoins retrieves available coin list (filters out unavailable ones) func GetAvailableCoins() ([]string, error) { coins, err := GetCoinPool() if err != nil { @@ -260,27 +260,27 @@ func GetAvailableCoins() ([]string, error) { var symbols []string for _, coin := range coins { if coin.IsAvailable { - // 确保symbol格式正确(转为大写USDT交易对) + // Ensure symbol format is correct (convert to uppercase USDT pair) symbol := normalizeSymbol(coin.Pair) symbols = append(symbols, symbol) } } if len(symbols) == 0 { - return nil, fmt.Errorf("没有可用的币种") + return nil, fmt.Errorf("no available coins") } return symbols, nil } -// GetTopRatedCoins 获取评分最高的N个币种(按评分从大到小排序) +// GetTopRatedCoins retrieves top N coins by score (sorted by score descending) func GetTopRatedCoins(limit int) ([]string, error) { coins, err := GetCoinPool() if err != nil { return nil, err } - // 过滤可用的币种 + // Filter available coins var availableCoins []CoinInfo for _, coin := range coins { if coin.IsAvailable { @@ -289,10 +289,10 @@ func GetTopRatedCoins(limit int) ([]string, error) { } if len(availableCoins) == 0 { - return nil, fmt.Errorf("没有可用的币种") + return nil, fmt.Errorf("no available coins") } - // 按Score降序排序(冒泡排序) + // Sort by Score descending (bubble sort) for i := 0; i < len(availableCoins); i++ { for j := i + 1; j < len(availableCoins); j++ { if availableCoins[i].Score < availableCoins[j].Score { @@ -301,7 +301,7 @@ func GetTopRatedCoins(limit int) ([]string, error) { } } - // 取前N个 + // Take top N maxCount := limit if len(availableCoins) < maxCount { maxCount = len(availableCoins) @@ -316,15 +316,15 @@ func GetTopRatedCoins(limit int) ([]string, error) { return symbols, nil } -// normalizeSymbol 标准化币种符号 +// normalizeSymbol normalizes coin symbol func normalizeSymbol(symbol string) string { - // 移除空格 + // Remove spaces symbol = trimSpaces(symbol) - // 转为大写 + // Convert to uppercase symbol = toUpper(symbol) - // 确保以USDT结尾 + // Ensure ends with USDT if !endsWith(symbol, "USDT") { symbol = symbol + "USDT" } @@ -332,7 +332,7 @@ func normalizeSymbol(symbol string) string { return symbol } -// 辅助函数 +// Helper functions func trimSpaces(s string) string { result := "" for i := 0; i < len(s); i++ { @@ -362,7 +362,7 @@ func endsWith(s, suffix string) bool { return s[len(s)-len(suffix):] == suffix } -// convertSymbolsToCoins 将币种符号列表转换为CoinInfo列表 +// convertSymbolsToCoins converts symbol list to CoinInfo list func convertSymbolsToCoins(symbols []string) []CoinInfo { coins := make([]CoinInfo, 0, len(symbols)) for _, symbol := range symbols { @@ -375,22 +375,22 @@ func convertSymbolsToCoins(symbols []string) []CoinInfo { return coins } -// ========== OI Top(持仓量增长Top20)数据 ========== +// ========== OI Top (Open Interest Growth Top 20) Data ========== -// OIPosition 持仓量数据 +// OIPosition open interest data type OIPosition struct { Symbol string `json:"symbol"` Rank int `json:"rank"` - CurrentOI float64 `json:"current_oi"` // 当前持仓量 - OIDelta float64 `json:"oi_delta"` // 持仓量变化 - OIDeltaPercent float64 `json:"oi_delta_percent"` // 持仓量变化百分比 - OIDeltaValue float64 `json:"oi_delta_value"` // 持仓量变化价值 - PriceDeltaPercent float64 `json:"price_delta_percent"` // 价格变化百分比 - NetLong float64 `json:"net_long"` // 净多仓 - NetShort float64 `json:"net_short"` // 净空仓 + CurrentOI float64 `json:"current_oi"` // Current open interest + OIDelta float64 `json:"oi_delta"` // Open interest change + OIDeltaPercent float64 `json:"oi_delta_percent"` // Open interest change percentage + OIDeltaValue float64 `json:"oi_delta_value"` // Open interest change value + PriceDeltaPercent float64 `json:"price_delta_percent"` // Price change percentage + NetLong float64 `json:"net_long"` // Net long position + NetShort float64 `json:"net_short"` // Net short position } -// OITopAPIResponse OI Top API返回的数据结构 +// OITopAPIResponse data structure returned by OI Top API type OITopAPIResponse struct { Success bool `json:"success"` Data struct { @@ -401,7 +401,7 @@ type OITopAPIResponse struct { } `json:"data"` } -// OITopCache OI Top 缓存 +// OITopCache OI Top cache type OITopCache struct { Positions []OIPosition `json:"positions"` FetchedAt time.Time `json:"fetched_at"` @@ -418,56 +418,56 @@ var oiTopConfig = struct { CacheDir: "coin_pool_cache", } -// GetOITopPositions 获取持仓量增长Top20数据(带重试和缓存) +// GetOITopPositions retrieves OI Top 20 data (with retry and cache) func GetOITopPositions() ([]OIPosition, error) { - // 检查API URL是否配置 + // Check if API URL is configured if strings.TrimSpace(oiTopConfig.APIURL) == "" { - log.Printf("⚠️ 未配置OI Top API URL,跳过OI Top数据获取") - return []OIPosition{}, nil // 返回空列表,不是错误 + log.Printf("⚠️ OI Top API URL not configured, skipping OI Top data fetch") + return []OIPosition{}, nil // Return empty list, not an error } maxRetries := 3 var lastErr error - // 尝试从API获取 + // Try to fetch from API for attempt := 1; attempt <= maxRetries; attempt++ { if attempt > 1 { - log.Printf("⚠️ 第%d次重试获取OI Top数据(共%d次)...", attempt, maxRetries) + log.Printf("⚠️ Retry attempt %d of %d to fetch OI Top data...", attempt, maxRetries) time.Sleep(2 * time.Second) } positions, err := fetchOITop() if err == nil { if attempt > 1 { - log.Printf("✓ 第%d次重试成功", attempt) + log.Printf("✓ Retry attempt %d succeeded", attempt) } - // 成功获取后保存到缓存 + // Save to cache after successful fetch if err := saveOITopCache(positions); err != nil { - log.Printf("⚠️ 保存OI Top缓存失败: %v", err) + log.Printf("⚠️ Failed to save OI Top cache: %v", err) } return positions, nil } lastErr = err - log.Printf("❌ 第%d次请求OI Top失败: %v", attempt, err) + log.Printf("❌ OI Top request attempt %d failed: %v", attempt, err) } - // API获取失败,尝试使用缓存 - log.Printf("⚠️ OI Top API请求全部失败,尝试使用历史缓存数据...") + // API fetch failed, try to use cache + log.Printf("⚠️ All OI Top API requests failed, trying to use historical cache data...") cachedPositions, err := loadOITopCache() if err == nil { - log.Printf("✓ 使用历史OI Top缓存数据(共%d个币种)", len(cachedPositions)) + log.Printf("✓ Using historical OI Top cache data (%d coins)", len(cachedPositions)) return cachedPositions, nil } - // 缓存也失败,返回空列表(OI Top是可选的) - log.Printf("⚠️ 无法加载OI Top缓存数据(最后错误: %v),跳过OI Top数据", lastErr) + // Cache also failed, return empty list (OI Top is optional) + log.Printf("⚠️ Unable to load OI Top cache data (last error: %v), skipping OI Top data", lastErr) return []OIPosition{}, nil } -// fetchOITop 实际执行OI Top请求 +// fetchOITop actually executes OI Top request func fetchOITop() ([]OIPosition, error) { - log.Printf("🔄 正在请求OI Top数据...") + log.Printf("🔄 Requesting OI Top data...") client := &http.Client{ Timeout: oiTopConfig.Timeout, @@ -475,42 +475,42 @@ func fetchOITop() ([]OIPosition, error) { resp, err := client.Get(oiTopConfig.APIURL) if err != nil { - return nil, fmt.Errorf("请求OI Top API失败: %w", err) + return nil, fmt.Errorf("failed to request OI Top API: %w", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("读取OI Top响应失败: %w", err) + return nil, fmt.Errorf("failed to read OI Top response: %w", err) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("OI Top API返回错误 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("OI Top API returned error (status %d): %s", resp.StatusCode, string(body)) } - // 解析API响应 + // Parse API response var response OITopAPIResponse if err := json.Unmarshal(body, &response); err != nil { - return nil, fmt.Errorf("OI Top JSON解析失败: %w", err) + return nil, fmt.Errorf("OI Top JSON parsing failed: %w", err) } if !response.Success { - return nil, fmt.Errorf("OI Top API返回失败状态") + return nil, fmt.Errorf("OI Top API returned failure status") } if len(response.Data.Positions) == 0 { - return nil, fmt.Errorf("OI Top持仓列表为空") + return nil, fmt.Errorf("OI Top position list is empty") } - log.Printf("✓ 成功获取%d个OI Top币种(时间范围: %s)", + log.Printf("✓ Successfully fetched %d OI Top coins (time range: %s)", len(response.Data.Positions), response.Data.TimeRange) return response.Data.Positions, nil } -// saveOITopCache 保存OI Top数据到缓存 +// saveOITopCache saves OI Top data to cache func saveOITopCache(positions []OIPosition) error { if err := os.MkdirAll(oiTopConfig.CacheDir, 0755); err != nil { - return fmt.Errorf("创建缓存目录失败: %w", err) + return fmt.Errorf("failed to create cache directory: %w", err) } cache := OITopCache{ @@ -521,41 +521,41 @@ func saveOITopCache(positions []OIPosition) error { data, err := json.MarshalIndent(cache, "", " ") if err != nil { - return fmt.Errorf("序列化OI Top缓存数据失败: %w", err) + return fmt.Errorf("failed to serialize OI Top cache data: %w", err) } cachePath := filepath.Join(oiTopConfig.CacheDir, "oi_top_latest.json") if err := ioutil.WriteFile(cachePath, data, 0644); err != nil { - return fmt.Errorf("写入OI Top缓存文件失败: %w", err) + return fmt.Errorf("failed to write OI Top cache file: %w", err) } - log.Printf("💾 已保存OI Top缓存(%d个币种)", len(positions)) + log.Printf("💾 OI Top cache saved (%d coins)", len(positions)) return nil } -// loadOITopCache 从缓存加载OI Top数据 +// loadOITopCache loads OI Top data from cache func loadOITopCache() ([]OIPosition, error) { cachePath := filepath.Join(oiTopConfig.CacheDir, "oi_top_latest.json") if _, err := os.Stat(cachePath); os.IsNotExist(err) { - return nil, fmt.Errorf("OI Top缓存文件不存在") + return nil, fmt.Errorf("OI Top cache file does not exist") } data, err := ioutil.ReadFile(cachePath) if err != nil { - return nil, fmt.Errorf("读取OI Top缓存文件失败: %w", err) + return nil, fmt.Errorf("failed to read OI Top cache file: %w", err) } var cache OITopCache if err := json.Unmarshal(data, &cache); err != nil { - return nil, fmt.Errorf("解析OI Top缓存数据失败: %w", err) + return nil, fmt.Errorf("failed to parse OI Top cache data: %w", err) } cacheAge := time.Since(cache.FetchedAt) if cacheAge > 24*time.Hour { - log.Printf("⚠️ OI Top缓存数据较旧(%.1f小时前),但仍可使用", cacheAge.Hours()) + log.Printf("⚠️ OI Top cache data is old (%.1f hours ago), but still usable", cacheAge.Hours()) } else { - log.Printf("📂 OI Top缓存数据时间: %s(%.1f分钟前)", + log.Printf("📂 OI Top cache data timestamp: %s (%.1f minutes ago)", cache.FetchedAt.Format("2006-01-02 15:04:05"), cacheAge.Minutes()) } @@ -563,7 +563,7 @@ func loadOITopCache() ([]OIPosition, error) { return cache.Positions, nil } -// GetOITopSymbols 获取OI Top的币种符号列表 +// GetOITopSymbols retrieves OI Top coin symbol list func GetOITopSymbols() ([]string, error) { positions, err := GetOITopPositions() if err != nil { @@ -579,41 +579,41 @@ func GetOITopSymbols() ([]string, error) { return symbols, nil } -// MergedCoinPool 合并的币种池(AI500 + OI Top) +// MergedCoinPool merged coin pool (AI500 + OI Top) type MergedCoinPool struct { - AI500Coins []CoinInfo // AI500评分币种 - OITopCoins []OIPosition // 持仓量增长Top20 - AllSymbols []string // 所有不重复的币种符号 - SymbolSources map[string][]string // 每个币种的来源("ai500"/"oi_top") + AI500Coins []CoinInfo // AI500 score coins + OITopCoins []OIPosition // Open interest growth Top 20 + AllSymbols []string // All unique coin symbols + SymbolSources map[string][]string // Source of each coin ("ai500"/"oi_top") } -// GetMergedCoinPool 获取合并后的币种池(AI500 + OI Top,去重) +// GetMergedCoinPool retrieves merged coin pool (AI500 + OI Top, deduplicated) func GetMergedCoinPool(ai500Limit int) (*MergedCoinPool, error) { - // 1. 获取AI500数据 + // 1. Get AI500 data ai500TopSymbols, err := GetTopRatedCoins(ai500Limit) if err != nil { - log.Printf("⚠️ 获取AI500数据失败: %v", err) - ai500TopSymbols = []string{} // 失败时用空列表 + log.Printf("⚠️ Failed to get AI500 data: %v", err) + ai500TopSymbols = []string{} // Use empty list on failure } - // 2. 获取OI Top数据 + // 2. Get OI Top data oiTopSymbols, err := GetOITopSymbols() if err != nil { - log.Printf("⚠️ 获取OI Top数据失败: %v", err) - oiTopSymbols = []string{} // 失败时用空列表 + log.Printf("⚠️ Failed to get OI Top data: %v", err) + oiTopSymbols = []string{} // Use empty list on failure } - // 3. 合并并去重 + // 3. Merge and deduplicate symbolSet := make(map[string]bool) symbolSources := make(map[string][]string) - // 添加AI500币种 + // Add AI500 coins for _, symbol := range ai500TopSymbols { symbolSet[symbol] = true symbolSources[symbol] = append(symbolSources[symbol], "ai500") } - // 添加OI Top币种 + // Add OI Top coins for _, symbol := range oiTopSymbols { if !symbolSet[symbol] { symbolSet[symbol] = true @@ -621,13 +621,13 @@ func GetMergedCoinPool(ai500Limit int) (*MergedCoinPool, error) { symbolSources[symbol] = append(symbolSources[symbol], "oi_top") } - // 转换为数组 + // Convert to array var allSymbols []string for symbol := range symbolSet { allSymbols = append(allSymbols, symbol) } - // 获取完整数据 + // Get complete data ai500Coins, _ := GetCoinPool() oiTopPositions, _ := GetOITopPositions() @@ -638,7 +638,7 @@ func GetMergedCoinPool(ai500Limit int) (*MergedCoinPool, error) { SymbolSources: symbolSources, } - log.Printf("📊 币种池合并完成: AI500=%d, OI_Top=%d, 总计(去重)=%d", + log.Printf("📊 Coin pool merge complete: AI500=%d, OI_Top=%d, Total(deduplicated)=%d", len(ai500TopSymbols), len(oiTopSymbols), len(allSymbols)) return merged, nil diff --git a/scripts/migrate_encryption.go b/scripts/migrate_encryption.go index 2c4b9d38..cd1910ec 100644 --- a/scripts/migrate_encryption.go +++ b/scripts/migrate_encryption.go @@ -12,64 +12,64 @@ import ( ) func main() { - log.Println("🔄 开始迁移数据库到加密格式...") + log.Println("🔄 Starting database migration to encrypted format...") - // 1. 检查数据库文件 + // 1. Check database file dbPath := "data.db" if len(os.Args) > 1 { dbPath = os.Args[1] } if _, err := os.Stat(dbPath); os.IsNotExist(err) { - log.Fatalf("❌ 数据库文件不存在: %s", dbPath) + log.Fatalf("❌ Database file does not exist: %s", dbPath) } - // 2. 备份数据库 + // 2. Backup database backupPath := fmt.Sprintf("%s.pre_encryption_backup", dbPath) - log.Printf("📦 备份数据库到: %s", backupPath) + log.Printf("📦 Backing up database to: %s", backupPath) input, err := os.ReadFile(dbPath) if err != nil { - log.Fatalf("❌ 读取数据库失败: %v", err) + log.Fatalf("❌ Failed to read database: %v", err) } if err := os.WriteFile(backupPath, input, 0600); err != nil { - log.Fatalf("❌ 备份失败: %v", err) + log.Fatalf("❌ Backup failed: %v", err) } - // 3. 打开数据库 + // 3. Open database db, err := sql.Open("sqlite", dbPath) if err != nil { - log.Fatalf("❌ 打开数据库失败: %v", err) + log.Fatalf("❌ Failed to open database: %v", err) } defer db.Close() - // 4. 初始化 CryptoService(从环境变量加载密钥) + // 4. Initialize CryptoService (load key from environment variables) cs, err := crypto.NewCryptoService() if err != nil { - log.Fatalf("❌ 初始化加密服务失败: %v", err) + log.Fatalf("❌ Failed to initialize encryption service: %v", err) } - // 5. 迁移交易所配置 + // 5. Migrate exchange configurations if err := migrateExchanges(db, cs); err != nil { - log.Fatalf("❌ 迁移交易所配置失败: %v", err) + log.Fatalf("❌ Failed to migrate exchange configurations: %v", err) } - // 6. 迁移 AI 模型配置 + // 6. Migrate AI model configurations if err := migrateAIModels(db, cs); err != nil { - log.Fatalf("❌ 迁移 AI 模型配置失败: %v", err) + log.Fatalf("❌ Failed to migrate AI model configurations: %v", err) } - log.Println("✅ 数据迁移完成!") - log.Printf("📝 原始数据备份位于: %s", backupPath) - log.Println("⚠️ 请验证系统功能正常后,手动删除备份文件") + log.Println("✅ Data migration completed!") + log.Printf("📝 Original data backed up at: %s", backupPath) + log.Println("⚠️ Please verify system functionality before manually deleting backup file") } -// migrateExchanges 迁移交易所配置 +// migrateExchanges migrates exchange configurations func migrateExchanges(db *sql.DB, cs *crypto.CryptoService) error { - log.Println("🔄 迁移交易所配置...") + log.Println("🔄 Migrating exchange configurations...") - // 查询所有未加密的记录(加密数据以 ENC:v1: 开头) + // Query all unencrypted records (encrypted data starts with ENC:v1:) rows, err := db.Query(` SELECT user_id, id, api_key, secret_key, COALESCE(hyperliquid_private_key, ''), @@ -96,22 +96,22 @@ func migrateExchanges(db *sql.DB, cs *crypto.CryptoService) error { return err } - // 加密每个字段 + // Encrypt each field encAPIKey, err := cs.EncryptForStorage(apiKey) if err != nil { - return fmt.Errorf("加密 API Key 失败: %w", err) + return fmt.Errorf("failed to encrypt API Key: %w", err) } encSecretKey, err := cs.EncryptForStorage(secretKey) if err != nil { - return fmt.Errorf("加密 Secret Key 失败: %w", err) + return fmt.Errorf("failed to encrypt Secret Key: %w", err) } encHLPrivateKey := "" if hlPrivateKey != "" { encHLPrivateKey, err = cs.EncryptForStorage(hlPrivateKey) if err != nil { - return fmt.Errorf("加密 Hyperliquid Private Key 失败: %w", err) + return fmt.Errorf("failed to encrypt Hyperliquid Private Key: %w", err) } } @@ -119,11 +119,11 @@ func migrateExchanges(db *sql.DB, cs *crypto.CryptoService) error { if asterPrivateKey != "" { encAsterPrivateKey, err = cs.EncryptForStorage(asterPrivateKey) if err != nil { - return fmt.Errorf("加密 Aster Private Key 失败: %w", err) + return fmt.Errorf("failed to encrypt Aster Private Key: %w", err) } } - // 更新数据库 + // Update database _, err = tx.Exec(` UPDATE exchanges SET api_key = ?, secret_key = ?, @@ -132,10 +132,10 @@ func migrateExchanges(db *sql.DB, cs *crypto.CryptoService) error { `, encAPIKey, encSecretKey, encHLPrivateKey, encAsterPrivateKey, userID, exchangeID) if err != nil { - return fmt.Errorf("更新数据库失败: %w", err) + return fmt.Errorf("failed to update database: %w", err) } - log.Printf(" ✓ 已加密: [%s] %s", userID, exchangeID) + log.Printf(" ✓ Encrypted: [%s] %s", userID, exchangeID) count++ } @@ -143,13 +143,13 @@ func migrateExchanges(db *sql.DB, cs *crypto.CryptoService) error { return err } - log.Printf("✅ 已迁移 %d 个交易所配置", count) + log.Printf("✅ Migrated %d exchange configurations", count) return nil } -// migrateAIModels 迁移 AI 模型配置 +// migrateAIModels migrates AI model configurations func migrateAIModels(db *sql.DB, cs *crypto.CryptoService) error { - log.Println("🔄 迁移 AI 模型配置...") + log.Println("🔄 Migrating AI model configurations...") rows, err := db.Query(` SELECT user_id, id, api_key @@ -176,7 +176,7 @@ func migrateAIModels(db *sql.DB, cs *crypto.CryptoService) error { encAPIKey, err := cs.EncryptForStorage(apiKey) if err != nil { - return fmt.Errorf("加密 API Key 失败: %w", err) + return fmt.Errorf("failed to encrypt API Key: %w", err) } _, err = tx.Exec(` @@ -184,10 +184,10 @@ func migrateAIModels(db *sql.DB, cs *crypto.CryptoService) error { `, encAPIKey, userID, modelID) if err != nil { - return fmt.Errorf("更新数据库失败: %w", err) + return fmt.Errorf("failed to update database: %w", err) } - log.Printf(" ✓ 已加密: [%s] %s", userID, modelID) + log.Printf(" ✓ Encrypted: [%s] %s", userID, modelID) count++ } @@ -195,6 +195,6 @@ func migrateAIModels(db *sql.DB, cs *crypto.CryptoService) error { return err } - log.Printf("✅ 已迁移 %d 个 AI 模型配置", count) + log.Printf("✅ Migrated %d AI model configurations", count) return nil } diff --git a/store/ai_model.go b/store/ai_model.go index 556325e8..6a1e19d0 100644 --- a/store/ai_model.go +++ b/store/ai_model.go @@ -9,14 +9,14 @@ import ( "time" ) -// AIModelStore AI模型存储 +// AIModelStore AI model storage type AIModelStore struct { db *sql.DB encryptFunc func(string) string decryptFunc func(string) string } -// AIModel AI模型配置 +// AIModel AI model configuration type AIModel struct { ID string `json:"id"` UserID string `json:"user_id"` @@ -49,7 +49,7 @@ func (s *AIModelStore) initTables() error { return err } - // 触发器 + // Trigger _, err = s.db.Exec(` CREATE TRIGGER IF NOT EXISTS update_ai_models_updated_at AFTER UPDATE ON ai_models @@ -61,7 +61,7 @@ func (s *AIModelStore) initTables() error { return err } - // 向后兼容:添加可能缺失的列 + // Backward compatibility: add potentially missing columns s.db.Exec(`ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`) s.db.Exec(`ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`) @@ -82,7 +82,7 @@ func (s *AIModelStore) initDefaultData() error { VALUES (?, 'default', ?, ?, 0) `, model.id, model.name, model.provider) if err != nil { - return fmt.Errorf("初始化AI模型失败: %w", err) + return fmt.Errorf("failed to initialize AI model: %w", err) } } return nil @@ -102,7 +102,7 @@ func (s *AIModelStore) decrypt(encrypted string) string { return encrypted } -// List 获取用户的AI模型列表 +// List retrieves user's AI model list func (s *AIModelStore) List(userID string) ([]*AIModel, error) { rows, err := s.db.Query(` SELECT id, user_id, name, provider, enabled, api_key, @@ -136,10 +136,10 @@ func (s *AIModelStore) List(userID string) ([]*AIModel, error) { return models, nil } -// Get 获取单个AI模型 +// Get retrieves a single AI model func (s *AIModelStore) Get(userID, modelID string) (*AIModel, error) { if modelID == "" { - return nil, fmt.Errorf("模型ID不能为空") + return nil, fmt.Errorf("model ID cannot be empty") } candidates := []string{} @@ -178,7 +178,7 @@ func (s *AIModelStore) Get(userID, modelID string) (*AIModel, error) { return nil, sql.ErrNoRows } -// GetDefault 获取默认启用的AI模型 +// GetDefault retrieves the default enabled AI model func (s *AIModelStore) GetDefault(userID string) (*AIModel, error) { if userID == "" { userID = "default" @@ -193,7 +193,7 @@ func (s *AIModelStore) GetDefault(userID string) (*AIModel, error) { if userID != "default" { return s.firstEnabled("default") } - return nil, fmt.Errorf("请先在系统中配置可用的AI模型") + return nil, fmt.Errorf("please configure an available AI model in the system first") } func (s *AIModelStore) firstEnabled(userID string) (*AIModel, error) { @@ -218,9 +218,9 @@ func (s *AIModelStore) firstEnabled(userID string) (*AIModel, error) { return &model, nil } -// Update 更新AI模型,不存在则创建 +// Update updates AI model, creates if not exists func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error { - // 先尝试精确匹配ID + // Try exact ID match first var existingID string err := s.db.QueryRow(`SELECT id FROM ai_models WHERE user_id = ? AND id = ? LIMIT 1`, userID, id).Scan(&existingID) if err == nil { @@ -232,11 +232,11 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI return err } - // 尝试兼容旧逻辑:将id作为provider查找 + // Try legacy logic compatibility: use id as provider to search provider := id err = s.db.QueryRow(`SELECT id FROM ai_models WHERE user_id = ? AND provider = ? LIMIT 1`, userID, provider).Scan(&existingID) if err == nil { - logger.Warnf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID) + logger.Warnf("⚠️ Using legacy provider matching to update model: %s -> %s", provider, existingID) encryptedAPIKey := s.encrypt(apiKey) _, err = s.db.Exec(` UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now') @@ -245,7 +245,7 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI return err } - // 创建新记录 + // Create new record if provider == id && (provider == "deepseek" || provider == "qwen") { provider = id } else { @@ -274,7 +274,7 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI newModelID = fmt.Sprintf("%s_%s", userID, provider) } - logger.Infof("✓ 创建新的 AI 模型配置: ID=%s, Provider=%s, Name=%s", newModelID, provider, name) + logger.Infof("✓ Creating new AI model configuration: ID=%s, Provider=%s, Name=%s", newModelID, provider, name) encryptedAPIKey := s.encrypt(apiKey) _, err = s.db.Exec(` INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) @@ -283,7 +283,7 @@ func (s *AIModelStore) Update(userID, id string, enabled bool, apiKey, customAPI return err } -// Create 创建AI模型 +// Create creates an AI model func (s *AIModelStore) Create(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error { _, err := s.db.Exec(` INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url) diff --git a/store/backtest.go b/store/backtest.go index 89ecb14d..2ab4c846 100644 --- a/store/backtest.go +++ b/store/backtest.go @@ -7,12 +7,12 @@ import ( "time" ) -// BacktestStore 回测数据存储 +// BacktestStore backtest data storage type BacktestStore struct { db *sql.DB } -// RunState 回测状态 +// RunState backtest state type RunState string const ( @@ -23,7 +23,7 @@ const ( RunStateFailed RunState = "failed" ) -// RunMetadata 回测元数据 +// RunMetadata backtest metadata type RunMetadata struct { RunID string `json:"run_id"` UserID string `json:"user_id"` @@ -36,7 +36,7 @@ type RunMetadata struct { UpdatedAt time.Time `json:"updated_at"` } -// RunSummary 回测摘要 +// RunSummary backtest summary type RunSummary struct { SymbolCount int `json:"symbol_count"` DecisionTF string `json:"decision_tf"` @@ -48,7 +48,7 @@ type RunSummary struct { LiquidationNote string `json:"liquidation_note"` } -// EquityPoint 权益点 +// EquityPoint equity point type EquityPoint struct { Timestamp int64 `json:"timestamp"` Equity float64 `json:"equity"` @@ -59,7 +59,7 @@ type EquityPoint struct { Cycle int `json:"cycle"` } -// TradeEvent 交易事件 +// TradeEvent trade event type TradeEvent struct { Timestamp int64 `json:"timestamp"` Symbol string `json:"symbol"` @@ -78,7 +78,7 @@ type TradeEvent struct { Note string `json:"note"` } -// RunIndexEntry 回测索引条目 +// RunIndexEntry backtest index entry type RunIndexEntry struct { RunID string `json:"run_id"` State string `json:"state"` @@ -92,10 +92,10 @@ type RunIndexEntry struct { UpdatedAtISO string `json:"updated_at"` } -// initTables 初始化回测相关表 +// initTables initializes backtest related tables func (s *BacktestStore) initTables() error { queries := []string{ - // 回测运行主表 + // Backtest runs main table `CREATE TABLE IF NOT EXISTS backtest_runs ( run_id TEXT PRIMARY KEY, user_id TEXT NOT NULL DEFAULT '', @@ -120,7 +120,7 @@ func (s *BacktestStore) initTables() error { updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, - // 回测检查点 + // Backtest checkpoints `CREATE TABLE IF NOT EXISTS backtest_checkpoints ( run_id TEXT PRIMARY KEY, payload BLOB NOT NULL, @@ -128,7 +128,7 @@ func (s *BacktestStore) initTables() error { FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, - // 回测权益曲线 + // Backtest equity curve `CREATE TABLE IF NOT EXISTS backtest_equity ( id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL, @@ -142,7 +142,7 @@ func (s *BacktestStore) initTables() error { FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, - // 回测交易记录 + // Backtest trade records `CREATE TABLE IF NOT EXISTS backtest_trades ( id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL, @@ -164,7 +164,7 @@ func (s *BacktestStore) initTables() error { FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, - // 回测指标 + // Backtest metrics `CREATE TABLE IF NOT EXISTS backtest_metrics ( run_id TEXT PRIMARY KEY, payload BLOB NOT NULL, @@ -172,7 +172,7 @@ func (s *BacktestStore) initTables() error { FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, - // 回测决策日志 + // Backtest decision logs `CREATE TABLE IF NOT EXISTS backtest_decisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL, @@ -182,7 +182,7 @@ func (s *BacktestStore) initTables() error { FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, - // 索引 + // Indexes `CREATE INDEX IF NOT EXISTS idx_backtest_runs_state ON backtest_runs(state, updated_at)`, `CREATE INDEX IF NOT EXISTS idx_backtest_equity_run_ts ON backtest_equity(run_id, ts)`, `CREATE INDEX IF NOT EXISTS idx_backtest_trades_run_ts ON backtest_trades(run_id, ts)`, @@ -191,11 +191,11 @@ func (s *BacktestStore) initTables() error { for _, query := range queries { if _, err := s.db.Exec(query); err != nil { - return fmt.Errorf("执行SQL失败: %w", err) + return fmt.Errorf("failed to execute SQL: %w", err) } } - // 添加可能缺失的列(向后兼容) + // Add potentially missing columns (backward compatibility) s.addColumnIfNotExists("backtest_runs", "label", "TEXT DEFAULT ''") s.addColumnIfNotExists("backtest_runs", "last_error", "TEXT DEFAULT ''") s.addColumnIfNotExists("backtest_trades", "leverage", "INTEGER DEFAULT 0") @@ -219,14 +219,14 @@ func (s *BacktestStore) addColumnIfNotExists(table, column, definition string) { continue } if name == column { - return // 列已存在 + return // Column already exists } } s.db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, definition)) } -// SaveCheckpoint 保存检查点 +// SaveCheckpoint saves checkpoint func (s *BacktestStore) SaveCheckpoint(runID string, payload []byte) error { _, err := s.db.Exec(` INSERT INTO backtest_checkpoints (run_id, payload, updated_at) @@ -236,14 +236,14 @@ func (s *BacktestStore) SaveCheckpoint(runID string, payload []byte) error { return err } -// LoadCheckpoint 加载检查点 +// LoadCheckpoint loads checkpoint func (s *BacktestStore) LoadCheckpoint(runID string) ([]byte, error) { var payload []byte err := s.db.QueryRow(`SELECT payload FROM backtest_checkpoints WHERE run_id = ?`, runID).Scan(&payload) return payload, err } -// SaveRunMetadata 保存运行元数据 +// SaveRunMetadata saves run metadata func (s *BacktestStore) SaveRunMetadata(meta *RunMetadata) error { created := meta.CreatedAt.UTC().Format(time.RFC3339) updated := meta.UpdatedAt.UTC().Format(time.RFC3339) @@ -270,7 +270,7 @@ func (s *BacktestStore) SaveRunMetadata(meta *RunMetadata) error { return err } -// LoadRunMetadata 加载运行元数据 +// LoadRunMetadata loads run metadata func (s *BacktestStore) LoadRunMetadata(runID string) (*RunMetadata, error) { var ( userID string @@ -326,7 +326,7 @@ func (s *BacktestStore) LoadRunMetadata(runID string) (*RunMetadata, error) { return meta, nil } -// ListRunIDs 列出所有运行ID +// ListRunIDs lists all run IDs func (s *BacktestStore) ListRunIDs() ([]string, error) { rows, err := s.db.Query(`SELECT run_id FROM backtest_runs ORDER BY datetime(updated_at) DESC`) if err != nil { @@ -345,7 +345,7 @@ func (s *BacktestStore) ListRunIDs() ([]string, error) { return ids, rows.Err() } -// AppendEquityPoint 添加权益点 +// AppendEquityPoint appends equity point func (s *BacktestStore) AppendEquityPoint(runID string, point EquityPoint) error { _, err := s.db.Exec(` INSERT INTO backtest_equity (run_id, ts, equity, available, pnl, pnl_pct, dd_pct, cycle) @@ -355,7 +355,7 @@ func (s *BacktestStore) AppendEquityPoint(runID string, point EquityPoint) error return err } -// LoadEquityPoints 加载权益点 +// LoadEquityPoints loads equity points func (s *BacktestStore) LoadEquityPoints(runID string) ([]EquityPoint, error) { rows, err := s.db.Query(` SELECT ts, equity, available, pnl, pnl_pct, dd_pct, cycle @@ -378,7 +378,7 @@ func (s *BacktestStore) LoadEquityPoints(runID string) ([]EquityPoint, error) { return points, rows.Err() } -// AppendTradeEvent 添加交易事件 +// AppendTradeEvent appends trade event func (s *BacktestStore) AppendTradeEvent(runID string, event TradeEvent) error { _, err := s.db.Exec(` INSERT INTO backtest_trades (run_id, ts, symbol, action, side, qty, price, fee, @@ -391,7 +391,7 @@ func (s *BacktestStore) AppendTradeEvent(runID string, event TradeEvent) error { return err } -// LoadTradeEvents 加载交易事件 +// LoadTradeEvents loads trade events func (s *BacktestStore) LoadTradeEvents(runID string) ([]TradeEvent, error) { rows, err := s.db.Query(` SELECT ts, symbol, action, side, qty, price, fee, slippage, order_value, @@ -417,7 +417,7 @@ func (s *BacktestStore) LoadTradeEvents(runID string) ([]TradeEvent, error) { return events, rows.Err() } -// SaveMetrics 保存指标 +// SaveMetrics saves metrics func (s *BacktestStore) SaveMetrics(runID string, payload []byte) error { _, err := s.db.Exec(` INSERT INTO backtest_metrics (run_id, payload, updated_at) @@ -427,14 +427,14 @@ func (s *BacktestStore) SaveMetrics(runID string, payload []byte) error { return err } -// LoadMetrics 加载指标 +// LoadMetrics loads metrics func (s *BacktestStore) LoadMetrics(runID string) ([]byte, error) { var payload []byte err := s.db.QueryRow(`SELECT payload FROM backtest_metrics WHERE run_id = ?`, runID).Scan(&payload) return payload, err } -// SaveDecisionRecord 保存决策记录 +// SaveDecisionRecord saves decision record func (s *BacktestStore) SaveDecisionRecord(runID string, cycle int, payload []byte) error { _, err := s.db.Exec(` INSERT INTO backtest_decisions (run_id, cycle, payload) @@ -443,7 +443,7 @@ func (s *BacktestStore) SaveDecisionRecord(runID string, cycle int, payload []by return err } -// LoadDecisionRecords 加载决策记录 +// LoadDecisionRecords loads decision records func (s *BacktestStore) LoadDecisionRecords(runID string, limit, offset int) ([]json.RawMessage, error) { rows, err := s.db.Query(` SELECT payload FROM backtest_decisions @@ -467,7 +467,7 @@ func (s *BacktestStore) LoadDecisionRecords(runID string, limit, offset int) ([] return records, rows.Err() } -// LoadLatestDecision 加载最新决策 +// LoadLatestDecision loads latest decision func (s *BacktestStore) LoadLatestDecision(runID string, cycle int) ([]byte, error) { var query string var args []interface{} @@ -485,7 +485,7 @@ func (s *BacktestStore) LoadLatestDecision(runID string, cycle int) ([]byte, err return payload, err } -// UpdateProgress 更新进度 +// UpdateProgress updates progress func (s *BacktestStore) UpdateProgress(runID string, progressPct, equity float64, barIndex int, liquidated bool) error { _, err := s.db.Exec(` UPDATE backtest_runs @@ -495,7 +495,7 @@ func (s *BacktestStore) UpdateProgress(runID string, progressPct, equity float64 return err } -// ListIndexEntries 列出索引条目 +// ListIndexEntries lists index entries func (s *BacktestStore) ListIndexEntries() ([]RunIndexEntry, error) { rows, err := s.db.Query(` SELECT run_id, state, symbol_count, decision_tf, equity_last, max_drawdown_pct, @@ -524,7 +524,7 @@ func (s *BacktestStore) ListIndexEntries() ([]RunIndexEntry, error) { entry.UpdatedAtISO = updatedISO entry.Symbols = make([]string, 0, symbolCnt) - // 尝试从配置中提取更多信息 + // Try to extract more information from config if len(cfgJSON) > 0 { var cfg struct { Symbols []string `json:"symbols"` @@ -543,13 +543,13 @@ func (s *BacktestStore) ListIndexEntries() ([]RunIndexEntry, error) { return entries, rows.Err() } -// DeleteRun 删除运行 +// DeleteRun deletes run func (s *BacktestStore) DeleteRun(runID string) error { _, err := s.db.Exec(`DELETE FROM backtest_runs WHERE run_id = ?`, runID) return err } -// SaveConfig 保存配置 +// SaveConfig saves config func (s *BacktestStore) SaveConfig(runID, userID, template, customPrompt, provider, model string, override bool, configJSON []byte) error { now := time.Now().UTC().Format(time.RFC3339) if userID == "" { @@ -575,7 +575,7 @@ func (s *BacktestStore) SaveConfig(runID, userID, template, customPrompt, provid return err } -// LoadConfig 加载配置 +// LoadConfig loads config func (s *BacktestStore) LoadConfig(runID string) ([]byte, error) { var payload []byte err := s.db.QueryRow(`SELECT config_json FROM backtest_runs WHERE run_id = ?`, runID).Scan(&payload) diff --git a/store/decision.go b/store/decision.go index b122b78b..6641f9dd 100644 --- a/store/decision.go +++ b/store/decision.go @@ -7,12 +7,12 @@ import ( "time" ) -// DecisionStore 决策日志存储 +// DecisionStore decision log storage type DecisionStore struct { db *sql.DB } -// DecisionRecord 决策记录 +// DecisionRecord decision record type DecisionRecord struct { ID int64 `json:"id"` TraderID string `json:"trader_id"` @@ -32,7 +32,7 @@ type DecisionRecord struct { Decisions []DecisionAction `json:"decisions"` } -// AccountSnapshot 账户状态快照 +// AccountSnapshot account state snapshot type AccountSnapshot struct { TotalBalance float64 `json:"total_balance"` AvailableBalance float64 `json:"available_balance"` @@ -42,7 +42,7 @@ type AccountSnapshot struct { InitialBalance float64 `json:"initial_balance"` } -// PositionSnapshot 持仓快照 +// PositionSnapshot position snapshot type PositionSnapshot struct { Symbol string `json:"symbol"` Side string `json:"side"` @@ -54,8 +54,8 @@ type PositionSnapshot struct { LiquidationPrice float64 `json:"liquidation_price"` } -// DecisionAction 决策动作 -type DecisionAction struct { +// DecisionAction decision action +type DecisionAction struct{ Action string `json:"action"` Symbol string `json:"symbol"` Quantity float64 `json:"quantity"` @@ -67,7 +67,7 @@ type DecisionAction struct { Error string `json:"error"` } -// Statistics 统计信息 +// Statistics statistics information type Statistics struct { TotalCycles int `json:"total_cycles"` SuccessfulCycles int `json:"successful_cycles"` @@ -76,11 +76,11 @@ type Statistics struct { TotalClosePositions int `json:"total_close_positions"` } -// initTables 初始化 AI 决策日志表 -// 注意:账户净值曲线数据已迁移到 trader_equity_snapshots 表(由 EquityStore 管理) +// initTables initializes AI decision log tables +// Note: Account equity curve data has been migrated to trader_equity_snapshots table (managed by EquityStore) func (s *DecisionStore) initTables() error { queries := []string{ - // AI 决策日志表(记录 AI 的输入输出、思维链等) + // AI decision log table (records AI input/output, chain of thought, etc.) `CREATE TABLE IF NOT EXISTS decision_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, trader_id TEXT NOT NULL, @@ -97,21 +97,21 @@ func (s *DecisionStore) initTables() error { ai_request_duration_ms INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, - // 索引 + // Indexes `CREATE INDEX IF NOT EXISTS idx_decision_records_trader_time ON decision_records(trader_id, timestamp DESC)`, `CREATE INDEX IF NOT EXISTS idx_decision_records_timestamp ON decision_records(timestamp DESC)`, } for _, query := range queries { if _, err := s.db.Exec(query); err != nil { - return fmt.Errorf("执行SQL失败: %w", err) + return fmt.Errorf("failed to execute SQL: %w", err) } } return nil } -// LogDecision 记录决策(仅保存 AI 决策日志,净值曲线已迁移到 equity 表) +// LogDecision logs decision (only saves AI decision log, equity curve has been migrated to equity table) func (s *DecisionStore) LogDecision(record *DecisionRecord) error { if record.Timestamp.IsZero() { record.Timestamp = time.Now().UTC() @@ -119,11 +119,11 @@ func (s *DecisionStore) LogDecision(record *DecisionRecord) error { record.Timestamp = record.Timestamp.UTC() } - // 序列化候选币种和执行日志为 JSON + // Serialize candidate coins and execution log to JSON candidateCoinsJSON, _ := json.Marshal(record.CandidateCoins) executionLogJSON, _ := json.Marshal(record.ExecutionLog) - // 插入决策记录主表(仅保存 AI 决策相关内容) + // Insert decision record main table (only save AI decision related content) result, err := s.db.Exec(` INSERT INTO decision_records ( trader_id, cycle_number, timestamp, system_prompt, input_prompt, @@ -137,19 +137,19 @@ func (s *DecisionStore) LogDecision(record *DecisionRecord) error { record.Success, record.ErrorMessage, record.AIRequestDurationMs, ) if err != nil { - return fmt.Errorf("插入决策记录失败: %w", err) + return fmt.Errorf("failed to insert decision record: %w", err) } decisionID, err := result.LastInsertId() if err != nil { - return fmt.Errorf("获取决策ID失败: %w", err) + return fmt.Errorf("failed to get decision ID: %w", err) } record.ID = decisionID return nil } -// GetLatestRecords 获取指定交易员最近N条记录(按时间正序:从旧到新) +// GetLatestRecords gets the latest N records for specified trader (sorted by time in ascending order: old to new) func (s *DecisionStore) GetLatestRecords(traderID string, n int) ([]*DecisionRecord, error) { rows, err := s.db.Query(` SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt, @@ -161,7 +161,7 @@ func (s *DecisionStore) GetLatestRecords(traderID string, n int) ([]*DecisionRec LIMIT ? `, traderID, n) if err != nil { - return nil, fmt.Errorf("查询决策记录失败: %w", err) + return nil, fmt.Errorf("failed to query decision records: %w", err) } defer rows.Close() @@ -174,12 +174,12 @@ func (s *DecisionStore) GetLatestRecords(traderID string, n int) ([]*DecisionRec records = append(records, record) } - // 填充关联数据 + // Fill associated data for _, record := range records { s.fillRecordDetails(record) } - // 反转数组,让时间从旧到新排列 + // Reverse array to sort time from old to new for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 { records[i], records[j] = records[j], records[i] } @@ -187,7 +187,7 @@ func (s *DecisionStore) GetLatestRecords(traderID string, n int) ([]*DecisionRec return records, nil } -// GetAllLatestRecords 获取所有交易员最近N条记录 +// GetAllLatestRecords gets the latest N records for all traders func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) { rows, err := s.db.Query(` SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt, @@ -198,7 +198,7 @@ func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) { LIMIT ? `, n) if err != nil { - return nil, fmt.Errorf("查询决策记录失败: %w", err) + return nil, fmt.Errorf("failed to query decision records: %w", err) } defer rows.Close() @@ -211,7 +211,7 @@ func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) { records = append(records, record) } - // 反转数组 + // Reverse array for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 { records[i], records[j] = records[j], records[i] } @@ -219,7 +219,7 @@ func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) { return records, nil } -// GetRecordsByDate 获取指定交易员指定日期的所有记录 +// GetRecordsByDate gets all records for a specified trader on a specified date func (s *DecisionStore) GetRecordsByDate(traderID string, date time.Time) ([]*DecisionRecord, error) { dateStr := date.Format("2006-01-02") @@ -232,7 +232,7 @@ func (s *DecisionStore) GetRecordsByDate(traderID string, date time.Time) ([]*De ORDER BY timestamp ASC `, traderID, dateStr) if err != nil { - return nil, fmt.Errorf("查询决策记录失败: %w", err) + return nil, fmt.Errorf("failed to query decision records: %w", err) } defer rows.Close() @@ -248,7 +248,7 @@ func (s *DecisionStore) GetRecordsByDate(traderID string, date time.Time) ([]*De return records, nil } -// CleanOldRecords 清理N天前的旧记录 +// CleanOldRecords cleans old records from N days ago func (s *DecisionStore) CleanOldRecords(traderID string, days int) (int64, error) { cutoffTime := time.Now().AddDate(0, 0, -days).Format(time.RFC3339) @@ -257,13 +257,13 @@ func (s *DecisionStore) CleanOldRecords(traderID string, days int) (int64, error WHERE trader_id = ? AND timestamp < ? `, traderID, cutoffTime) if err != nil { - return 0, fmt.Errorf("清理旧记录失败: %w", err) + return 0, fmt.Errorf("failed to clean old records: %w", err) } return result.RowsAffected() } -// GetStatistics 获取指定交易员的统计信息 +// GetStatistics gets statistics information for specified trader func (s *DecisionStore) GetStatistics(traderID string) (*Statistics, error) { stats := &Statistics{} @@ -271,24 +271,24 @@ func (s *DecisionStore) GetStatistics(traderID string) (*Statistics, error) { SELECT COUNT(*) FROM decision_records WHERE trader_id = ? `, traderID).Scan(&stats.TotalCycles) if err != nil { - return nil, fmt.Errorf("查询总周期数失败: %w", err) + return nil, fmt.Errorf("failed to query total cycles: %w", err) } err = s.db.QueryRow(` SELECT COUNT(*) FROM decision_records WHERE trader_id = ? AND success = 1 `, traderID).Scan(&stats.SuccessfulCycles) if err != nil { - return nil, fmt.Errorf("查询成功周期数失败: %w", err) + return nil, fmt.Errorf("failed to query successful cycles: %w", err) } stats.FailedCycles = stats.TotalCycles - stats.SuccessfulCycles - // 从 trader_orders 表统计开仓次数 + // Count open positions from trader_orders table s.db.QueryRow(` SELECT COUNT(*) FROM trader_orders WHERE trader_id = ? AND status = 'FILLED' AND action IN ('open_long', 'open_short') `, traderID).Scan(&stats.TotalOpenPositions) - // 从 trader_orders 表统计平仓次数 + // Count close positions from trader_orders table s.db.QueryRow(` SELECT COUNT(*) FROM trader_orders WHERE trader_id = ? AND status = 'FILLED' AND action IN ('close_long', 'close_short', 'auto_close_long', 'auto_close_short') @@ -297,7 +297,7 @@ func (s *DecisionStore) GetStatistics(traderID string) (*Statistics, error) { return stats, nil } -// GetAllStatistics 获取所有交易员的统计信息 +// GetAllStatistics gets statistics information for all traders func (s *DecisionStore) GetAllStatistics() (*Statistics, error) { stats := &Statistics{} @@ -305,7 +305,7 @@ func (s *DecisionStore) GetAllStatistics() (*Statistics, error) { s.db.QueryRow(`SELECT COUNT(*) FROM decision_records WHERE success = 1`).Scan(&stats.SuccessfulCycles) stats.FailedCycles = stats.TotalCycles - stats.SuccessfulCycles - // 从 trader_orders 表统计 + // Count from trader_orders table s.db.QueryRow(` SELECT COUNT(*) FROM trader_orders WHERE status = 'FILLED' AND action IN ('open_long', 'open_short') @@ -319,7 +319,7 @@ func (s *DecisionStore) GetAllStatistics() (*Statistics, error) { return stats, nil } -// GetLastCycleNumber 获取指定交易员的最后周期编号 +// GetLastCycleNumber gets the last cycle number for specified trader func (s *DecisionStore) GetLastCycleNumber(traderID string) (int, error) { var cycleNumber int err := s.db.QueryRow(` @@ -331,7 +331,7 @@ func (s *DecisionStore) GetLastCycleNumber(traderID string) (int, error) { return cycleNumber, nil } -// scanDecisionRecord 从行中扫描决策记录 +// scanDecisionRecord scans decision record from row func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, error) { var record DecisionRecord var timestampStr string @@ -354,11 +354,11 @@ func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, err return &record, nil } -// fillRecordDetails 填充决策记录的关联数据(旧的关联表已删除,此函数保留用于兼容性) -// 注意:账户快照、持仓快照、决策动作等数据已不再存储在 decision 相关表中 -// - 净值数据请使用 EquityStore.GetLatest() -// - 订单数据请使用 OrderStore +// fillRecordDetails fills associated data for decision record (old associated tables removed, this function kept for compatibility) +// Note: Account snapshot, position snapshot, decision action data are no longer stored in decision related tables +// - For equity data use EquityStore.GetLatest() +// - For order data use OrderStore func (s *DecisionStore) fillRecordDetails(record *DecisionRecord) { - // 旧的关联表已删除,不再需要填充 - // AccountState, Positions, Decisions 字段将保持为零值 + // Old associated tables removed, no longer need to fill + // AccountState, Positions, Decisions fields will remain at zero values } diff --git a/store/equity.go b/store/equity.go index b8351433..34ec2c77 100644 --- a/store/equity.go +++ b/store/equity.go @@ -6,27 +6,27 @@ import ( "time" ) -// EquityStore 账户净值存储(用于绘制收益率曲线) +// EquityStore account equity storage (for plotting return curves) type EquityStore struct { db *sql.DB } -// EquitySnapshot 净值快照 +// EquitySnapshot equity snapshot type EquitySnapshot struct { ID int64 `json:"id"` TraderID string `json:"trader_id"` Timestamp time.Time `json:"timestamp"` - TotalEquity float64 `json:"total_equity"` // 账户净值 (余额 + 未实现盈亏) - Balance float64 `json:"balance"` // 账户余额 - UnrealizedPnL float64 `json:"unrealized_pnl"` // 未实现盈亏 - PositionCount int `json:"position_count"` // 持仓数量 - MarginUsedPct float64 `json:"margin_used_pct"` // 保证金使用率 + TotalEquity float64 `json:"total_equity"` // Account equity (balance + unrealized PnL) + Balance float64 `json:"balance"` // Account balance + UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized profit and loss + PositionCount int `json:"position_count"` // Position count + MarginUsedPct float64 `json:"margin_used_pct"` // Margin usage percentage } -// initTables 初始化净值表 +// initTables initializes equity tables func (s *EquityStore) initTables() error { queries := []string{ - // 净值快照表 - 专门用于收益率曲线 + // Equity snapshot table - specifically for return curves `CREATE TABLE IF NOT EXISTS trader_equity_snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, trader_id TEXT NOT NULL, @@ -38,21 +38,21 @@ func (s *EquityStore) initTables() error { margin_used_pct REAL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, - // 索引 + // Indexes `CREATE INDEX IF NOT EXISTS idx_equity_trader_time ON trader_equity_snapshots(trader_id, timestamp DESC)`, `CREATE INDEX IF NOT EXISTS idx_equity_timestamp ON trader_equity_snapshots(timestamp DESC)`, } for _, query := range queries { if _, err := s.db.Exec(query); err != nil { - return fmt.Errorf("执行SQL失败: %w", err) + return fmt.Errorf("failed to execute SQL: %w", err) } } return nil } -// Save 保存净值快照 +// Save saves equity snapshot func (s *EquityStore) Save(snapshot *EquitySnapshot) error { if snapshot.Timestamp.IsZero() { snapshot.Timestamp = time.Now().UTC() @@ -75,7 +75,7 @@ func (s *EquityStore) Save(snapshot *EquitySnapshot) error { snapshot.MarginUsedPct, ) if err != nil { - return fmt.Errorf("保存净值快照失败: %w", err) + return fmt.Errorf("failed to save equity snapshot: %w", err) } id, _ := result.LastInsertId() @@ -83,7 +83,7 @@ func (s *EquityStore) Save(snapshot *EquitySnapshot) error { return nil } -// GetLatest 获取指定交易员最近N条净值记录(按时间正序:从旧到新) +// GetLatest gets the latest N equity records for specified trader (sorted in ascending chronological order: old to new) func (s *EquityStore) GetLatest(traderID string, limit int) ([]*EquitySnapshot, error) { rows, err := s.db.Query(` SELECT id, trader_id, timestamp, total_equity, balance, @@ -94,7 +94,7 @@ func (s *EquityStore) GetLatest(traderID string, limit int) ([]*EquitySnapshot, LIMIT ? `, traderID, limit) if err != nil { - return nil, fmt.Errorf("查询净值记录失败: %w", err) + return nil, fmt.Errorf("failed to query equity records: %w", err) } defer rows.Close() @@ -113,7 +113,7 @@ func (s *EquityStore) GetLatest(traderID string, limit int) ([]*EquitySnapshot, snapshots = append(snapshots, snap) } - // 反转数组,让时间从旧到新排列(适合绘制曲线) + // Reverse the array to sort time from old to new (suitable for plotting curves) for i, j := 0, len(snapshots)-1; i < j; i, j = i+1, j-1 { snapshots[i], snapshots[j] = snapshots[j], snapshots[i] } @@ -121,7 +121,7 @@ func (s *EquityStore) GetLatest(traderID string, limit int) ([]*EquitySnapshot, return snapshots, nil } -// GetByTimeRange 获取指定时间范围内的净值记录 +// GetByTimeRange gets equity records within specified time range func (s *EquityStore) GetByTimeRange(traderID string, start, end time.Time) ([]*EquitySnapshot, error) { rows, err := s.db.Query(` SELECT id, trader_id, timestamp, total_equity, balance, @@ -131,7 +131,7 @@ func (s *EquityStore) GetByTimeRange(traderID string, start, end time.Time) ([]* ORDER BY timestamp ASC `, traderID, start.Format(time.RFC3339), end.Format(time.RFC3339)) if err != nil { - return nil, fmt.Errorf("查询净值记录失败: %w", err) + return nil, fmt.Errorf("failed to query equity records: %w", err) } defer rows.Close() @@ -153,7 +153,7 @@ func (s *EquityStore) GetByTimeRange(traderID string, start, end time.Time) ([]* return snapshots, nil } -// GetAllTradersLatest 获取所有交易员的最新净值(用于排行榜) +// GetAllTradersLatest gets latest equity for all traders (for leaderboards) func (s *EquityStore) GetAllTradersLatest() (map[string]*EquitySnapshot, error) { rows, err := s.db.Query(` SELECT e.id, e.trader_id, e.timestamp, e.total_equity, e.balance, @@ -166,7 +166,7 @@ func (s *EquityStore) GetAllTradersLatest() (map[string]*EquitySnapshot, error) ) latest ON e.trader_id = latest.trader_id AND e.timestamp = latest.max_ts `) if err != nil { - return nil, fmt.Errorf("查询最新净值失败: %w", err) + return nil, fmt.Errorf("failed to query latest equity: %w", err) } defer rows.Close() @@ -188,7 +188,7 @@ func (s *EquityStore) GetAllTradersLatest() (map[string]*EquitySnapshot, error) return result, nil } -// CleanOldRecords 清理N天前的旧记录 +// CleanOldRecords cleans old records from N days ago func (s *EquityStore) CleanOldRecords(traderID string, days int) (int64, error) { cutoffTime := time.Now().AddDate(0, 0, -days).Format(time.RFC3339) @@ -197,13 +197,13 @@ func (s *EquityStore) CleanOldRecords(traderID string, days int) (int64, error) WHERE trader_id = ? AND timestamp < ? `, traderID, cutoffTime) if err != nil { - return 0, fmt.Errorf("清理旧记录失败: %w", err) + return 0, fmt.Errorf("failed to clean old records: %w", err) } return result.RowsAffected() } -// GetCount 获取指定交易员的记录数 +// GetCount gets record count for specified trader func (s *EquityStore) GetCount(traderID string) (int, error) { var count int err := s.db.QueryRow(` @@ -212,26 +212,26 @@ func (s *EquityStore) GetCount(traderID string) (int, error) { return count, err } -// MigrateFromDecision 从旧的 decision_account_snapshots 迁移数据 +// MigrateFromDecision migrates data from old decision_account_snapshots table func (s *EquityStore) MigrateFromDecision() (int64, error) { - // 检查是否需要迁移(新表是否为空) + // Check if migration is needed (whether new table is empty) var count int s.db.QueryRow(`SELECT COUNT(*) FROM trader_equity_snapshots`).Scan(&count) if count > 0 { - return 0, nil // 已有数据,跳过迁移 + return 0, nil // Already has data, skip migration } - // 检查旧表是否存在 + // Check if old table exists var tableName string err := s.db.QueryRow(` SELECT name FROM sqlite_master WHERE type='table' AND name='decision_account_snapshots' `).Scan(&tableName) if err != nil { - return 0, nil // 旧表不存在,跳过 + return 0, nil // Old table doesn't exist, skip } - // 迁移数据:从 decision_records + decision_account_snapshots 联合查询 + // Migrate data: join query from decision_records + decision_account_snapshots result, err := s.db.Exec(` INSERT INTO trader_equity_snapshots ( trader_id, timestamp, total_equity, balance, @@ -250,7 +250,7 @@ func (s *EquityStore) MigrateFromDecision() (int64, error) { ORDER BY dr.timestamp ASC `) if err != nil { - return 0, fmt.Errorf("迁移数据失败: %w", err) + return 0, fmt.Errorf("failed to migrate data: %w", err) } return result.RowsAffected() diff --git a/store/exchange.go b/store/exchange.go index e9981485..a4819ff1 100644 --- a/store/exchange.go +++ b/store/exchange.go @@ -8,14 +8,14 @@ import ( "time" ) -// ExchangeStore 交易所存储 +// ExchangeStore exchange storage type ExchangeStore struct { db *sql.DB encryptFunc func(string) string decryptFunc func(string) string } -// Exchange 交易所配置 +// Exchange exchange configuration type Exchange struct { ID string `json:"id"` UserID string `json:"user_id"` @@ -24,7 +24,7 @@ type Exchange struct { Enabled bool `json:"enabled"` APIKey string `json:"apiKey"` SecretKey string `json:"secretKey"` - Passphrase string `json:"passphrase"` // OKX专用 + Passphrase string `json:"passphrase"` // OKX-specific Testnet bool `json:"testnet"` HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` AsterUser string `json:"asterUser"` @@ -65,10 +65,10 @@ func (s *ExchangeStore) initTables() error { return err } - // 迁移:添加 passphrase 列(如果不存在) + // Migration: add passphrase column (if not exists) s.db.Exec(`ALTER TABLE exchanges ADD COLUMN passphrase TEXT DEFAULT ''`) - // 触发器 + // Trigger _, err = s.db.Exec(` CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at AFTER UPDATE ON exchanges @@ -97,7 +97,7 @@ func (s *ExchangeStore) initDefaultData() error { VALUES (?, 'default', ?, ?, 0) `, exchange.id, exchange.name, exchange.typ) if err != nil { - return fmt.Errorf("初始化交易所失败: %w", err) + return fmt.Errorf("failed to initialize exchange: %w", err) } } return nil @@ -117,7 +117,7 @@ func (s *ExchangeStore) decrypt(encrypted string) string { return encrypted } -// EnsureUserExchanges 确保用户有所有支持的交易所记录 +// EnsureUserExchanges ensures user has records for all supported exchanges func (s *ExchangeStore) EnsureUserExchanges(userID string) error { exchanges := []struct { id, name, typ string @@ -136,17 +136,17 @@ func (s *ExchangeStore) EnsureUserExchanges(userID string) error { VALUES (?, ?, ?, ?, 0) `, exchange.id, userID, exchange.name, exchange.typ) if err != nil { - return fmt.Errorf("确保用户交易所失败: %w", err) + return fmt.Errorf("failed to ensure user exchanges: %w", err) } } return nil } -// List 获取用户的交易所列表 +// List gets user's exchange list func (s *ExchangeStore) List(userID string) ([]*Exchange, error) { - // 确保用户有所有支持的交易所记录 + // Ensure user has records for all supported exchanges if err := s.EnsureUserExchanges(userID); err != nil { - logger.Debugf("⚠️ 确保用户交易所记录失败: %v", err) + logger.Debugf("Warning: failed to ensure user exchange records: %v", err) } rows, err := s.db.Query(` @@ -194,7 +194,7 @@ func (s *ExchangeStore) List(userID string) ([]*Exchange, error) { return exchanges, nil } -// Update 更新交易所配置 +// Update updates exchange configuration func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKey, passphrase string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey, lighterApiKeyPrivateKey string) error { @@ -246,7 +246,7 @@ func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKe rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { - // 创建新记录,type 使用交易所 ID 以便后续正确识别 + // Create new record, use exchange ID as type for correct identification var name, typ string switch id { case "binance": @@ -278,7 +278,7 @@ func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKe return nil } -// Create 创建交易所配置 +// Create creates exchange configuration func (s *ExchangeStore) Create(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { _, err := s.db.Exec(` diff --git a/store/order.go b/store/order.go index 68649b47..752d4a49 100644 --- a/store/order.go +++ b/store/order.go @@ -7,73 +7,73 @@ import ( "time" ) -// TraderOrder 交易员订单记录 +// TraderOrder trader order record type TraderOrder struct { ID int64 `json:"id"` - TraderID string `json:"trader_id"` // 交易员ID - OrderID string `json:"order_id"` // 交易所订单ID - ClientOrderID string `json:"client_order_id"` // 客户端订单ID - Symbol string `json:"symbol"` // 交易对 + TraderID string `json:"trader_id"` // Trader ID + OrderID string `json:"order_id"` // Exchange order ID + ClientOrderID string `json:"client_order_id"` // Client order ID + Symbol string `json:"symbol"` // Trading pair Side string `json:"side"` // BUY/SELL PositionSide string `json:"position_side"` // LONG/SHORT/BOTH Action string `json:"action"` // open_long/close_long/open_short/close_short OrderType string `json:"order_type"` // MARKET/LIMIT - Quantity float64 `json:"quantity"` // 订单数量 - Price float64 `json:"price"` // 订单价格 - AvgPrice float64 `json:"avg_price"` // 实际成交均价 - ExecutedQty float64 `json:"executed_qty"` // 已成交数量 - Leverage int `json:"leverage"` // 杠杆倍数 + Quantity float64 `json:"quantity"` // Order quantity + Price float64 `json:"price"` // Order price + AvgPrice float64 `json:"avg_price"` // Actual average execution price + ExecutedQty float64 `json:"executed_qty"` // Executed quantity + Leverage int `json:"leverage"` // Leverage multiplier Status string `json:"status"` // NEW/FILLED/CANCELED/EXPIRED - Fee float64 `json:"fee"` // 手续费 - FeeAsset string `json:"fee_asset"` // 手续费资产 - RealizedPnL float64 `json:"realized_pnl"` // 已实现盈亏(平仓时) - EntryPrice float64 `json:"entry_price"` // 开仓价(平仓时记录) + Fee float64 `json:"fee"` // Fee + FeeAsset string `json:"fee_asset"` // Fee asset + RealizedPnL float64 `json:"realized_pnl"` // Realized PnL (when closing) + EntryPrice float64 `json:"entry_price"` // Entry price (recorded when closing) CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - FilledAt time.Time `json:"filled_at"` // 成交时间 + FilledAt time.Time `json:"filled_at"` // Filled time } -// TraderStats 交易统计指标 +// TraderStats trading statistics metrics type TraderStats struct { - TotalTrades int `json:"total_trades"` // 总交易数(已平仓) - WinTrades int `json:"win_trades"` // 盈利交易数 - LossTrades int `json:"loss_trades"` // 亏损交易数 - WinRate float64 `json:"win_rate"` // 胜率 (%) - ProfitFactor float64 `json:"profit_factor"` // 盈亏比 - SharpeRatio float64 `json:"sharpe_ratio"` // 夏普比 - TotalPnL float64 `json:"total_pnl"` // 总盈亏 - TotalFee float64 `json:"total_fee"` // 总手续费 - AvgWin float64 `json:"avg_win"` // 平均盈利 - AvgLoss float64 `json:"avg_loss"` // 平均亏损 - MaxDrawdownPct float64 `json:"max_drawdown_pct"` // 最大回撤 (%) + TotalTrades int `json:"total_trades"` // Total trades (closed) + WinTrades int `json:"win_trades"` // Winning trades + LossTrades int `json:"loss_trades"` // Losing trades + WinRate float64 `json:"win_rate"` // Win rate (%) + ProfitFactor float64 `json:"profit_factor"` // Profit factor + SharpeRatio float64 `json:"sharpe_ratio"` // Sharpe ratio + TotalPnL float64 `json:"total_pnl"` // Total PnL + TotalFee float64 `json:"total_fee"` // Total fees + AvgWin float64 `json:"avg_win"` // Average win + AvgLoss float64 `json:"avg_loss"` // Average loss + MaxDrawdownPct float64 `json:"max_drawdown_pct"` // Max drawdown (%) } -// CompletedOrder 已完成订单(用于AI输入) +// CompletedOrder completed order (for AI input) type CompletedOrder struct { - Symbol string `json:"symbol"` // 交易对 + Symbol string `json:"symbol"` // Trading pair Action string `json:"action"` // close_long/close_short Side string `json:"side"` // long/short - Quantity float64 `json:"quantity"` // 数量 - EntryPrice float64 `json:"entry_price"` // 开仓价 - ExitPrice float64 `json:"exit_price"` // 平仓价 - RealizedPnL float64 `json:"realized_pnl"` // 已实现盈亏 - PnLPct float64 `json:"pnl_pct"` // 盈亏百分比 - Fee float64 `json:"fee"` // 手续费 - Leverage int `json:"leverage"` // 杠杆 - FilledAt time.Time `json:"filled_at"` // 成交时间 + Quantity float64 `json:"quantity"` // Quantity + EntryPrice float64 `json:"entry_price"` // Entry price + ExitPrice float64 `json:"exit_price"` // Exit price + RealizedPnL float64 `json:"realized_pnl"` // Realized PnL + PnLPct float64 `json:"pnl_pct"` // PnL percentage + Fee float64 `json:"fee"` // Fee + Leverage int `json:"leverage"` // Leverage + FilledAt time.Time `json:"filled_at"` // Filled time } -// OrderStore 订单存储 +// OrderStore order storage type OrderStore struct { db *sql.DB } -// NewOrderStore 创建订单存储实例 +// NewOrderStore creates order storage instance func NewOrderStore(db *sql.DB) *OrderStore { return &OrderStore{db: db} } -// InitTables 初始化订单表 +// InitTables initializes order tables func (s *OrderStore) InitTables() error { _, err := s.db.Exec(` CREATE TABLE IF NOT EXISTS trader_orders ( @@ -103,10 +103,10 @@ func (s *OrderStore) InitTables() error { ) `) if err != nil { - return fmt.Errorf("创建trader_orders表失败: %w", err) + return fmt.Errorf("failed to create trader_orders table: %w", err) } - // 创建索引 + // Create indexes indices := []string{ `CREATE INDEX IF NOT EXISTS idx_trader_orders_trader ON trader_orders(trader_id)`, `CREATE INDEX IF NOT EXISTS idx_trader_orders_status ON trader_orders(trader_id, status)`, @@ -115,14 +115,14 @@ func (s *OrderStore) InitTables() error { } for _, idx := range indices { if _, err := s.db.Exec(idx); err != nil { - return fmt.Errorf("创建索引失败: %w", err) + return fmt.Errorf("failed to create index: %w", err) } } return nil } -// Create 创建订单记录 +// Create creates order record func (s *OrderStore) Create(order *TraderOrder) error { now := time.Now().Format(time.RFC3339) result, err := s.db.Exec(` @@ -140,7 +140,7 @@ func (s *OrderStore) Create(order *TraderOrder) error { order.RealizedPnL, order.EntryPrice, now, now, ) if err != nil { - return fmt.Errorf("创建订单记录失败: %w", err) + return fmt.Errorf("failed to create order record: %w", err) } id, _ := result.LastInsertId() @@ -148,7 +148,7 @@ func (s *OrderStore) Create(order *TraderOrder) error { return nil } -// Update 更新订单记录 +// Update updates order record func (s *OrderStore) Update(order *TraderOrder) error { now := time.Now().Format(time.RFC3339) filledAt := "" @@ -167,12 +167,12 @@ func (s *OrderStore) Update(order *TraderOrder) error { order.TraderID, order.OrderID, ) if err != nil { - return fmt.Errorf("更新订单记录失败: %w", err) + return fmt.Errorf("failed to update order record: %w", err) } return nil } -// GetByOrderID 根据订单ID获取订单 +// GetByOrderID gets order by order ID func (s *OrderStore) GetByOrderID(traderID, orderID string) (*TraderOrder, error) { var order TraderOrder var createdAt, updatedAt, filledAt sql.NullString @@ -208,9 +208,9 @@ func (s *OrderStore) GetByOrderID(traderID, orderID string) (*TraderOrder, error return &order, nil } -// GetLatestOpenOrder 获取某币种最近的开仓订单(用于计算平仓盈亏) +// GetLatestOpenOrder gets the latest open order for a symbol (for calculating close PnL) func (s *OrderStore) GetLatestOpenOrder(traderID, symbol, side string) (*TraderOrder, error) { - // side: long -> 找 open_long, short -> 找 open_short + // side: long -> find open_long, short -> find open_short action := "open_long" if side == "short" { action = "open_short" @@ -252,7 +252,7 @@ func (s *OrderStore) GetLatestOpenOrder(traderID, symbol, side string) (*TraderO return &order, nil } -// GetRecentCompletedOrders 获取最近已完成的平仓订单 +// GetRecentCompletedOrders gets recent completed close orders func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]CompletedOrder, error) { rows, err := s.db.Query(` SELECT symbol, action, side, executed_qty, entry_price, avg_price, @@ -264,7 +264,7 @@ func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]Com LIMIT ? `, traderID, limit) if err != nil { - return nil, fmt.Errorf("查询已完成订单失败: %w", err) + return nil, fmt.Errorf("failed to query completed orders: %w", err) } defer rows.Close() @@ -282,7 +282,7 @@ func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]Com continue } - // 根据action推断side + // Infer side from action if o.Action == "close_long" { o.Side = "long" } else if o.Action == "close_short" { @@ -291,7 +291,7 @@ func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]Com o.Side = side.String } - // 计算盈亏百分比 + // Calculate PnL percentage if o.EntryPrice > 0 { if o.Side == "long" { o.PnLPct = (o.ExitPrice - o.EntryPrice) / o.EntryPrice * 100 * float64(o.Leverage) @@ -310,11 +310,11 @@ func (s *OrderStore) GetRecentCompletedOrders(traderID string, limit int) ([]Com return orders, nil } -// GetTraderStats 获取交易统计指标 +// GetTraderStats gets trading statistics metrics func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) { stats := &TraderStats{} - // 查询所有已完成的平仓订单 + // Query all completed close orders rows, err := s.db.Query(` SELECT realized_pnl, fee, filled_at FROM trader_orders @@ -323,7 +323,7 @@ func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) { ORDER BY filled_at ASC `, traderID) if err != nil { - return nil, fmt.Errorf("查询订单统计失败: %w", err) + return nil, fmt.Errorf("failed to query order statistics: %w", err) } defer rows.Close() @@ -351,17 +351,17 @@ func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) { } } - // 计算胜率 + // Calculate win rate if stats.TotalTrades > 0 { stats.WinRate = float64(stats.WinTrades) / float64(stats.TotalTrades) * 100 } - // 计算盈亏比 + // Calculate profit factor if totalLoss > 0 { stats.ProfitFactor = totalWin / totalLoss } - // 计算平均盈亏 + // Calculate average win/loss if stats.WinTrades > 0 { stats.AvgWin = totalWin / float64(stats.WinTrades) } @@ -369,12 +369,12 @@ func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) { stats.AvgLoss = totalLoss / float64(stats.LossTrades) } - // 计算夏普比(使用盈亏序列) + // Calculate Sharpe ratio (using PnL sequence) if len(pnls) > 1 { stats.SharpeRatio = calculateSharpeRatio(pnls) } - // 计算最大回撤 + // Calculate max drawdown if len(pnls) > 0 { stats.MaxDrawdownPct = calculateMaxDrawdown(pnls) } @@ -382,20 +382,20 @@ func (s *OrderStore) GetTraderStats(traderID string) (*TraderStats, error) { return stats, nil } -// calculateSharpeRatio 计算夏普比 +// calculateSharpeRatio calculates Sharpe ratio func calculateSharpeRatio(pnls []float64) float64 { if len(pnls) < 2 { return 0 } - // 计算平均收益 + // Calculate average return var sum float64 for _, pnl := range pnls { sum += pnl } mean := sum / float64(len(pnls)) - // 计算标准差 + // Calculate standard deviation var variance float64 for _, pnl := range pnls { variance += (pnl - mean) * (pnl - mean) @@ -406,17 +406,17 @@ func calculateSharpeRatio(pnls []float64) float64 { return 0 } - // 夏普比 = 平均收益 / 标准差 + // Sharpe ratio = average return / standard deviation return mean / stdDev } -// calculateMaxDrawdown 计算最大回撤 +// calculateMaxDrawdown calculates max drawdown func calculateMaxDrawdown(pnls []float64) float64 { if len(pnls) == 0 { return 0 } - // 计算累计权益曲线 + // Calculate cumulative equity curve var cumulative float64 var peak float64 var maxDD float64 @@ -437,7 +437,7 @@ func calculateMaxDrawdown(pnls []float64) float64 { return maxDD } -// GetPendingOrders 获取未成交的订单(用于轮询) +// GetPendingOrders gets pending orders (for polling) func (s *OrderStore) GetPendingOrders(traderID string) ([]*TraderOrder, error) { rows, err := s.db.Query(` SELECT id, trader_id, order_id, client_order_id, symbol, side, position_side, @@ -449,14 +449,14 @@ func (s *OrderStore) GetPendingOrders(traderID string) ([]*TraderOrder, error) { ORDER BY created_at ASC `, traderID) if err != nil { - return nil, fmt.Errorf("查询未成交订单失败: %w", err) + return nil, fmt.Errorf("failed to query pending orders: %w", err) } defer rows.Close() return s.scanOrders(rows) } -// GetAllPendingOrders 获取所有未成交的订单(用于全局同步) +// GetAllPendingOrders gets all pending orders (for global sync) func (s *OrderStore) GetAllPendingOrders() ([]*TraderOrder, error) { rows, err := s.db.Query(` SELECT id, trader_id, order_id, client_order_id, symbol, side, position_side, @@ -468,14 +468,14 @@ func (s *OrderStore) GetAllPendingOrders() ([]*TraderOrder, error) { ORDER BY trader_id, created_at ASC `) if err != nil { - return nil, fmt.Errorf("查询未成交订单失败: %w", err) + return nil, fmt.Errorf("failed to query pending orders: %w", err) } defer rows.Close() return s.scanOrders(rows) } -// scanOrders 扫描订单行到结构体 +// scanOrders scans order rows to structs func (s *OrderStore) scanOrders(rows *sql.Rows) ([]*TraderOrder, error) { var orders []*TraderOrder for rows.Next() { diff --git a/store/position.go b/store/position.go index 3b26277e..a0735601 100644 --- a/store/position.go +++ b/store/position.go @@ -7,40 +7,40 @@ import ( "time" ) -// TraderPosition 仓位记录(完整的开平仓追踪) +// TraderPosition position record (complete open/close position tracking) type TraderPosition struct { ID int64 `json:"id"` TraderID string `json:"trader_id"` - ExchangeID string `json:"exchange_id"` // 交易所ID: binance/bybit/hyperliquid/aster/lighter + ExchangeID string `json:"exchange_id"` // Exchange ID: binance/bybit/hyperliquid/aster/lighter Symbol string `json:"symbol"` Side string `json:"side"` // LONG/SHORT - Quantity float64 `json:"quantity"` // 开仓数量 - EntryPrice float64 `json:"entry_price"` // 开仓均价 - EntryOrderID string `json:"entry_order_id"` // 开仓订单ID - EntryTime time.Time `json:"entry_time"` // 开仓时间 - ExitPrice float64 `json:"exit_price"` // 平仓均价 - ExitOrderID string `json:"exit_order_id"` // 平仓订单ID - ExitTime *time.Time `json:"exit_time"` // 平仓时间 - RealizedPnL float64 `json:"realized_pnl"` // 已实现盈亏 - Fee float64 `json:"fee"` // 手续费 - Leverage int `json:"leverage"` // 杠杆倍数 + Quantity float64 `json:"quantity"` // Opening quantity + EntryPrice float64 `json:"entry_price"` // Entry price + EntryOrderID string `json:"entry_order_id"` // Entry order ID + EntryTime time.Time `json:"entry_time"` // Entry time + ExitPrice float64 `json:"exit_price"` // Exit price + ExitOrderID string `json:"exit_order_id"` // Exit order ID + ExitTime *time.Time `json:"exit_time"` // Exit time + RealizedPnL float64 `json:"realized_pnl"` // Realized profit and loss + Fee float64 `json:"fee"` // Fee + Leverage int `json:"leverage"` // Leverage multiplier Status string `json:"status"` // OPEN/CLOSED - CloseReason string `json:"close_reason"` // 平仓原因: ai_decision/manual/stop_loss/take_profit + CloseReason string `json:"close_reason"` // Close reason: ai_decision/manual/stop_loss/take_profit CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } -// PositionStore 仓位存储 +// PositionStore position storage type PositionStore struct { db *sql.DB } -// NewPositionStore 创建仓位存储实例 +// NewPositionStore creates position storage instance func NewPositionStore(db *sql.DB) *PositionStore { return &PositionStore{db: db} } -// InitTables 初始化仓位表 +// InitTables initializes position tables func (s *PositionStore) InitTables() error { _, err := s.db.Exec(` CREATE TABLE IF NOT EXISTS trader_positions ( @@ -66,14 +66,14 @@ func (s *PositionStore) InitTables() error { ) `) if err != nil { - return fmt.Errorf("创建trader_positions表失败: %w", err) + return fmt.Errorf("failed to create trader_positions table: %w", err) } - // 迁移:为现有表添加 exchange_id 列(如果不存在) - // 必须在创建索引之前执行! + // Migration: add exchange_id column to existing table (if not exists) + // Must be executed before creating indexes! s.db.Exec(`ALTER TABLE trader_positions ADD COLUMN exchange_id TEXT NOT NULL DEFAULT ''`) - // 创建索引(在迁移之后) + // Create indexes (after migration) indices := []string{ `CREATE INDEX IF NOT EXISTS idx_positions_trader ON trader_positions(trader_id)`, `CREATE INDEX IF NOT EXISTS idx_positions_exchange ON trader_positions(exchange_id)`, @@ -84,14 +84,14 @@ func (s *PositionStore) InitTables() error { } for _, idx := range indices { if _, err := s.db.Exec(idx); err != nil { - return fmt.Errorf("创建索引失败: %w", err) + return fmt.Errorf("failed to create index: %w", err) } } return nil } -// Create 创建仓位记录(开仓时调用) +// Create creates position record (called when opening position) func (s *PositionStore) Create(pos *TraderPosition) error { now := time.Now() pos.CreatedAt = now @@ -109,7 +109,7 @@ func (s *PositionStore) Create(pos *TraderPosition) error { pos.Status, now.Format(time.RFC3339), now.Format(time.RFC3339), ) if err != nil { - return fmt.Errorf("创建仓位记录失败: %w", err) + return fmt.Errorf("failed to create position record: %w", err) } id, _ := result.LastInsertId() @@ -117,7 +117,7 @@ func (s *PositionStore) Create(pos *TraderPosition) error { return nil } -// ClosePosition 平仓(更新仓位记录) +// ClosePosition closes position (updates position record) func (s *PositionStore) ClosePosition(id int64, exitPrice float64, exitOrderID string, realizedPnL float64, fee float64, closeReason string) error { now := time.Now() _, err := s.db.Exec(` @@ -131,12 +131,12 @@ func (s *PositionStore) ClosePosition(id int64, exitPrice float64, exitOrderID s realizedPnL, fee, closeReason, now.Format(time.RFC3339), id, ) if err != nil { - return fmt.Errorf("更新仓位记录失败: %w", err) + return fmt.Errorf("failed to update position record: %w", err) } return nil } -// GetOpenPositions 获取所有未平仓位 +// GetOpenPositions gets all open positions func (s *PositionStore) GetOpenPositions(traderID string) ([]*TraderPosition, error) { rows, err := s.db.Query(` SELECT id, trader_id, exchange_id, symbol, side, quantity, entry_price, entry_order_id, @@ -147,14 +147,14 @@ func (s *PositionStore) GetOpenPositions(traderID string) ([]*TraderPosition, er ORDER BY entry_time DESC `, traderID) if err != nil { - return nil, fmt.Errorf("查询未平仓位失败: %w", err) + return nil, fmt.Errorf("failed to query open positions: %w", err) } defer rows.Close() return s.scanPositions(rows) } -// GetOpenPositionBySymbol 获取指定币种方向的未平仓位 +// GetOpenPositionBySymbol gets open position for specified symbol and direction func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (*TraderPosition, error) { var pos TraderPosition var entryTime, exitTime, createdAt, updatedAt sql.NullString @@ -183,7 +183,7 @@ func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) ( return &pos, nil } -// GetClosedPositions 获取已平仓位(历史记录) +// GetClosedPositions gets closed positions (historical records) func (s *PositionStore) GetClosedPositions(traderID string, limit int) ([]*TraderPosition, error) { rows, err := s.db.Query(` SELECT id, trader_id, exchange_id, symbol, side, quantity, entry_price, entry_order_id, @@ -195,14 +195,14 @@ func (s *PositionStore) GetClosedPositions(traderID string, limit int) ([]*Trade LIMIT ? `, traderID, limit) if err != nil { - return nil, fmt.Errorf("查询已平仓位失败: %w", err) + return nil, fmt.Errorf("failed to query closed positions: %w", err) } defer rows.Close() return s.scanPositions(rows) } -// GetAllOpenPositions 获取所有trader的未平仓位(用于全局同步) +// GetAllOpenPositions gets all traders' open positions (for global sync) func (s *PositionStore) GetAllOpenPositions() ([]*TraderPosition, error) { rows, err := s.db.Query(` SELECT id, trader_id, exchange_id, symbol, side, quantity, entry_price, entry_order_id, @@ -213,18 +213,18 @@ func (s *PositionStore) GetAllOpenPositions() ([]*TraderPosition, error) { ORDER BY trader_id, entry_time DESC `) if err != nil { - return nil, fmt.Errorf("查询所有未平仓位失败: %w", err) + return nil, fmt.Errorf("failed to query all open positions: %w", err) } defer rows.Close() return s.scanPositions(rows) } -// GetPositionStats 获取仓位统计(简单版) +// GetPositionStats gets position statistics (simplified version) func (s *PositionStore) GetPositionStats(traderID string) (map[string]interface{}, error) { stats := make(map[string]interface{}) - // 总交易数 + // Total trades var totalTrades, winTrades int var totalPnL, totalFee float64 @@ -254,11 +254,11 @@ func (s *PositionStore) GetPositionStats(traderID string) (map[string]interface{ return stats, nil } -// GetFullStats 获取完整的交易统计(与 TraderStats 兼容) +// GetFullStats gets complete trading statistics (compatible with TraderStats) func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) { stats := &TraderStats{} - // 查询所有已平仓位 + // Query all closed positions rows, err := s.db.Query(` SELECT realized_pnl, fee, exit_time FROM trader_positions @@ -266,7 +266,7 @@ func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) { ORDER BY exit_time ASC `, traderID) if err != nil { - return nil, fmt.Errorf("查询仓位统计失败: %w", err) + return nil, fmt.Errorf("failed to query position statistics: %w", err) } defer rows.Close() @@ -290,21 +290,21 @@ func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) { totalWin += pnl } else if pnl < 0 { stats.LossTrades++ - totalLoss += -pnl // 转为正数 + totalLoss += -pnl // Convert to positive } } - // 计算胜率 + // Calculate win rate if stats.TotalTrades > 0 { stats.WinRate = float64(stats.WinTrades) / float64(stats.TotalTrades) * 100 } - // 计算盈亏比 + // Calculate profit factor if totalLoss > 0 { stats.ProfitFactor = totalWin / totalLoss } - // 计算平均盈亏 + // Calculate average profit/loss if stats.WinTrades > 0 { stats.AvgWin = totalWin / float64(stats.WinTrades) } @@ -312,12 +312,12 @@ func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) { stats.AvgLoss = totalLoss / float64(stats.LossTrades) } - // 计算夏普比 + // Calculate Sharpe ratio if len(pnls) > 1 { stats.SharpeRatio = calculateSharpeRatioFromPnls(pnls) } - // 计算最大回撤 + // Calculate maximum drawdown if len(pnls) > 0 { stats.MaxDrawdownPct = calculateMaxDrawdownFromPnls(pnls) } @@ -325,7 +325,7 @@ func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) { return stats, nil } -// RecentTrade 最近的交易记录(用于AI输入) +// RecentTrade recent trade record (for AI input) type RecentTrade struct { Symbol string `json:"symbol"` Side string `json:"side"` // long/short @@ -336,7 +336,7 @@ type RecentTrade struct { ExitTime string `json:"exit_time"` } -// GetRecentTrades 获取最近的已平仓交易 +// GetRecentTrades gets recent closed trades func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTrade, error) { rows, err := s.db.Query(` SELECT symbol, side, entry_price, exit_price, realized_pnl, leverage, exit_time @@ -346,7 +346,7 @@ func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTra LIMIT ? `, traderID, limit) if err != nil { - return nil, fmt.Errorf("查询最近交易失败: %w", err) + return nil, fmt.Errorf("failed to query recent trades: %w", err) } defer rows.Close() @@ -361,14 +361,14 @@ func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTra continue } - // 转换 side 格式 + // Convert side format if t.Side == "LONG" { t.Side = "long" } else if t.Side == "SHORT" { t.Side = "short" } - // 计算盈亏百分比 + // Calculate profit/loss percentage if t.EntryPrice > 0 { if t.Side == "long" { t.PnLPct = (t.ExitPrice - t.EntryPrice) / t.EntryPrice * 100 * float64(leverage) @@ -377,7 +377,7 @@ func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTra } } - // 格式化时间 + // Format time if exitTime.Valid { if parsed, err := time.Parse(time.RFC3339, exitTime.String); err == nil { t.ExitTime = parsed.Format("01-02 15:04") @@ -390,7 +390,7 @@ func (s *PositionStore) GetRecentTrades(traderID string, limit int) ([]RecentTra return trades, nil } -// calculateSharpeRatioFromPnls 计算夏普比 +// calculateSharpeRatioFromPnls calculates Sharpe ratio func calculateSharpeRatioFromPnls(pnls []float64) float64 { if len(pnls) < 2 { return 0 @@ -415,7 +415,7 @@ func calculateSharpeRatioFromPnls(pnls []float64) float64 { return mean / stdDev } -// calculateMaxDrawdownFromPnls 计算最大回撤 +// calculateMaxDrawdownFromPnls calculates maximum drawdown func calculateMaxDrawdownFromPnls(pnls []float64) float64 { if len(pnls) == 0 { return 0 @@ -438,7 +438,7 @@ func calculateMaxDrawdownFromPnls(pnls []float64) float64 { return maxDD } -// scanPositions 扫描仓位行到结构体 +// scanPositions scans position rows into structs func (s *PositionStore) scanPositions(rows *sql.Rows) ([]*TraderPosition, error) { var positions []*TraderPosition for rows.Next() { @@ -462,7 +462,7 @@ func (s *PositionStore) scanPositions(rows *sql.Rows) ([]*TraderPosition, error) return positions, nil } -// parsePositionTimes 解析时间字段 +// parsePositionTimes parses time fields func (s *PositionStore) parsePositionTimes(pos *TraderPosition, entryTime, exitTime, createdAt, updatedAt sql.NullString) { if entryTime.Valid { pos.EntryTime, _ = time.Parse(time.RFC3339, entryTime.String) diff --git a/store/store.go b/store/store.go index 19a50359..7397d045 100644 --- a/store/store.go +++ b/store/store.go @@ -1,5 +1,5 @@ -// Package store 提供统一的数据库存储层 -// 所有数据库操作都应该通过这个包进行 +// Package store provides unified database storage layer +// All database operations should go through this package package store import ( @@ -11,11 +11,11 @@ import ( _ "modernc.org/sqlite" ) -// Store 统一的数据存储接口 +// Store unified data storage interface type Store struct { db *sql.DB - // 子存储(延迟初始化) + // Sub-stores (lazy initialization) user *UserStore aiModel *AIModelStore exchange *ExchangeStore @@ -27,80 +27,80 @@ type Store struct { strategy *StrategyStore equity *EquityStore - // 加密函数 + // Encryption functions encryptFunc func(string) string decryptFunc func(string) string mu sync.RWMutex } -// New 创建新的 Store 实例 +// New creates new Store instance func New(dbPath string) (*Store, error) { db, err := sql.Open("sqlite", dbPath) if err != nil { - return nil, fmt.Errorf("打开数据库失败: %w", err) + return nil, fmt.Errorf("failed to open database: %w", err) } - // SQLite 配置 + // SQLite configuration db.SetMaxOpenConns(1) db.SetMaxIdleConns(1) - // 启用外键约束 + // Enable foreign key constraints if _, err := db.Exec(`PRAGMA foreign_keys = ON`); err != nil { db.Close() - return nil, fmt.Errorf("启用外键失败: %w", err) + return nil, fmt.Errorf("failed to enable foreign keys: %w", err) } - // 使用 DELETE 模式(传统模式)以确保 Docker bind mount 兼容性 - // 注意:WAL 模式在 macOS Docker 下会导致数据同步问题 + // Use DELETE mode (traditional mode) to ensure Docker bind mount compatibility + // Note: WAL mode causes data sync issues on macOS Docker if _, err := db.Exec("PRAGMA journal_mode=DELETE"); err != nil { db.Close() - return nil, fmt.Errorf("设置journal_mode失败: %w", err) + return nil, fmt.Errorf("failed to set journal_mode: %w", err) } - // 设置 synchronous=FULL + // Set synchronous=FULL if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil { db.Close() - return nil, fmt.Errorf("设置synchronous失败: %w", err) + return nil, fmt.Errorf("failed to set synchronous: %w", err) } - // 设置 busy_timeout + // Set busy_timeout if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil { db.Close() - return nil, fmt.Errorf("设置busy_timeout失败: %w", err) + return nil, fmt.Errorf("failed to set busy_timeout: %w", err) } s := &Store{db: db} - // 初始化所有表结构 + // Initialize all table structures if err := s.initTables(); err != nil { db.Close() - return nil, fmt.Errorf("初始化表结构失败: %w", err) + return nil, fmt.Errorf("failed to initialize table structure: %w", err) } - // 初始化默认数据 + // Initialize default data if err := s.initDefaultData(); err != nil { db.Close() - return nil, fmt.Errorf("初始化默认数据失败: %w", err) + return nil, fmt.Errorf("failed to initialize default data: %w", err) } - logger.Info("✅ 数据库已启用 DELETE 模式和 FULL 同步") + logger.Info("✅ Database enabled DELETE mode and FULL sync") return s, nil } -// NewFromDB 从现有数据库连接创建 Store +// NewFromDB creates Store from existing database connection func NewFromDB(db *sql.DB) *Store { return &Store{db: db} } -// SetCryptoFuncs 设置加密解密函数 +// SetCryptoFuncs sets encryption/decryption functions func (s *Store) SetCryptoFuncs(encrypt, decrypt func(string) string) { s.mu.Lock() defer s.mu.Unlock() s.encryptFunc = encrypt s.decryptFunc = decrypt - // 更新已初始化的子存储 + // Update already initialized sub-stores if s.aiModel != nil { s.aiModel.encryptFunc = encrypt s.aiModel.decryptFunc = decrypt @@ -114,43 +114,43 @@ func (s *Store) SetCryptoFuncs(encrypt, decrypt func(string) string) { } } -// initTables 初始化所有数据库表 +// initTables initializes all database tables func (s *Store) initTables() error { - // 按依赖顺序初始化 + // Initialize in dependency order if err := s.User().initTables(); err != nil { - return fmt.Errorf("初始化用户表失败: %w", err) + return fmt.Errorf("failed to initialize user tables: %w", err) } if err := s.AIModel().initTables(); err != nil { - return fmt.Errorf("初始化AI模型表失败: %w", err) + return fmt.Errorf("failed to initialize AI model tables: %w", err) } if err := s.Exchange().initTables(); err != nil { - return fmt.Errorf("初始化交易所表失败: %w", err) + return fmt.Errorf("failed to initialize exchange tables: %w", err) } if err := s.Trader().initTables(); err != nil { - return fmt.Errorf("初始化交易员表失败: %w", err) + return fmt.Errorf("failed to initialize trader tables: %w", err) } if err := s.Decision().initTables(); err != nil { - return fmt.Errorf("初始化决策日志表失败: %w", err) + return fmt.Errorf("failed to initialize decision log tables: %w", err) } if err := s.Backtest().initTables(); err != nil { - return fmt.Errorf("初始化回测表失败: %w", err) + return fmt.Errorf("failed to initialize backtest tables: %w", err) } if err := s.Order().InitTables(); err != nil { - return fmt.Errorf("初始化订单表失败: %w", err) + return fmt.Errorf("failed to initialize order tables: %w", err) } if err := s.Position().InitTables(); err != nil { - return fmt.Errorf("初始化仓位表失败: %w", err) + return fmt.Errorf("failed to initialize position tables: %w", err) } if err := s.Strategy().initTables(); err != nil { - return fmt.Errorf("初始化策略表失败: %w", err) + return fmt.Errorf("failed to initialize strategy tables: %w", err) } if err := s.Equity().initTables(); err != nil { - return fmt.Errorf("初始化净值表失败: %w", err) + return fmt.Errorf("failed to initialize equity tables: %w", err) } return nil } -// initDefaultData 初始化默认数据 +// initDefaultData initializes default data func (s *Store) initDefaultData() error { if err := s.AIModel().initDefaultData(); err != nil { return err @@ -161,16 +161,16 @@ func (s *Store) initDefaultData() error { if err := s.Strategy().initDefaultData(); err != nil { return err } - // 迁移旧的 decision_account_snapshots 数据到新的 trader_equity_snapshots 表 + // Migrate old decision_account_snapshots data to new trader_equity_snapshots table if migrated, err := s.Equity().MigrateFromDecision(); err != nil { - logger.Warnf("迁移净值数据失败: %v", err) + logger.Warnf("failed to migrate equity data: %v", err) } else if migrated > 0 { - logger.Infof("✅ 已迁移 %d 条净值数据到新表", migrated) + logger.Infof("✅ Migrated %d equity records to new table", migrated) } return nil } -// User 获取用户存储 +// User gets user storage func (s *Store) User() *UserStore { s.mu.Lock() defer s.mu.Unlock() @@ -180,7 +180,7 @@ func (s *Store) User() *UserStore { return s.user } -// AIModel 获取AI模型存储 +// AIModel gets AI model storage func (s *Store) AIModel() *AIModelStore { s.mu.Lock() defer s.mu.Unlock() @@ -194,7 +194,7 @@ func (s *Store) AIModel() *AIModelStore { return s.aiModel } -// Exchange 获取交易所存储 +// Exchange gets exchange storage func (s *Store) Exchange() *ExchangeStore { s.mu.Lock() defer s.mu.Unlock() @@ -208,7 +208,7 @@ func (s *Store) Exchange() *ExchangeStore { return s.exchange } -// Trader 获取交易员存储 +// Trader gets trader storage func (s *Store) Trader() *TraderStore { s.mu.Lock() defer s.mu.Unlock() @@ -221,7 +221,7 @@ func (s *Store) Trader() *TraderStore { return s.trader } -// Decision 获取决策日志存储 +// Decision gets decision log storage func (s *Store) Decision() *DecisionStore { s.mu.Lock() defer s.mu.Unlock() @@ -231,7 +231,7 @@ func (s *Store) Decision() *DecisionStore { return s.decision } -// Backtest 获取回测数据存储 +// Backtest gets backtest data storage func (s *Store) Backtest() *BacktestStore { s.mu.Lock() defer s.mu.Unlock() @@ -241,7 +241,7 @@ func (s *Store) Backtest() *BacktestStore { return s.backtest } -// Order 获取订单存储 +// Order gets order storage func (s *Store) Order() *OrderStore { s.mu.Lock() defer s.mu.Unlock() @@ -251,7 +251,7 @@ func (s *Store) Order() *OrderStore { return s.order } -// Position 获取仓位存储 +// Position gets position storage func (s *Store) Position() *PositionStore { s.mu.Lock() defer s.mu.Unlock() @@ -261,7 +261,7 @@ func (s *Store) Position() *PositionStore { return s.position } -// Strategy 获取策略存储 +// Strategy gets strategy storage func (s *Store) Strategy() *StrategyStore { s.mu.Lock() defer s.mu.Unlock() @@ -271,7 +271,7 @@ func (s *Store) Strategy() *StrategyStore { return s.strategy } -// Equity 获取净值存储 +// Equity gets equity storage func (s *Store) Equity() *EquityStore { s.mu.Lock() defer s.mu.Unlock() @@ -281,22 +281,22 @@ func (s *Store) Equity() *EquityStore { return s.equity } -// Close 关闭数据库连接 +// Close closes database connection func (s *Store) Close() error { return s.db.Close() } -// DB 获取底层数据库连接(仅用于兼容旧代码,逐步废弃) -// Deprecated: 使用 Store 的方法代替 +// DB gets underlying database connection (for legacy code compatibility, gradually deprecated) +// Deprecated: use Store methods instead func (s *Store) DB() *sql.DB { return s.db } -// Transaction 执行事务 +// Transaction executes transaction func (s *Store) Transaction(fn func(tx *sql.Tx) error) error { tx, err := s.db.Begin() if err != nil { - return fmt.Errorf("开始事务失败: %w", err) + return fmt.Errorf("failed to begin transaction: %w", err) } if err := fn(tx); err != nil { @@ -305,7 +305,7 @@ func (s *Store) Transaction(fn func(tx *sql.Tx) error) error { } if err := tx.Commit(); err != nil { - return fmt.Errorf("提交事务失败: %w", err) + return fmt.Errorf("failed to commit transaction: %w", err) } return nil } diff --git a/store/strategy.go b/store/strategy.go index 8c2d714b..9a66be51 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -7,139 +7,139 @@ import ( "time" ) -// StrategyStore 策略存储 +// StrategyStore strategy storage type StrategyStore struct { db *sql.DB } -// Strategy 策略配置 +// Strategy strategy configuration type Strategy struct { ID string `json:"id"` UserID string `json:"user_id"` Name string `json:"name"` Description string `json:"description"` - IsActive bool `json:"is_active"` // 是否激活(一个用户只能有一个激活的策略) - IsDefault bool `json:"is_default"` // 是否为系统默认策略 - Config string `json:"config"` // JSON 格式的策略配置 + IsActive bool `json:"is_active"` // whether it is active (a user can only have one active strategy) + IsDefault bool `json:"is_default"` // whether it is a system default strategy + Config string `json:"config"` // strategy configuration in JSON format CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } -// StrategyConfig 策略配置详情(JSON 结构) +// StrategyConfig strategy configuration details (JSON structure) type StrategyConfig struct { - // 币种来源配置 + // coin source configuration CoinSource CoinSourceConfig `json:"coin_source"` - // 量化数据配置 + // quantitative data configuration Indicators IndicatorConfig `json:"indicators"` - // 自定义 Prompt(附加在最后) + // custom prompt (appended at the end) CustomPrompt string `json:"custom_prompt,omitempty"` - // 风险控制配置 + // risk control configuration RiskControl RiskControlConfig `json:"risk_control"` - // System Prompt 可编辑部分 + // editable sections of System Prompt PromptSections PromptSectionsConfig `json:"prompt_sections,omitempty"` } -// PromptSectionsConfig System Prompt 可编辑部分 +// PromptSectionsConfig editable sections of System Prompt type PromptSectionsConfig struct { - // 角色定义(标题+描述) + // role definition (title + description) RoleDefinition string `json:"role_definition,omitempty"` - // 交易频率认知 + // trading frequency awareness TradingFrequency string `json:"trading_frequency,omitempty"` - // 开仓标准 + // entry standards EntryStandards string `json:"entry_standards,omitempty"` - // 决策流程 + // decision process DecisionProcess string `json:"decision_process,omitempty"` } -// CoinSourceConfig 币种来源配置 +// CoinSourceConfig coin source configuration type CoinSourceConfig struct { - // 来源类型: "static" | "coinpool" | "oi_top" | "mixed" + // source type: "static" | "coinpool" | "oi_top" | "mixed" SourceType string `json:"source_type"` - // 静态币种列表(当 source_type = "static" 时使用) + // static coin list (used when source_type = "static") StaticCoins []string `json:"static_coins,omitempty"` - // 是否使用 AI500 币种池 + // whether to use AI500 coin pool UseCoinPool bool `json:"use_coin_pool"` - // AI500 币种池最大数量 + // AI500 coin pool maximum count CoinPoolLimit int `json:"coin_pool_limit,omitempty"` - // AI500 币种池 API URL(策略级别配置) + // AI500 coin pool API URL (strategy-level configuration) CoinPoolAPIURL string `json:"coin_pool_api_url,omitempty"` - // 是否使用 OI Top + // whether to use OI Top UseOITop bool `json:"use_oi_top"` - // OI Top 最大数量 + // OI Top maximum count OITopLimit int `json:"oi_top_limit,omitempty"` - // OI Top API URL(策略级别配置) + // OI Top API URL (strategy-level configuration) OITopAPIURL string `json:"oi_top_api_url,omitempty"` } -// IndicatorConfig 指标配置 +// IndicatorConfig indicator configuration type IndicatorConfig struct { - // K线配置 + // K-line configuration Klines KlineConfig `json:"klines"` - // 技术指标开关 + // technical indicator switches EnableEMA bool `json:"enable_ema"` EnableMACD bool `json:"enable_macd"` EnableRSI bool `json:"enable_rsi"` EnableATR bool `json:"enable_atr"` EnableVolume bool `json:"enable_volume"` - EnableOI bool `json:"enable_oi"` // 持仓量 - EnableFundingRate bool `json:"enable_funding_rate"` // 资金费率 - // EMA 周期配置 - EMAPeriods []int `json:"ema_periods,omitempty"` // 默认 [20, 50] - // RSI 周期配置 - RSIPeriods []int `json:"rsi_periods,omitempty"` // 默认 [7, 14] - // ATR 周期配置 - ATRPeriods []int `json:"atr_periods,omitempty"` // 默认 [14] - // 外部数据源 + EnableOI bool `json:"enable_oi"` // open interest + EnableFundingRate bool `json:"enable_funding_rate"` // funding rate + // EMA period configuration + EMAPeriods []int `json:"ema_periods,omitempty"` // default [20, 50] + // RSI period configuration + RSIPeriods []int `json:"rsi_periods,omitempty"` // default [7, 14] + // ATR period configuration + ATRPeriods []int `json:"atr_periods,omitempty"` // default [14] + // external data sources ExternalDataSources []ExternalDataSource `json:"external_data_sources,omitempty"` - // 量化数据源(资金流向、持仓变化、价格变化) - EnableQuantData bool `json:"enable_quant_data"` // 是否启用量化数据 - QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // 量化数据 API 地址 + // quantitative data sources (capital flow, position changes, price changes) + EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data + QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // quantitative data API address } -// KlineConfig K线配置 +// KlineConfig K-line configuration type KlineConfig struct { - // 主时间周期: "1m", "3m", "5m", "15m", "1h", "4h" + // primary timeframe: "1m", "3m", "5m", "15m", "1h", "4h" PrimaryTimeframe string `json:"primary_timeframe"` - // 主时间周期 K 线数量 + // primary timeframe K-line count PrimaryCount int `json:"primary_count"` - // 长周期时间框架 + // longer timeframe LongerTimeframe string `json:"longer_timeframe,omitempty"` - // 长周期 K 线数量 + // longer timeframe K-line count LongerCount int `json:"longer_count,omitempty"` - // 是否启用多时间框架分析 + // whether to enable multi-timeframe analysis EnableMultiTimeframe bool `json:"enable_multi_timeframe"` - // 选中的时间周期列表(新增:支持多周期选择) + // selected timeframe list (new: supports multi-timeframe selection) SelectedTimeframes []string `json:"selected_timeframes,omitempty"` } -// ExternalDataSource 外部数据源配置 +// ExternalDataSource external data source configuration type ExternalDataSource struct { - Name string `json:"name"` // 数据源名称 - Type string `json:"type"` // 类型: "api" | "webhook" + Name string `json:"name"` // data source name + Type string `json:"type"` // type: "api" | "webhook" URL string `json:"url"` // API URL - Method string `json:"method"` // HTTP 方法 + Method string `json:"method"` // HTTP method Headers map[string]string `json:"headers,omitempty"` - DataPath string `json:"data_path,omitempty"` // JSON 数据路径 - RefreshSecs int `json:"refresh_secs,omitempty"` // 刷新间隔(秒) + DataPath string `json:"data_path,omitempty"` // JSON data path + RefreshSecs int `json:"refresh_secs,omitempty"` // refresh interval (seconds) } -// RiskControlConfig 风险控制配置 +// RiskControlConfig risk control configuration type RiskControlConfig struct { - // 最大持仓数量 + // maximum number of positions MaxPositions int `json:"max_positions"` - // BTC/ETH 最大杠杆 + // BTC/ETH maximum leverage BTCETHMaxLeverage int `json:"btc_eth_max_leverage"` - // 山寨币最大杠杆 + // altcoin maximum leverage AltcoinMaxLeverage int `json:"altcoin_max_leverage"` - // 最小风险回报比 + // minimum risk-reward ratio MinRiskRewardRatio float64 `json:"min_risk_reward_ratio"` - // 最大保证金使用率 + // maximum margin usage MaxMarginUsage float64 `json:"max_margin_usage"` - // 单币种最大仓位比例(相对账户净值) + // maximum position ratio per coin (relative to account equity) MaxPositionRatio float64 `json:"max_position_ratio"` - // 最小开仓金额(USDT) + // minimum position size (USDT) MinPositionSize float64 `json:"min_position_size"` - // 最小信心度 + // minimum confidence level MinConfidence int `json:"min_confidence"` } @@ -161,11 +161,11 @@ func (s *StrategyStore) initTables() error { return err } - // 创建索引 + // create indexes _, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_strategies_user_id ON strategies(user_id)`) _, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_strategies_is_active ON strategies(is_active)`) - // 触发器:更新时自动更新 updated_at + // trigger: automatically update updated_at on update _, err = s.db.Exec(` CREATE TRIGGER IF NOT EXISTS update_strategies_updated_at AFTER UPDATE ON strategies @@ -178,14 +178,14 @@ func (s *StrategyStore) initTables() error { } func (s *StrategyStore) initDefaultData() error { - // 检查是否已有默认策略 + // check if default strategy already exists var count int s.db.QueryRow(`SELECT COUNT(*) FROM strategies WHERE is_default = 1`).Scan(&count) if count > 0 { return nil } - // 创建系统默认策略 + // create system default strategy defaultConfig := StrategyConfig{ CoinSource: CoinSourceConfig{ SourceType: "coinpool", @@ -228,23 +228,23 @@ func (s *StrategyStore) initDefaultData() error { MinConfidence: 75, }, PromptSections: PromptSectionsConfig{ - RoleDefinition: `# 你是专业的加密货币交易AI + RoleDefinition: `# You are a professional cryptocurrency trading AI -你的任务是根据提供的市场数据做出交易决策。你是一位经验丰富的量化交易员,擅长技术分析和风险管理。`, - TradingFrequency: `# ⏱️ 交易频率认知 +Your task is to make trading decisions based on the provided market data. You are an experienced quantitative trader skilled in technical analysis and risk management.`, + TradingFrequency: `# ⏱️ Trading Frequency Awareness -- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔 -- 每小时>2笔 = 过度交易 -- 单笔持仓时间≥30-60分钟 -如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。`, - EntryStandards: `# 🎯 开仓标准(严格) +- Excellent trader: 2-4 trades per day ≈ 0.1-0.2 trades per hour +- >2 trades per hour = overtrading +- Single position holding time ≥ 30-60 minutes +If you find yourself trading every cycle → standards are too low; if closing positions in <30 minutes → too impulsive.`, + EntryStandards: `# 🎯 Entry Standards (Strict) -只在多重信号共振时开仓。自由运用任何有效的分析方法,避免单一指标、信号矛盾、横盘震荡、刚平仓即重启等低质量行为。`, - DecisionProcess: `# 📋 决策流程 +Only enter positions when multiple signals resonate. Freely use any effective analysis methods, avoid low-quality behaviors such as single indicators, contradictory signals, sideways oscillation, or immediately restarting after closing positions.`, + DecisionProcess: `# 📋 Decision Process -1. 检查持仓 → 是否该止盈/止损 -2. 扫描候选币 + 多时间框 → 是否存在强信号 -3. 先写思维链,再输出结构化JSON`, +1. Check positions → whether to take profit/stop loss +2. Scan candidate coins + multi-timeframe → whether strong signals exist +3. Write chain of thought first, then output structured JSON`, }, } @@ -252,13 +252,13 @@ func (s *StrategyStore) initDefaultData() error { _, err := s.db.Exec(` INSERT INTO strategies (id, user_id, name, description, is_active, is_default, config) - VALUES ('default', 'system', '默认山寨策略', '系统默认的山寨币交易策略,使用 AI500 币种池,包含完整的技术指标', 0, 1, ?) + VALUES ('default', 'system', 'Default Altcoin Strategy', 'System default altcoin trading strategy, uses AI500 coin pool, includes complete technical indicators', 0, 1, ?) `, string(configJSON)) return err } -// Create 创建策略 +// Create create a strategy func (s *StrategyStore) Create(strategy *Strategy) error { _, err := s.db.Exec(` INSERT INTO strategies (id, user_id, name, description, is_active, is_default, config) @@ -267,7 +267,7 @@ func (s *StrategyStore) Create(strategy *Strategy) error { return err } -// Update 更新策略 +// Update update a strategy func (s *StrategyStore) Update(strategy *Strategy) error { _, err := s.db.Exec(` UPDATE strategies SET @@ -277,22 +277,22 @@ func (s *StrategyStore) Update(strategy *Strategy) error { return err } -// Delete 删除策略 +// Delete delete a strategy func (s *StrategyStore) Delete(userID, id string) error { - // 不允许删除系统默认策略 + // do not allow deleting system default strategy var isDefault bool s.db.QueryRow(`SELECT is_default FROM strategies WHERE id = ?`, id).Scan(&isDefault) if isDefault { - return fmt.Errorf("不能删除系统默认策略") + return fmt.Errorf("cannot delete system default strategy") } _, err := s.db.Exec(`DELETE FROM strategies WHERE id = ? AND user_id = ?`, id, userID) return err } -// List 获取用户的策略列表 +// List get user's strategy list func (s *StrategyStore) List(userID string) ([]*Strategy, error) { - // 获取用户自己的策略 + 系统默认策略 + // get user's own strategies + system default strategy rows, err := s.db.Query(` SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at FROM strategies @@ -323,7 +323,7 @@ func (s *StrategyStore) List(userID string) ([]*Strategy, error) { return strategies, nil } -// Get 获取单个策略 +// Get get a single strategy func (s *StrategyStore) Get(userID, id string) (*Strategy, error) { var st Strategy var createdAt, updatedAt string @@ -344,7 +344,7 @@ func (s *StrategyStore) Get(userID, id string) (*Strategy, error) { return &st, nil } -// GetActive 获取用户当前激活的策略 +// GetActive get user's currently active strategy func (s *StrategyStore) GetActive(userID string) (*Strategy, error) { var st Strategy var createdAt, updatedAt string @@ -358,7 +358,7 @@ func (s *StrategyStore) GetActive(userID string) (*Strategy, error) { &createdAt, &updatedAt, ) if err == sql.ErrNoRows { - // 没有激活的策略,返回系统默认策略 + // no active strategy, return system default strategy return s.GetDefault() } if err != nil { @@ -369,7 +369,7 @@ func (s *StrategyStore) GetActive(userID string) (*Strategy, error) { return &st, nil } -// GetDefault 获取系统默认策略 +// GetDefault get system default strategy func (s *StrategyStore) GetDefault() (*Strategy, error) { var st Strategy var createdAt, updatedAt string @@ -391,22 +391,22 @@ func (s *StrategyStore) GetDefault() (*Strategy, error) { return &st, nil } -// SetActive 设置激活策略(会先取消其他策略的激活状态) +// SetActive set active strategy (will first deactivate other strategies) func (s *StrategyStore) SetActive(userID, strategyID string) error { - // 开启事务 + // begin transaction tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() - // 先取消该用户所有策略的激活状态 + // first deactivate all strategies for the user _, err = tx.Exec(`UPDATE strategies SET is_active = 0 WHERE user_id = ?`, userID) if err != nil { return err } - // 激活指定策略 + // activate specified strategy _, err = tx.Exec(`UPDATE strategies SET is_active = 1 WHERE id = ? AND (user_id = ? OR is_default = 1)`, strategyID, userID) if err != nil { return err @@ -415,20 +415,20 @@ func (s *StrategyStore) SetActive(userID, strategyID string) error { return tx.Commit() } -// Duplicate 复制策略(用于基于默认策略创建自定义策略) +// Duplicate duplicate a strategy (used to create custom strategy based on default strategy) func (s *StrategyStore) Duplicate(userID, sourceID, newID, newName string) error { - // 获取源策略 + // get source strategy source, err := s.Get(userID, sourceID) if err != nil { - return fmt.Errorf("获取源策略失败: %w", err) + return fmt.Errorf("failed to get source strategy: %w", err) } - // 创建新策略 + // create new strategy newStrategy := &Strategy{ ID: newID, UserID: userID, Name: newName, - Description: "基于 [" + source.Name + "] 创建", + Description: "Created based on [" + source.Name + "]", IsActive: false, IsDefault: false, Config: source.Config, @@ -437,20 +437,20 @@ func (s *StrategyStore) Duplicate(userID, sourceID, newID, newName string) error return s.Create(newStrategy) } -// ParseConfig 解析策略配置 JSON +// ParseConfig parse strategy configuration JSON func (s *Strategy) ParseConfig() (*StrategyConfig, error) { var config StrategyConfig if err := json.Unmarshal([]byte(s.Config), &config); err != nil { - return nil, fmt.Errorf("解析策略配置失败: %w", err) + return nil, fmt.Errorf("failed to parse strategy configuration: %w", err) } return &config, nil } -// SetConfig 设置策略配置 +// SetConfig set strategy configuration func (s *Strategy) SetConfig(config *StrategyConfig) error { data, err := json.Marshal(config) if err != nil { - return fmt.Errorf("序列化策略配置失败: %w", err) + return fmt.Errorf("failed to serialize strategy configuration: %w", err) } s.Config = string(data) return nil diff --git a/store/trader.go b/store/trader.go index f300fe3b..d05bec9a 100644 --- a/store/trader.go +++ b/store/trader.go @@ -5,20 +5,20 @@ import ( "time" ) -// TraderStore 交易员存储 +// TraderStore trader storage type TraderStore struct { db *sql.DB decryptFunc func(string) string } -// Trader 交易员配置 +// Trader trader configuration type Trader struct { ID string `json:"id"` UserID string `json:"user_id"` Name string `json:"name"` AIModelID string `json:"ai_model_id"` ExchangeID string `json:"exchange_id"` - StrategyID string `json:"strategy_id"` // 关联策略ID + StrategyID string `json:"strategy_id"` // Associated strategy ID InitialBalance float64 `json:"initial_balance"` ScanIntervalMinutes int `json:"scan_interval_minutes"` IsRunning bool `json:"is_running"` @@ -26,7 +26,7 @@ type Trader struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - // 以下字段已废弃,保留用于向后兼容,新交易员应使用 StrategyID + // Following fields are deprecated, kept for backward compatibility, new traders should use StrategyID BTCETHLeverage int `json:"btc_eth_leverage,omitempty"` AltcoinLeverage int `json:"altcoin_leverage,omitempty"` TradingSymbols string `json:"trading_symbols,omitempty"` @@ -37,12 +37,12 @@ type Trader struct { SystemPromptTemplate string `json:"system_prompt_template,omitempty"` } -// TraderFullConfig 交易员完整配置(包含AI模型、交易所和策略) +// TraderFullConfig trader full configuration (includes AI model, exchange and strategy) type TraderFullConfig struct { Trader *Trader AIModel *AIModel Exchange *Exchange - Strategy *Strategy // 关联的策略配置 + Strategy *Strategy // Associated strategy configuration } func (s *TraderStore) initTables() error { @@ -74,7 +74,7 @@ func (s *TraderStore) initTables() error { return err } - // 触发器 + // Trigger _, err = s.db.Exec(` CREATE TRIGGER IF NOT EXISTS update_traders_updated_at AFTER UPDATE ON traders @@ -86,7 +86,7 @@ func (s *TraderStore) initTables() error { return err } - // 向后兼容 + // Backward compatibility alterQueries := []string{ `ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`, `ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`, @@ -113,7 +113,7 @@ func (s *TraderStore) decrypt(encrypted string) string { return encrypted } -// Create 创建交易员 +// Create creates trader func (s *TraderStore) Create(trader *Trader) error { _, err := s.db.Exec(` INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, strategy_id, initial_balance, @@ -128,7 +128,7 @@ func (s *TraderStore) Create(trader *Trader) error { return err } -// List 获取用户的交易员列表 +// List gets user's trader list func (s *TraderStore) List(userID string) ([]*Trader, error) { rows, err := s.db.Query(` SELECT id, user_id, name, ai_model_id, exchange_id, COALESCE(strategy_id, ''), @@ -165,13 +165,13 @@ func (s *TraderStore) List(userID string) ([]*Trader, error) { return traders, nil } -// UpdateStatus 更新交易员运行状态 +// UpdateStatus updates trader running status func (s *TraderStore) UpdateStatus(userID, id string, isRunning bool) error { _, err := s.db.Exec(`UPDATE traders SET is_running = ? WHERE id = ? AND user_id = ?`, isRunning, id, userID) return err } -// Update 更新交易员配置 +// Update updates trader configuration func (s *TraderStore) Update(trader *Trader) error { _, err := s.db.Exec(` UPDATE traders SET @@ -184,26 +184,26 @@ func (s *TraderStore) Update(trader *Trader) error { return err } -// UpdateInitialBalance 更新初始余额 +// UpdateInitialBalance updates initial balance func (s *TraderStore) UpdateInitialBalance(userID, id string, newBalance float64) error { _, err := s.db.Exec(`UPDATE traders SET initial_balance = ? WHERE id = ? AND user_id = ?`, newBalance, id, userID) return err } -// UpdateCustomPrompt 更新自定义提示词 +// UpdateCustomPrompt updates custom prompt func (s *TraderStore) UpdateCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error { _, err := s.db.Exec(`UPDATE traders SET custom_prompt = ?, override_base_prompt = ? WHERE id = ? AND user_id = ?`, customPrompt, overrideBase, id, userID) return err } -// Delete 删除交易员 +// Delete deletes trader func (s *TraderStore) Delete(userID, id string) error { _, err := s.db.Exec(`DELETE FROM traders WHERE id = ? AND user_id = ?`, id, userID) return err } -// GetFullConfig 获取交易员完整配置 +// GetFullConfig gets trader full configuration func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig, error) { var trader Trader var aiModel AIModel @@ -255,7 +255,7 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig, exchange.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeCreatedAt) exchange.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeUpdatedAt) - // 解密 + // Decrypt aiModel.APIKey = s.decrypt(aiModel.APIKey) exchange.APIKey = s.decrypt(exchange.APIKey) exchange.SecretKey = s.decrypt(exchange.SecretKey) @@ -264,12 +264,12 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig, exchange.LighterPrivateKey = s.decrypt(exchange.LighterPrivateKey) exchange.LighterAPIKeyPrivateKey = s.decrypt(exchange.LighterAPIKeyPrivateKey) - // 加载关联的策略 + // Load associated strategy var strategy *Strategy if trader.StrategyID != "" { strategy, _ = s.getStrategyByID(userID, trader.StrategyID) } - // 如果没有关联策略,获取用户的激活策略或默认策略 + // If no associated strategy, get user's active strategy or default strategy if strategy == nil { strategy, _ = s.getActiveOrDefaultStrategy(userID) } @@ -282,7 +282,7 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig, }, nil } -// getStrategyByID 内部方法:根据ID获取策略 +// getStrategyByID internal method: gets strategy by ID func (s *TraderStore) getStrategyByID(userID, strategyID string) (*Strategy, error) { var strategy Strategy var createdAt, updatedAt string @@ -301,12 +301,12 @@ func (s *TraderStore) getStrategyByID(userID, strategyID string) (*Strategy, err return &strategy, nil } -// getActiveOrDefaultStrategy 内部方法:获取用户激活的策略或系统默认策略 +// getActiveOrDefaultStrategy internal method: gets user's active strategy or system default strategy func (s *TraderStore) getActiveOrDefaultStrategy(userID string) (*Strategy, error) { var strategy Strategy var createdAt, updatedAt string - // 先尝试获取用户激活的策略 + // First try to get user's active strategy err := s.db.QueryRow(` SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at FROM strategies WHERE user_id = ? AND is_active = 1 @@ -320,7 +320,7 @@ func (s *TraderStore) getActiveOrDefaultStrategy(userID string) (*Strategy, erro return &strategy, nil } - // 回退到系统默认策略 + // Fallback to system default strategy err = s.db.QueryRow(` SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at FROM strategies WHERE is_default = 1 LIMIT 1 @@ -336,7 +336,7 @@ func (s *TraderStore) getActiveOrDefaultStrategy(userID string) (*Strategy, erro return &strategy, nil } -// ListAll 获取所有用户的交易员列表 +// ListAll gets all users' trader list func (s *TraderStore) ListAll() ([]*Trader, error) { rows, err := s.db.Query(` SELECT id, user_id, name, ai_model_id, exchange_id, COALESCE(strategy_id, ''), diff --git a/store/user.go b/store/user.go index 6e9c993f..35f44537 100644 --- a/store/user.go +++ b/store/user.go @@ -7,12 +7,12 @@ import ( "time" ) -// UserStore 用户存储 +// UserStore user storage type UserStore struct { db *sql.DB } -// User 用户 +// User user type User struct { ID string `json:"id"` Email string `json:"email"` @@ -23,7 +23,7 @@ type User struct { UpdatedAt time.Time `json:"updated_at"` } -// GenerateOTPSecret 生成OTP密钥 +// GenerateOTPSecret generates OTP secret func GenerateOTPSecret() (string, error) { secret := make([]byte, 20) _, err := rand.Read(secret) @@ -49,7 +49,7 @@ func (s *UserStore) initTables() error { return err } - // 触发器 + // Trigger _, err = s.db.Exec(` CREATE TRIGGER IF NOT EXISTS update_users_updated_at AFTER UPDATE ON users @@ -64,7 +64,7 @@ func (s *UserStore) initTables() error { return nil } -// Create 创建用户 +// Create creates user func (s *UserStore) Create(user *User) error { _, err := s.db.Exec(` INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) @@ -73,7 +73,7 @@ func (s *UserStore) Create(user *User) error { return err } -// GetByEmail 通过邮箱获取用户 +// GetByEmail gets user by email func (s *UserStore) GetByEmail(email string) (*User, error) { var user User var createdAt, updatedAt string @@ -92,7 +92,7 @@ func (s *UserStore) GetByEmail(email string) (*User, error) { return &user, nil } -// GetByID 通过ID获取用户 +// GetByID gets user by ID func (s *UserStore) GetByID(userID string) (*User, error) { var user User var createdAt, updatedAt string @@ -111,7 +111,7 @@ func (s *UserStore) GetByID(userID string) (*User, error) { return &user, nil } -// GetAllIDs 获取所有用户ID +// GetAllIDs gets all user IDs func (s *UserStore) GetAllIDs() ([]string, error) { rows, err := s.db.Query(`SELECT id FROM users ORDER BY id`) if err != nil { @@ -130,13 +130,13 @@ func (s *UserStore) GetAllIDs() ([]string, error) { return userIDs, nil } -// UpdateOTPVerified 更新OTP验证状态 +// UpdateOTPVerified updates OTP verification status func (s *UserStore) UpdateOTPVerified(userID string, verified bool) error { _, err := s.db.Exec(`UPDATE users SET otp_verified = ? WHERE id = ?`, verified, userID) return err } -// UpdatePassword 更新密码 +// UpdatePassword updates password func (s *UserStore) UpdatePassword(userID, passwordHash string) error { _, err := s.db.Exec(` UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? @@ -144,7 +144,7 @@ func (s *UserStore) UpdatePassword(userID, passwordHash string) error { return err } -// EnsureAdmin 确保admin用户存在 +// EnsureAdmin ensures admin user exists func (s *UserStore) EnsureAdmin() error { var count int err := s.db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = 'admin'`).Scan(&count) diff --git a/trader/aster_trader.go b/trader/aster_trader.go index 7a172739..7cb35b10 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -25,40 +25,40 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) -// AsterTrader Aster交易平台实现 +// AsterTrader Aster trading platform implementation type AsterTrader struct { ctx context.Context - user string // 主钱包地址 (ERC20) - signer string // API钱包地址 - privateKey *ecdsa.PrivateKey // API钱包私钥 + user string // Main wallet address (ERC20) + signer string // API wallet address + privateKey *ecdsa.PrivateKey // API wallet private key client *http.Client baseURL string - // 缓存交易对精度信息 + // Cache symbol precision information symbolPrecision map[string]SymbolPrecision mu sync.RWMutex } -// SymbolPrecision 交易对精度信息 +// SymbolPrecision Symbol precision information type SymbolPrecision struct { PricePrecision int QuantityPrecision int - TickSize float64 // 价格步进值 - StepSize float64 // 数量步进值 + TickSize float64 // Price tick size + StepSize float64 // Quantity step size } -// NewAsterTrader 创建Aster交易器 -// user: 主钱包地址 (登录地址) -// signer: API钱包地址 (从 https://www.asterdex.com/en/api-wallet 获取) -// privateKey: API钱包私钥 (从 https://www.asterdex.com/en/api-wallet 获取) +// NewAsterTrader Create Aster trader +// user: Main wallet address (login address) +// signer: API wallet address (obtained from https://www.asterdex.com/en/api-wallet) +// privateKey: API wallet private key (obtained from https://www.asterdex.com/en/api-wallet) func NewAsterTrader(user, signer, privateKeyHex string) (*AsterTrader, error) { - // 解析私钥 + // Parse private key privKey, err := crypto.HexToECDSA(strings.TrimPrefix(privateKeyHex, "0x")) if err != nil { - return nil, fmt.Errorf("解析私钥失败: %w", err) + return nil, fmt.Errorf("failed to parse private key: %w", err) } client := &http.Client{ - Timeout: 30 * time.Second, // 增加到30秒 + Timeout: 30 * time.Second, // Increased to 30 seconds Transport: &http.Transport{ TLSHandshakeTimeout: 10 * time.Second, ResponseHeaderTimeout: 10 * time.Second, @@ -81,12 +81,12 @@ func NewAsterTrader(user, signer, privateKeyHex string) (*AsterTrader, error) { }, nil } -// genNonce 生成微秒时间戳 +// genNonce Generate microsecond timestamp func (t *AsterTrader) genNonce() uint64 { return uint64(time.Now().UnixMicro()) } -// getPrecision 获取交易对精度信息 +// getPrecision Get symbol precision information func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) { t.mu.RLock() if prec, ok := t.symbolPrecision[symbol]; ok { @@ -95,7 +95,7 @@ func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) { } t.mu.RUnlock() - // 获取交易所信息 + // Get exchange information resp, err := t.client.Get(t.baseURL + "/fapi/v3/exchangeInfo") if err != nil { return SymbolPrecision{}, err @@ -116,7 +116,7 @@ func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) { return SymbolPrecision{}, err } - // 缓存所有交易对的精度 + // Cache precision for all symbols t.mu.Lock() for _, s := range info.Symbols { prec := SymbolPrecision{ @@ -124,7 +124,7 @@ func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) { QuantityPrecision: s.QuantityPrecision, } - // 解析filters获取tickSize和stepSize + // Parse filters to get tickSize and stepSize for _, filter := range s.Filters { filterType, _ := filter["filterType"].(string) switch filterType { @@ -147,69 +147,69 @@ func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) { return prec, nil } - return SymbolPrecision{}, fmt.Errorf("未找到交易对 %s 的精度信息", symbol) + return SymbolPrecision{}, fmt.Errorf("precision information not found for symbol %s", symbol) } -// roundToTickSize 将价格/数量四舍五入到tick size/step size的整数倍 +// roundToTickSize Round price/quantity to the nearest multiple of tick size/step size func roundToTickSize(value float64, tickSize float64) float64 { if tickSize <= 0 { return value } - // 计算有多少个tick size + // Calculate how many tick sizes steps := value / tickSize - // 四舍五入到最近的整数 + // Round to the nearest integer roundedSteps := math.Round(steps) - // 乘回tick size + // Multiply back by tick size return roundedSteps * tickSize } -// formatPrice 格式化价格到正确精度和tick size +// formatPrice Format price to correct precision and tick size func (t *AsterTrader) formatPrice(symbol string, price float64) (float64, error) { prec, err := t.getPrecision(symbol) if err != nil { return 0, err } - // 优先使用tick size,确保价格是tick size的整数倍 + // Prioritize tick size to ensure price is a multiple of tick size if prec.TickSize > 0 { return roundToTickSize(price, prec.TickSize), nil } - // 如果没有tick size,则按精度四舍五入 + // If no tick size, round by precision multiplier := math.Pow10(prec.PricePrecision) return math.Round(price*multiplier) / multiplier, nil } -// formatQuantity 格式化数量到正确精度和step size +// formatQuantity Format quantity to correct precision and step size func (t *AsterTrader) formatQuantity(symbol string, quantity float64) (float64, error) { prec, err := t.getPrecision(symbol) if err != nil { return 0, err } - // 优先使用step size,确保数量是step size的整数倍 + // Prioritize step size to ensure quantity is a multiple of step size if prec.StepSize > 0 { return roundToTickSize(quantity, prec.StepSize), nil } - // 如果没有step size,则按精度四舍五入 + // If no step size, round by precision multiplier := math.Pow10(prec.QuantityPrecision) return math.Round(quantity*multiplier) / multiplier, nil } -// formatFloatWithPrecision 将浮点数格式化为指定精度的字符串(去除末尾的0) +// formatFloatWithPrecision Format float to string with specified precision (remove trailing zeros) func (t *AsterTrader) formatFloatWithPrecision(value float64, precision int) string { - // 使用指定精度格式化 + // Format with specified precision formatted := strconv.FormatFloat(value, 'f', precision, 64) - // 去除末尾的0和小数点(如果有) + // Remove trailing zeros and decimal point (if any) formatted = strings.TrimRight(formatted, "0") formatted = strings.TrimRight(formatted, ".") return formatted } -// normalizeAndStringify 对参数进行规范化并序列化为JSON字符串(按key排序) +// normalizeAndStringify Normalize parameters and serialize to JSON string (sorted by key) func (t *AsterTrader) normalizeAndStringify(params map[string]interface{}) (string, error) { normalized, err := t.normalize(params) if err != nil { @@ -222,7 +222,7 @@ func (t *AsterTrader) normalizeAndStringify(params map[string]interface{}) (stri return string(bs), nil } -// normalize 递归规范化参数(按key排序,所有值转为字符串) +// normalize Recursively normalize parameters (sorted by key, all values converted to strings) func (t *AsterTrader) normalize(v interface{}) (interface{}, error) { switch val := v.(type) { case map[string]interface{}: @@ -261,24 +261,24 @@ func (t *AsterTrader) normalize(v interface{}) (interface{}, error) { case bool: return fmt.Sprintf("%v", val), nil default: - // 其他类型转为字符串 + // Convert other types to string return fmt.Sprintf("%v", val), nil } } -// sign 对请求参数进行签名 +// sign Sign request parameters func (t *AsterTrader) sign(params map[string]interface{}, nonce uint64) error { - // 添加时间戳和接收窗口 + // Add timestamp and receive window params["recvWindow"] = "50000" params["timestamp"] = strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) - // 规范化参数为JSON字符串 + // Normalize parameters to JSON string jsonStr, err := t.normalizeAndStringify(params) if err != nil { return err } - // ABI编码: (string, address, address, uint256) + // ABI encoding: (string, address, address, uint256) addrUser := common.HexToAddress(t.user) addrSigner := common.HexToAddress(t.signer) nonceBig := new(big.Int).SetUint64(nonce) @@ -296,29 +296,29 @@ func (t *AsterTrader) sign(params map[string]interface{}, nonce uint64) error { packed, err := arguments.Pack(jsonStr, addrUser, addrSigner, nonceBig) if err != nil { - return fmt.Errorf("ABI编码失败: %w", err) + return fmt.Errorf("ABI encoding failed: %w", err) } - // Keccak256哈希 + // Keccak256 hash hash := crypto.Keccak256(packed) - // 以太坊签名消息前缀 + // Ethereum signed message prefix prefixedMsg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(hash), hash) msgHash := crypto.Keccak256Hash([]byte(prefixedMsg)) - // ECDSA签名 + // ECDSA signature sig, err := crypto.Sign(msgHash.Bytes(), t.privateKey) if err != nil { - return fmt.Errorf("签名失败: %w", err) + return fmt.Errorf("signature failed: %w", err) } - // 将v从0/1转换为27/28 + // Convert v from 0/1 to 27/28 if len(sig) != 65 { - return fmt.Errorf("签名长度异常: %d", len(sig)) + return fmt.Errorf("signature length abnormal: %d", len(sig)) } sig[64] += 27 - // 添加签名参数 + // Add signature parameters params["user"] = t.user params["signer"] = t.signer params["signature"] = "0x" + hex.EncodeToString(sig) @@ -327,20 +327,20 @@ func (t *AsterTrader) sign(params map[string]interface{}, nonce uint64) error { return nil } -// request 发送HTTP请求(带重试机制) +// request Send HTTP request (with retry mechanism) func (t *AsterTrader) request(method, endpoint string, params map[string]interface{}) ([]byte, error) { const maxRetries = 3 var lastErr error for attempt := 1; attempt <= maxRetries; attempt++ { - // 每次重试都生成新的nonce和签名 + // Generate new nonce and signature for each retry nonce := t.genNonce() paramsCopy := make(map[string]interface{}) for k, v := range params { paramsCopy[k] = v } - // 签名 + // Sign if err := t.sign(paramsCopy, nonce); err != nil { return nil, err } @@ -352,7 +352,7 @@ func (t *AsterTrader) request(method, endpoint string, params map[string]interfa lastErr = err - // 如果是网络超时或临时错误,重试 + // Retry if network timeout or temporary error if strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "connection reset") || strings.Contains(err.Error(), "EOF") { @@ -363,21 +363,21 @@ func (t *AsterTrader) request(method, endpoint string, params map[string]interfa } } - // 其他错误(如400/401等)不重试 + // Don't retry other errors (like 400/401) return nil, err } - return nil, fmt.Errorf("请求失败(已重试%d次): %w", maxRetries, lastErr) + return nil, fmt.Errorf("request failed (retried %d times): %w", maxRetries, lastErr) } -// doRequest 执行实际的HTTP请求 +// doRequest Execute actual HTTP request func (t *AsterTrader) doRequest(method, endpoint string, params map[string]interface{}) ([]byte, error) { fullURL := t.baseURL + endpoint method = strings.ToUpper(method) switch method { case "POST": - // POST请求:参数放在表单body中 + // POST request: parameters in form body form := url.Values{} for k, v := range params { form.Set(k, fmt.Sprintf("%v", v)) @@ -401,7 +401,7 @@ func (t *AsterTrader) doRequest(method, endpoint string, params map[string]inter return body, nil case "GET", "DELETE": - // GET/DELETE请求:参数放在querystring中 + // GET/DELETE request: parameters in querystring q := url.Values{} for k, v := range params { q.Set(k, fmt.Sprintf("%v", v)) @@ -427,11 +427,11 @@ func (t *AsterTrader) doRequest(method, endpoint string, params map[string]inter return body, nil default: - return nil, fmt.Errorf("不支持的HTTP方法: %s", method) + return nil, fmt.Errorf("unsupported HTTP method: %s", method) } } -// GetBalance 获取账户余额 +// GetBalance Get account balance func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { params := make(map[string]interface{}) body, err := t.request("GET", "/fapi/v3/balance", params) @@ -444,7 +444,7 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { return nil, err } - // 查找USDT余额 + // Find USDT balance availableBalance := 0.0 crossUnPnl := 0.0 crossWalletBalance := 0.0 @@ -454,7 +454,7 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { if asset, ok := bal["asset"].(string); ok && asset == "USDT" { foundUSDT = true - // 解析Aster字段(参考: https://github.com/asterdex/api-docs) + // Parse Aster fields (reference: https://github.com/asterdex/api-docs) if avail, ok := bal["availableBalance"].(string); ok { availableBalance, _ = strconv.ParseFloat(avail, 64) } @@ -469,14 +469,14 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { } if !foundUSDT { - logger.Infof("⚠️ 未找到USDT资产记录!") + logger.Infof("⚠️ USDT asset record not found!") } - // 获取持仓计算保证金占用和真实未实现盈亏 + // Get positions to calculate margin used and real unrealized PnL positions, err := t.GetPositions() if err != nil { - logger.Infof("⚠️ 获取持仓信息失败: %v", err) - // fallback: 无法获取持仓时使用简单计算 + logger.Infof("⚠️ Failed to get position information: %v", err) + // fallback: use simple calculation when unable to get positions return map[string]interface{}{ "totalWalletBalance": crossWalletBalance, "availableBalance": availableBalance, @@ -484,8 +484,8 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { }, nil } - // ⚠️ 关键修复:从持仓中累加真正的未实现盈亏 - // Aster 的 crossUnPnl 字段不准确,需要从持仓数据中重新计算 + // ⚠️ Critical fix: accumulate real unrealized PnL from positions + // Aster's crossUnPnl field is inaccurate, need to recalculate from position data totalMarginUsed := 0.0 realUnrealizedPnl := 0.0 for _, pos := range positions { @@ -505,21 +505,21 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { totalMarginUsed += marginUsed } - // ✅ Aster 正确计算方式: - // 总净值 = 可用余额 + 保证金占用 - // 钱包余额 = 总净值 - 未实现盈亏 - // 未实现盈亏 = 从持仓累加计算(不使用API的crossUnPnl) + // ✅ Aster correct calculation method: + // Total equity = available balance + margin used + // Wallet balance = total equity - unrealized PnL + // Unrealized PnL = calculated from accumulated positions (don't use API's crossUnPnl) totalEquity := availableBalance + totalMarginUsed totalWalletBalance := totalEquity - realUnrealizedPnl return map[string]interface{}{ - "totalWalletBalance": totalWalletBalance, // 钱包余额(不含未实现盈亏) - "availableBalance": availableBalance, // 可用余额 - "totalUnrealizedProfit": realUnrealizedPnl, // 未实现盈亏(从持仓累加) + "totalWalletBalance": totalWalletBalance, // Wallet balance (excluding unrealized PnL) + "availableBalance": availableBalance, // Available balance + "totalUnrealizedProfit": realUnrealizedPnl, // Unrealized PnL (accumulated from positions) }, nil } -// GetPositions 获取持仓信息 +// GetPositions Get position information func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) { params := make(map[string]interface{}) body, err := t.request("GET", "/fapi/v3/positionRisk", params) @@ -541,7 +541,7 @@ func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) { posAmt, _ := strconv.ParseFloat(posAmtStr, 64) if posAmt == 0 { - continue // 跳过空仓位 + continue // Skip empty positions } entryPrice, _ := strconv.ParseFloat(pos["entryPrice"].(string), 64) @@ -550,14 +550,14 @@ func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) { leverageVal, _ := strconv.ParseFloat(pos["leverage"].(string), 64) liquidationPrice, _ := strconv.ParseFloat(pos["liquidationPrice"].(string), 64) - // 判断方向(与Binance一致) + // Determine direction (consistent with Binance) side := "long" if posAmt < 0 { side = "short" posAmt = -posAmt } - // 返回与Binance相同的字段名 + // Return same field names as Binance result = append(result, map[string]interface{}{ "symbol": pos["symbol"], "side": side, @@ -573,28 +573,28 @@ func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) { return result, nil } -// OpenLong 开多单 +// OpenLong Open long position func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 开仓前先取消所有挂单,防止残留挂单导致仓位叠加 + // Cancel all pending orders before opening position to prevent position stacking from residual orders if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败(继续开仓): %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders (continuing to open position): %v", err) } - // 先设置杠杆 + // Set leverage first if err := t.SetLeverage(symbol, leverage); err != nil { - return nil, fmt.Errorf("设置杠杆失败: %w", err) + return nil, fmt.Errorf("failed to set leverage: %w", err) } - // 获取当前价格 + // Get current price price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } - // 使用限价单模拟市价单(价格设置得稍高一些以确保成交) + // Use limit order to simulate market order (price set slightly higher to ensure execution) limitPrice := price * 1.01 - // 格式化价格和数量到正确精度 + // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) if err != nil { return nil, err @@ -604,17 +604,17 @@ func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (m return nil, err } - // 获取精度信息 + // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return nil, err } - // 转换为字符串,使用正确的精度格式 + // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) - logger.Infof(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)", + logger.Infof(" 📏 Precision handling: price %.8f -> %s (precision=%d), quantity %.8f -> %s (precision=%d)", limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) params := map[string]interface{}{ @@ -640,28 +640,28 @@ func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (m return result, nil } -// OpenShort 开空单 +// OpenShort Open short position func (t *AsterTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 开仓前先取消所有挂单,防止残留挂单导致仓位叠加 + // Cancel all pending orders before opening position to prevent position stacking from residual orders if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败(继续开仓): %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders (continuing to open position): %v", err) } - // 先设置杠杆 + // Set leverage first if err := t.SetLeverage(symbol, leverage); err != nil { - return nil, fmt.Errorf("设置杠杆失败: %w", err) + return nil, fmt.Errorf("failed to set leverage: %w", err) } - // 获取当前价格 + // Get current price price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } - // 使用限价单模拟市价单(价格设置得稍低一些以确保成交) + // Use limit order to simulate market order (price set slightly lower to ensure execution) limitPrice := price * 0.99 - // 格式化价格和数量到正确精度 + // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) if err != nil { return nil, err @@ -671,17 +671,17 @@ func (t *AsterTrader) OpenShort(symbol string, quantity float64, leverage int) ( return nil, err } - // 获取精度信息 + // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return nil, err } - // 转换为字符串,使用正确的精度格式 + // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) - logger.Infof(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)", + logger.Infof(" 📏 Precision handling: price %.8f -> %s (precision=%d), quantity %.8f -> %s (precision=%d)", limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) params := map[string]interface{}{ @@ -707,9 +707,9 @@ func (t *AsterTrader) OpenShort(symbol string, quantity float64, leverage int) ( return result, nil } -// CloseLong 平多单 +// CloseLong Close long position func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果数量为0,获取当前持仓数量 + // If quantity is 0, get current position quantity if quantity == 0 { positions, err := t.GetPositions() if err != nil { @@ -724,9 +724,9 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int } if quantity == 0 { - return nil, fmt.Errorf("没有找到 %s 的多仓", symbol) + return nil, fmt.Errorf("no long position found for %s", symbol) } - logger.Infof(" 📊 获取到多仓数量: %.8f", quantity) + logger.Infof(" 📊 Retrieved long position quantity: %.8f", quantity) } price, err := t.GetMarketPrice(symbol) @@ -736,7 +736,7 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int limitPrice := price * 0.99 - // 格式化价格和数量到正确精度 + // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) if err != nil { return nil, err @@ -746,17 +746,17 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int return nil, err } - // 获取精度信息 + // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return nil, err } - // 转换为字符串,使用正确的精度格式 + // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) - logger.Infof(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)", + logger.Infof(" 📏 Precision handling: price %.8f -> %s (precision=%d), quantity %.8f -> %s (precision=%d)", limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) params := map[string]interface{}{ @@ -779,19 +779,19 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int return nil, err } - logger.Infof("✓ 平多仓成功: %s 数量: %s", symbol, qtyStr) + logger.Infof("✓ Successfully closed long position: %s quantity: %s", symbol, qtyStr) - // 平仓后取消该币种的所有挂单(止损止盈单) + // Cancel all pending orders for this symbol after closing position (stop-loss/take-profit orders) if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } return result, nil } -// CloseShort 平空单 +// CloseShort Close short position func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果数量为0,获取当前持仓数量 + // If quantity is 0, get current position quantity if quantity == 0 { positions, err := t.GetPositions() if err != nil { @@ -800,16 +800,16 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in for _, pos := range positions { if pos["symbol"] == symbol && pos["side"] == "short" { - // Aster的GetPositions已经将空仓数量转换为正数,直接使用 + // Aster's GetPositions has already converted short position quantity to positive, use directly quantity = pos["positionAmt"].(float64) break } } if quantity == 0 { - return nil, fmt.Errorf("没有找到 %s 的空仓", symbol) + return nil, fmt.Errorf("no short position found for %s", symbol) } - logger.Infof(" 📊 获取到空仓数量: %.8f", quantity) + logger.Infof(" 📊 Retrieved short position quantity: %.8f", quantity) } price, err := t.GetMarketPrice(symbol) @@ -819,7 +819,7 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in limitPrice := price * 1.01 - // 格式化价格和数量到正确精度 + // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) if err != nil { return nil, err @@ -829,17 +829,17 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in return nil, err } - // 获取精度信息 + // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return nil, err } - // 转换为字符串,使用正确的精度格式 + // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) - logger.Infof(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)", + logger.Infof(" 📏 Precision handling: price %.8f -> %s (precision=%d), quantity %.8f -> %s (precision=%d)", limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) params := map[string]interface{}{ @@ -862,20 +862,20 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in return nil, err } - logger.Infof("✓ 平空仓成功: %s 数量: %s", symbol, qtyStr) + logger.Infof("✓ Successfully closed short position: %s quantity: %s", symbol, qtyStr) - // 平仓后取消该币种的所有挂单(止损止盈单) + // Cancel all pending orders for this symbol after closing position (stop-loss/take-profit orders) if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } return result, nil } -// SetMarginMode 设置仓位模式 +// SetMarginMode Set margin mode func (t *AsterTrader) SetMarginMode(symbol string, isCrossMargin bool) error { - // Aster支持仓位模式设置 - // API格式与币安相似:CROSSED(全仓) / ISOLATED(逐仓) + // Aster supports margin mode settings + // API format similar to Binance: CROSSED (cross margin) / ISOLATED (isolated margin) marginType := "CROSSED" if !isCrossMargin { marginType = "ISOLATED" @@ -886,40 +886,40 @@ func (t *AsterTrader) SetMarginMode(symbol string, isCrossMargin bool) error { "marginType": marginType, } - // 使用request方法调用API + // Use request method to call API _, err := t.request("POST", "/fapi/v3/marginType", params) if err != nil { - // 如果错误表示无需更改,忽略错误 + // Ignore error if it indicates no need to change if strings.Contains(err.Error(), "No need to change") || strings.Contains(err.Error(), "Margin type cannot be changed") { - logger.Infof(" ✓ %s 仓位模式已是 %s 或有持仓无法更改", symbol, marginType) + logger.Infof(" ✓ %s margin mode is already %s or cannot be changed due to existing positions", symbol, marginType) return nil } - // 检测多资产模式(错误码 -4168) + // Detect multi-assets mode (error code -4168) if strings.Contains(err.Error(), "Multi-Assets mode") || strings.Contains(err.Error(), "-4168") || strings.Contains(err.Error(), "4168") { - logger.Infof(" ⚠️ %s 检测到多资产模式,强制使用全仓模式", symbol) - logger.Infof(" 💡 提示:如需使用逐仓模式,请在交易所关闭多资产模式") + logger.Infof(" ⚠️ %s detected multi-assets mode, forcing cross margin mode", symbol) + logger.Infof(" 💡 Tip: To use isolated margin mode, please disable multi-assets mode on the exchange") return nil } - // 检测统一账户 API + // Detect unified account API if strings.Contains(err.Error(), "unified") || strings.Contains(err.Error(), "portfolio") || strings.Contains(err.Error(), "Portfolio") { - logger.Infof(" ❌ %s 检测到统一账户 API,无法进行合约交易", symbol) - return fmt.Errorf("请使用「现货与合约交易」API 权限,不要使用「统一账户 API」") + logger.Infof(" ❌ %s detected unified account API, cannot perform futures trading", symbol) + return fmt.Errorf("please use 'Spot & Futures Trading' API permission, not 'Unified Account API'") } - logger.Infof(" ⚠️ 设置仓位模式失败: %v", err) - // 不返回错误,让交易继续 + logger.Infof(" ⚠️ Failed to set margin mode: %v", err) + // Don't return error, let trading continue return nil } - logger.Infof(" ✓ %s 仓位模式已设置为 %s", symbol, marginType) + logger.Infof(" ✓ %s margin mode has been set to %s", symbol, marginType) return nil } -// SetLeverage 设置杠杆倍数 +// SetLeverage Set leverage multiplier func (t *AsterTrader) SetLeverage(symbol string, leverage int) error { params := map[string]interface{}{ "symbol": symbol, @@ -930,9 +930,9 @@ func (t *AsterTrader) SetLeverage(symbol string, leverage int) error { return err } -// GetMarketPrice 获取市场价格 +// GetMarketPrice Get market price func (t *AsterTrader) GetMarketPrice(symbol string) (float64, error) { - // 使用ticker接口获取当前价格 + // Use ticker interface to get current price resp, err := t.client.Get(fmt.Sprintf("%s/fapi/v3/ticker/price?symbol=%s", t.baseURL, symbol)) if err != nil { return 0, err @@ -951,20 +951,20 @@ func (t *AsterTrader) GetMarketPrice(symbol string) (float64, error) { priceStr, ok := result["price"].(string) if !ok { - return 0, errors.New("无法获取价格") + return 0, errors.New("unable to get price") } return strconv.ParseFloat(priceStr, 64) } -// SetStopLoss 设置止损 +// SetStopLoss Set stop loss func (t *AsterTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { side := "SELL" if positionSide == "SHORT" { side = "BUY" } - // 格式化价格和数量到正确精度 + // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, stopPrice) if err != nil { return err @@ -974,13 +974,13 @@ func (t *AsterTrader) SetStopLoss(symbol string, positionSide string, quantity, return err } - // 获取精度信息 + // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return err } - // 转换为字符串,使用正确的精度格式 + // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) @@ -998,14 +998,14 @@ func (t *AsterTrader) SetStopLoss(symbol string, positionSide string, quantity, return err } -// SetTakeProfit 设置止盈 +// SetTakeProfit Set take profit func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { side := "SELL" if positionSide == "SHORT" { side = "BUY" } - // 格式化价格和数量到正确精度 + // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, takeProfitPrice) if err != nil { return err @@ -1015,13 +1015,13 @@ func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity return err } - // 获取精度信息 + // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return err } - // 转换为字符串,使用正确的精度格式 + // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) @@ -1039,30 +1039,30 @@ func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity return err } -// CancelStopLossOrders 仅取消止损单(不影响止盈单) +// CancelStopLossOrders Cancel stop-loss orders only (does not affect take-profit orders) func (t *AsterTrader) CancelStopLossOrders(symbol string) error { - // 获取该币种的所有未完成订单 + // Get all open orders for this symbol params := map[string]interface{}{ "symbol": symbol, } body, err := t.request("GET", "/fapi/v3/openOrders", params) if err != nil { - return fmt.Errorf("获取未完成订单失败: %w", err) + return fmt.Errorf("failed to get open orders: %w", err) } var orders []map[string]interface{} if err := json.Unmarshal(body, &orders); err != nil { - return fmt.Errorf("解析订单数据失败: %w", err) + return fmt.Errorf("failed to parse order data: %w", err) } - // 过滤出止损单并取消(取消所有方向的止损单,包括LONG和SHORT) + // Filter and cancel stop-loss orders (cancel all directions including LONG and SHORT) canceledCount := 0 var cancelErrors []error for _, order := range orders { orderType, _ := order["type"].(string) - // 只取消止损订单(不取消止盈订单) + // Only cancel stop-loss orders (don't cancel take-profit orders) if orderType == "STOP_MARKET" || orderType == "STOP" { orderID, _ := order["orderId"].(float64) positionSide, _ := order["positionSide"].(string) @@ -1073,55 +1073,55 @@ func (t *AsterTrader) CancelStopLossOrders(symbol string) error { _, err := t.request("DELETE", "/fapi/v1/order", cancelParams) if err != nil { - errMsg := fmt.Sprintf("订单ID %d: %v", int64(orderID), err) + errMsg := fmt.Sprintf("order ID %d: %v", int64(orderID), err) cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg)) - logger.Infof(" ⚠ 取消止损单失败: %s", errMsg) + logger.Infof(" ⚠ Failed to cancel stop-loss order: %s", errMsg) continue } canceledCount++ - logger.Infof(" ✓ 已取消止损单 (订单ID: %d, 类型: %s, 方向: %s)", int64(orderID), orderType, positionSide) + logger.Infof(" ✓ Canceled stop-loss order (order ID: %d, type: %s, direction: %s)", int64(orderID), orderType, positionSide) } } if canceledCount == 0 && len(cancelErrors) == 0 { - logger.Infof(" ℹ %s 没有止损单需要取消", symbol) + logger.Infof(" ℹ %s no stop-loss orders to cancel", symbol) } else if canceledCount > 0 { - logger.Infof(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount) + logger.Infof(" ✓ Canceled %d stop-loss order(s) for %s", canceledCount, symbol) } - // 如果所有取消都失败了,返回错误 + // Return error if all cancellations failed if len(cancelErrors) > 0 && canceledCount == 0 { - return fmt.Errorf("取消止损单失败: %v", cancelErrors) + return fmt.Errorf("failed to cancel stop-loss orders: %v", cancelErrors) } return nil } -// CancelTakeProfitOrders 仅取消止盈单(不影响止损单) +// CancelTakeProfitOrders Cancel take-profit orders only (does not affect stop-loss orders) func (t *AsterTrader) CancelTakeProfitOrders(symbol string) error { - // 获取该币种的所有未完成订单 + // Get all open orders for this symbol params := map[string]interface{}{ "symbol": symbol, } body, err := t.request("GET", "/fapi/v3/openOrders", params) if err != nil { - return fmt.Errorf("获取未完成订单失败: %w", err) + return fmt.Errorf("failed to get open orders: %w", err) } var orders []map[string]interface{} if err := json.Unmarshal(body, &orders); err != nil { - return fmt.Errorf("解析订单数据失败: %w", err) + return fmt.Errorf("failed to parse order data: %w", err) } - // 过滤出止盈单并取消(取消所有方向的止盈单,包括LONG和SHORT) + // Filter and cancel take-profit orders (cancel all directions including LONG and SHORT) canceledCount := 0 var cancelErrors []error for _, order := range orders { orderType, _ := order["type"].(string) - // 只取消止盈订单(不取消止损订单) + // Only cancel take-profit orders (don't cancel stop-loss orders) if orderType == "TAKE_PROFIT_MARKET" || orderType == "TAKE_PROFIT" { orderID, _ := order["orderId"].(float64) positionSide, _ := order["positionSide"].(string) @@ -1132,32 +1132,32 @@ func (t *AsterTrader) CancelTakeProfitOrders(symbol string) error { _, err := t.request("DELETE", "/fapi/v1/order", cancelParams) if err != nil { - errMsg := fmt.Sprintf("订单ID %d: %v", int64(orderID), err) + errMsg := fmt.Sprintf("order ID %d: %v", int64(orderID), err) cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg)) - logger.Infof(" ⚠ 取消止盈单失败: %s", errMsg) + logger.Infof(" ⚠ Failed to cancel take-profit order: %s", errMsg) continue } canceledCount++ - logger.Infof(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s, 方向: %s)", int64(orderID), orderType, positionSide) + logger.Infof(" ✓ Canceled take-profit order (order ID: %d, type: %s, direction: %s)", int64(orderID), orderType, positionSide) } } if canceledCount == 0 && len(cancelErrors) == 0 { - logger.Infof(" ℹ %s 没有止盈单需要取消", symbol) + logger.Infof(" ℹ %s no take-profit orders to cancel", symbol) } else if canceledCount > 0 { - logger.Infof(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount) + logger.Infof(" ✓ Canceled %d take-profit order(s) for %s", canceledCount, symbol) } - // 如果所有取消都失败了,返回错误 + // Return error if all cancellations failed if len(cancelErrors) > 0 && canceledCount == 0 { - return fmt.Errorf("取消止盈单失败: %v", cancelErrors) + return fmt.Errorf("failed to cancel take-profit orders: %v", cancelErrors) } return nil } -// CancelAllOrders 取消所有订单 +// CancelAllOrders Cancel all orders func (t *AsterTrader) CancelAllOrders(symbol string) error { params := map[string]interface{}{ "symbol": symbol, @@ -1167,29 +1167,29 @@ func (t *AsterTrader) CancelAllOrders(symbol string) error { return err } -// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +// CancelStopOrders Cancel take-profit/stop-loss orders for this symbol (used to adjust TP/SL positions) func (t *AsterTrader) CancelStopOrders(symbol string) error { - // 获取该币种的所有未完成订单 + // Get all open orders for this symbol params := map[string]interface{}{ "symbol": symbol, } body, err := t.request("GET", "/fapi/v3/openOrders", params) if err != nil { - return fmt.Errorf("获取未完成订单失败: %w", err) + return fmt.Errorf("failed to get open orders: %w", err) } var orders []map[string]interface{} if err := json.Unmarshal(body, &orders); err != nil { - return fmt.Errorf("解析订单数据失败: %w", err) + return fmt.Errorf("failed to parse order data: %w", err) } - // 过滤出止盈止损单并取消 + // Filter and cancel take-profit/stop-loss orders canceledCount := 0 for _, order := range orders { orderType, _ := order["type"].(string) - // 只取消止损和止盈订单 + // Only cancel stop-loss and take-profit orders if orderType == "STOP_MARKET" || orderType == "TAKE_PROFIT_MARKET" || orderType == "STOP" || @@ -1203,26 +1203,26 @@ func (t *AsterTrader) CancelStopOrders(symbol string) error { _, err := t.request("DELETE", "/fapi/v3/order", cancelParams) if err != nil { - logger.Infof(" ⚠ 取消订单 %d 失败: %v", int64(orderID), err) + logger.Infof(" ⚠ Failed to cancel order %d: %v", int64(orderID), err) continue } canceledCount++ - logger.Infof(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)", + logger.Infof(" ✓ Canceled take-profit/stop-loss order for %s (order ID: %d, type: %s)", symbol, int64(orderID), orderType) } } if canceledCount == 0 { - logger.Infof(" ℹ %s 没有止盈/止损单需要取消", symbol) + logger.Infof(" ℹ %s no take-profit/stop-loss orders to cancel", symbol) } else { - logger.Infof(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + logger.Infof(" ✓ Canceled %d take-profit/stop-loss order(s) for %s", canceledCount, symbol) } return nil } -// FormatQuantity 格式化数量(实现Trader接口) +// FormatQuantity Format quantity (implements Trader interface) func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) { formatted, err := t.formatQuantity(symbol, quantity) if err != nil { @@ -1231,7 +1231,7 @@ func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, e return fmt.Sprintf("%v", formatted), nil } -// GetOrderStatus 获取订单状态 +// GetOrderStatus Get order status func (t *AsterTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { params := map[string]interface{}{ "symbol": symbol, @@ -1240,15 +1240,15 @@ func (t *AsterTrader) GetOrderStatus(symbol string, orderID string) (map[string] body, err := t.request("GET", "/fapi/v3/order", params) if err != nil { - return nil, fmt.Errorf("获取订单状态失败: %w", err) + return nil, fmt.Errorf("failed to get order status: %w", err) } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("解析订单响应失败: %w", err) + return nil, fmt.Errorf("failed to parse order response: %w", err) } - // 标准化返回字段 + // Standardize return fields response := map[string]interface{}{ "orderId": result["orderId"], "symbol": result["symbol"], @@ -1257,10 +1257,10 @@ func (t *AsterTrader) GetOrderStatus(symbol string, orderID string) (map[string] "type": result["type"], "time": result["time"], "updateTime": result["updateTime"], - "commission": 0.0, // Aster 可能需要单独查询 + "commission": 0.0, // Aster may require separate query } - // 解析数值字段 + // Parse numeric fields if avgPrice, ok := result["avgPrice"].(string); ok { if v, err := strconv.ParseFloat(avgPrice, 64); err == nil { response["avgPrice"] = v diff --git a/trader/aster_trader_test.go b/trader/aster_trader_test.go index 19a0b4a2..0a7a8c8e 100644 --- a/trader/aster_trader_test.go +++ b/trader/aster_trader_test.go @@ -13,27 +13,27 @@ import ( ) // ============================================================ -// 一、AsterTraderTestSuite - 继承 base test suite +// 1. AsterTraderTestSuite - inherits base test suite // ============================================================ -// AsterTraderTestSuite Aster交易器测试套件 -// 继承 TraderTestSuite 并添加 Aster 特定的 mock 逻辑 +// AsterTraderTestSuite Aster trader test suite +// Inherits TraderTestSuite and adds Aster specific mock logic type AsterTraderTestSuite struct { - *TraderTestSuite // 嵌入基础测试套件 + *TraderTestSuite // Embeds base test suite mockServer *httptest.Server } -// NewAsterTraderTestSuite 创建 Aster 测试套件 +// NewAsterTraderTestSuite creates Aster test suite func NewAsterTraderTestSuite(t *testing.T) *AsterTraderTestSuite { - // 创建 mock HTTP 服务器 + // Create mock HTTP server mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 根据不同的 URL 路径返回不同的 mock 响应 + // Return different mock responses based on URL path path := r.URL.Path var respBody interface{} switch { - // Mock GetBalance - /fapi/v3/balance (返回数组) + // Mock GetBalance - /fapi/v3/balance (returns array) case path == "/fapi/v3/balance": respBody = []map[string]interface{}{ { @@ -65,19 +65,19 @@ func NewAsterTraderTestSuite(t *testing.T) *AsterTraderTestSuite { }, } - // Mock GetMarketPrice - /fapi/v3/ticker/price (返回单个对象) + // Mock GetMarketPrice - /fapi/v3/ticker/price (returns single object) case path == "/fapi/v3/ticker/price": - // 从查询参数获取symbol + // Get symbol from query parameters symbol := r.URL.Query().Get("symbol") if symbol == "" { symbol = "BTCUSDT" } - // 根据symbol返回不同价格 + // Return different price based on symbol price := "50000.00" if symbol == "ETHUSDT" { price = "3000.00" } else if symbol == "INVALIDUSDT" { - // 返回错误响应 + // Return error response w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]interface{}{ "code": -1121, @@ -133,7 +133,7 @@ func NewAsterTraderTestSuite(t *testing.T) *AsterTraderTestSuite { // Mock CreateOrder - /fapi/v1/order and /fapi/v3/order case (path == "/fapi/v1/order" || path == "/fapi/v3/order") && r.Method == "POST": - // 从请求中解析参数以确定symbol + // Parse parameters from request to determine symbol bodyBytes, _ := io.ReadAll(r.Body) var orderParams map[string]interface{} json.Unmarshal(bodyBytes, &orderParams) @@ -182,26 +182,26 @@ func NewAsterTraderTestSuite(t *testing.T) *AsterTraderTestSuite { respBody = map[string]interface{}{} } - // 序列化响应 + // Serialize response w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(respBody) })) - // 生成一个测试用的私钥 + // Generate a private key for testing privateKey, _ := crypto.GenerateKey() - // 创建 mock trader,使用 mock server 的 URL + // Create mock trader using mock server's URL trader := &AsterTrader{ ctx: context.Background(), user: "0x1234567890123456789012345678901234567890", signer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", privateKey: privateKey, client: mockServer.Client(), - baseURL: mockServer.URL, // 使用 mock server 的 URL + baseURL: mockServer.URL, // Use mock server's URL symbolPrecision: make(map[string]SymbolPrecision), } - // 创建基础套件 + // Create base suite baseSuite := NewTraderTestSuite(t, trader) return &AsterTraderTestSuite{ @@ -210,7 +210,7 @@ func NewAsterTraderTestSuite(t *testing.T) *AsterTraderTestSuite { } } -// Cleanup 清理资源 +// Cleanup cleans up resources func (s *AsterTraderTestSuite) Cleanup() { if s.mockServer != nil { s.mockServer.Close() @@ -219,29 +219,29 @@ func (s *AsterTraderTestSuite) Cleanup() { } // ============================================================ -// 二、使用 AsterTraderTestSuite 运行通用测试 +// 2. Run common tests using AsterTraderTestSuite // ============================================================ -// TestAsterTrader_InterfaceCompliance 测试接口兼容性 +// TestAsterTrader_InterfaceCompliance tests interface compliance func TestAsterTrader_InterfaceCompliance(t *testing.T) { var _ Trader = (*AsterTrader)(nil) } -// TestAsterTrader_CommonInterface 使用测试套件运行所有通用接口测试 +// TestAsterTrader_CommonInterface runs all common interface tests using test suite func TestAsterTrader_CommonInterface(t *testing.T) { - // 创建测试套件 + // Create test suite suite := NewAsterTraderTestSuite(t) defer suite.Cleanup() - // 运行所有通用接口测试 + // Run all common interface tests suite.RunAllTests() } // ============================================================ -// 三、Aster 特定功能的单元测试 +// 3. Aster specific unit tests // ============================================================ -// TestNewAsterTrader 测试创建 Aster 交易器 +// TestNewAsterTrader tests creating Aster trader func TestNewAsterTrader(t *testing.T) { tests := []struct { name string @@ -252,22 +252,22 @@ func TestNewAsterTrader(t *testing.T) { errorContains string }{ { - name: "成功创建", + name: "successful creation", user: "0x1234567890123456789012345678901234567890", signer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", privateKeyHex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", wantError: false, }, { - name: "无效私钥格式", + name: "invalid private key format", user: "0x1234567890123456789012345678901234567890", signer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", privateKeyHex: "invalid_key", wantError: true, - errorContains: "解析私钥失败", + errorContains: "failed to parse private key", }, { - name: "带0x前缀的私钥", + name: "private key with 0x prefix", user: "0x1234567890123456789012345678901234567890", signer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", privateKeyHex: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", diff --git a/trader/auto_trader.go b/trader/auto_trader.go index e02df5f1..087f2bf2 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -14,107 +14,107 @@ import ( "time" ) -// AutoTraderConfig 自动交易配置(简化版 - AI全权决策) +// AutoTraderConfig auto trading configuration (simplified version - AI makes all decisions) type AutoTraderConfig struct { - // Trader标识 - ID string // Trader唯一标识(用于日志目录等) - Name string // Trader显示名称 - AIModel string // AI模型: "qwen" 或 "deepseek" + // Trader identification + ID string // Trader unique identifier (for log directory, etc.) + Name string // Trader display name + AIModel string // AI model: "qwen" or "deepseek" - // 交易平台选择 - Exchange string // "binance", "bybit", "okx", "hyperliquid", "aster" 或 "lighter" + // Trading platform selection + Exchange string // "binance", "bybit", "okx", "hyperliquid", "aster" or "lighter" - // 币安API配置 + // Binance API configuration BinanceAPIKey string BinanceSecretKey string - // Bybit API配置 + // Bybit API configuration BybitAPIKey string BybitSecretKey string - // OKX API配置 + // OKX API configuration OKXAPIKey string OKXSecretKey string OKXPassphrase string - // Hyperliquid配置 + // Hyperliquid configuration HyperliquidPrivateKey string HyperliquidWalletAddr string HyperliquidTestnet bool - // Aster配置 - AsterUser string // Aster主钱包地址 - AsterSigner string // Aster API钱包地址 - AsterPrivateKey string // Aster API钱包私钥 + // Aster configuration + AsterUser string // Aster main wallet address + AsterSigner string // Aster API wallet address + AsterPrivateKey string // Aster API wallet private key - // LIGHTER配置 - LighterWalletAddr string // LIGHTER钱包地址(L1 wallet) - LighterPrivateKey string // LIGHTER L1私钥(用于识别账户) - LighterAPIKeyPrivateKey string // LIGHTER API Key私钥(40字节,用于签名交易) - LighterTestnet bool // 是否使用testnet + // LIGHTER configuration + LighterWalletAddr string // LIGHTER wallet address (L1 wallet) + LighterPrivateKey string // LIGHTER L1 private key (for account identification) + LighterAPIKeyPrivateKey string // LIGHTER API Key private key (40 bytes, for transaction signing) + LighterTestnet bool // Whether to use testnet - // AI配置 + // AI configuration UseQwen bool DeepSeekKey string QwenKey string - // 自定义AI API配置 + // Custom AI API configuration CustomAPIURL string CustomAPIKey string CustomModelName string - // 扫描配置 - ScanInterval time.Duration // 扫描间隔(建议3分钟) + // Scan configuration + ScanInterval time.Duration // Scan interval (recommended 3 minutes) - // 账户配置 - InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置) + // Account configuration + InitialBalance float64 // Initial balance (for P&L calculation, must be set manually) - // 风险控制(仅作为提示,AI可自主决定) - MaxDailyLoss float64 // 最大日亏损百分比(提示) - MaxDrawdown float64 // 最大回撤百分比(提示) - StopTradingTime time.Duration // 触发风控后暂停时长 + // Risk control (only as hints, AI can make autonomous decisions) + MaxDailyLoss float64 // Maximum daily loss percentage (hint) + MaxDrawdown float64 // Maximum drawdown percentage (hint) + StopTradingTime time.Duration // Pause duration after risk control triggers - // 仓位模式 - IsCrossMargin bool // true=全仓模式, false=逐仓模式 + // Position mode + IsCrossMargin bool // true=cross margin mode, false=isolated margin mode - // 策略配置(使用完整策略配置) - StrategyConfig *store.StrategyConfig // 策略配置(包含币种来源、指标、风控、Prompt等) + // Strategy configuration (use complete strategy config) + StrategyConfig *store.StrategyConfig // Strategy configuration (includes coin sources, indicators, risk control, prompts, etc.) } -// AutoTrader 自动交易器 +// AutoTrader automatic trader type AutoTrader struct { - id string // Trader唯一标识 - name string // Trader显示名称 - aiModel string // AI模型名称 - exchange string // 交易平台名称 + id string // Trader unique identifier + name string // Trader display name + aiModel string // AI model name + exchange string // Trading platform name config AutoTraderConfig - trader Trader // 使用Trader接口(支持多平台) + trader Trader // Use Trader interface (supports multiple platforms) mcpClient mcp.AIClient - store *store.Store // 数据存储(决策记录等) - strategyEngine *decision.StrategyEngine // 策略引擎(使用策略配置) - cycleNumber int // 当前周期编号 + store *store.Store // Data storage (decision records, etc.) + strategyEngine *decision.StrategyEngine // Strategy engine (uses strategy configuration) + cycleNumber int // Current cycle number initialBalance float64 dailyPnL float64 - customPrompt string // 自定义交易策略prompt - overrideBasePrompt bool // 是否覆盖基础prompt + customPrompt string // Custom trading strategy prompt + overrideBasePrompt bool // Whether to override base prompt lastResetTime time.Time stopUntil time.Time isRunning bool - startTime time.Time // 系统启动时间 - callCount int // AI调用次数 - positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) - stopMonitorCh chan struct{} // 用于停止监控goroutine - monitorWg sync.WaitGroup // 用于等待监控goroutine结束 - peakPnLCache map[string]float64 // 最高收益缓存 (symbol -> 峰值盈亏百分比) - peakPnLCacheMutex sync.RWMutex // 缓存读写锁 - lastBalanceSyncTime time.Time // 上次余额同步时间 - userID string // 用户ID + startTime time.Time // System start time + callCount int // AI call count + positionFirstSeenTime map[string]int64 // Position first seen time (symbol_side -> timestamp in milliseconds) + stopMonitorCh chan struct{} // Used to stop monitoring goroutine + monitorWg sync.WaitGroup // Used to wait for monitoring goroutine to finish + peakPnLCache map[string]float64 // Peak profit cache (symbol -> peak P&L percentage) + peakPnLCacheMutex sync.RWMutex // Cache read-write lock + lastBalanceSyncTime time.Time // Last balance sync time + userID string // User ID } -// NewAutoTrader 创建自动交易器 -// st 参数用于存储决策记录到数据库 +// NewAutoTrader creates an automatic trader +// st parameter is used to store decision records to database func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*AutoTrader, error) { - // 设置默认值 + // Set default values if config.ID == "" { config.ID = "default_trader" } @@ -131,75 +131,75 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au mcpClient := mcp.New() - // 初始化AI + // Initialize AI if config.AIModel == "custom" { - // 使用自定义API + // Use custom API mcpClient.SetAPIKey(config.CustomAPIKey, config.CustomAPIURL, config.CustomModelName) - logger.Infof("🤖 [%s] 使用自定义AI API: %s (模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName) + logger.Infof("🤖 [%s] Using custom AI API: %s (model: %s)", config.Name, config.CustomAPIURL, config.CustomModelName) } else if config.UseQwen || config.AIModel == "qwen" { - // 使用Qwen (支持自定义URL和Model) + // Use Qwen (supports custom URL and Model) mcpClient = mcp.NewQwenClient() mcpClient.SetAPIKey(config.QwenKey, config.CustomAPIURL, config.CustomModelName) if config.CustomAPIURL != "" || config.CustomModelName != "" { - logger.Infof("🤖 [%s] 使用阿里云Qwen AI (自定义URL: %s, 模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName) + logger.Infof("🤖 [%s] Using Alibaba Cloud Qwen AI (custom URL: %s, model: %s)", config.Name, config.CustomAPIURL, config.CustomModelName) } else { - logger.Infof("🤖 [%s] 使用阿里云Qwen AI", config.Name) + logger.Infof("🤖 [%s] Using Alibaba Cloud Qwen AI", config.Name) } } else { - // 默认使用DeepSeek (支持自定义URL和Model) + // Default to DeepSeek (supports custom URL and Model) mcpClient = mcp.NewDeepSeekClient() mcpClient.SetAPIKey(config.DeepSeekKey, config.CustomAPIURL, config.CustomModelName) if config.CustomAPIURL != "" || config.CustomModelName != "" { - logger.Infof("🤖 [%s] 使用DeepSeek AI (自定义URL: %s, 模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName) + logger.Infof("🤖 [%s] Using DeepSeek AI (custom URL: %s, model: %s)", config.Name, config.CustomAPIURL, config.CustomModelName) } else { - logger.Infof("🤖 [%s] 使用DeepSeek AI", config.Name) + logger.Infof("🤖 [%s] Using DeepSeek AI", config.Name) } } - // 设置默认交易平台 + // Set default trading platform if config.Exchange == "" { config.Exchange = "binance" } - // 根据配置创建对应的交易器 + // Create corresponding trader based on configuration var trader Trader var err error - // 记录仓位模式(通用) - marginModeStr := "全仓" + // Record position mode (general) + marginModeStr := "Cross Margin" if !config.IsCrossMargin { - marginModeStr = "逐仓" + marginModeStr = "Isolated Margin" } - logger.Infof("📊 [%s] 仓位模式: %s", config.Name, marginModeStr) + logger.Infof("📊 [%s] Position mode: %s", config.Name, marginModeStr) switch config.Exchange { case "binance": - logger.Infof("🏦 [%s] 使用币安合约交易", config.Name) + logger.Infof("🏦 [%s] Using Binance Futures trading", config.Name) trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey, userID) case "bybit": - logger.Infof("🏦 [%s] 使用Bybit合约交易", config.Name) + logger.Infof("🏦 [%s] Using Bybit Futures trading", config.Name) trader = NewBybitTrader(config.BybitAPIKey, config.BybitSecretKey) case "okx": - logger.Infof("🏦 [%s] 使用OKX合约交易", config.Name) + logger.Infof("🏦 [%s] Using OKX Futures trading", config.Name) trader = NewOKXTrader(config.OKXAPIKey, config.OKXSecretKey, config.OKXPassphrase) case "hyperliquid": - logger.Infof("🏦 [%s] 使用Hyperliquid交易", config.Name) + logger.Infof("🏦 [%s] Using Hyperliquid trading", config.Name) trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet) if err != nil { - return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err) + return nil, fmt.Errorf("failed to initialize Hyperliquid trader: %w", err) } case "aster": - logger.Infof("🏦 [%s] 使用Aster交易", config.Name) + logger.Infof("🏦 [%s] Using Aster trading", config.Name) trader, err = NewAsterTrader(config.AsterUser, config.AsterSigner, config.AsterPrivateKey) if err != nil { - return nil, fmt.Errorf("初始化Aster交易器失败: %w", err) + return nil, fmt.Errorf("failed to initialize Aster trader: %w", err) } case "lighter": - logger.Infof("🏦 [%s] 使用LIGHTER交易", config.Name) + logger.Infof("🏦 [%s] Using LIGHTER trading", config.Name) - // 優先使用 V2(需要 API Key) + // Prefer V2 (requires API Key) if config.LighterAPIKeyPrivateKey != "" { - logger.Infof("✓ 使用 LIGHTER SDK (V2) - 完整簽名支持") + logger.Infof("✓ Using LIGHTER SDK (V2) - Full signature support") trader, err = NewLighterTraderV2( config.LighterPrivateKey, config.LighterWalletAddr, @@ -207,28 +207,28 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au config.LighterTestnet, ) if err != nil { - return nil, fmt.Errorf("初始化LIGHTER交易器(V2)失败: %w", err) + return nil, fmt.Errorf("failed to initialize LIGHTER trader (V2): %w", err) } } else { - // 降級使用 V1(基本HTTP實現) - logger.Infof("⚠️ 使用 LIGHTER 基本實現 (V1) - 功能受限,請配置 API Key") + // Fallback to V1 (basic HTTP implementation) + logger.Infof("⚠️ Using LIGHTER basic implementation (V1) - Limited functionality, please configure API Key") trader, err = NewLighterTrader(config.LighterPrivateKey, config.LighterWalletAddr, config.LighterTestnet) if err != nil { - return nil, fmt.Errorf("初始化LIGHTER交易器(V1)失败: %w", err) + return nil, fmt.Errorf("failed to initialize LIGHTER trader (V1): %w", err) } } default: - return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange) + return nil, fmt.Errorf("unsupported trading platform: %s", config.Exchange) } - // 验证初始金额配置,如果为0则自动从交易所获取 + // Validate initial balance configuration, auto-fetch from exchange if 0 if config.InitialBalance <= 0 { - logger.Infof("📊 [%s] 初始金额未设置,尝试从交易所获取当前余额...", config.Name) + logger.Infof("📊 [%s] Initial balance not set, attempting to fetch current balance from exchange...", config.Name) account, err := trader.GetBalance() if err != nil { - return nil, fmt.Errorf("初始金额未设置且无法从交易所获取余额: %w", err) + return nil, fmt.Errorf("initial balance not set and unable to fetch balance from exchange: %w", err) } - // 尝试多种余额字段名(不同交易所返回格式不同) + // Try multiple balance field names (different exchanges return different formats) balanceKeys := []string{"total_equity", "totalWalletBalance", "wallet_balance", "totalEq", "balance"} var foundBalance float64 for _, key := range balanceKeys { @@ -239,25 +239,25 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au } if foundBalance > 0 { config.InitialBalance = foundBalance - logger.Infof("✓ [%s] 自动获取初始金额: %.2f USDT", config.Name, foundBalance) + logger.Infof("✓ [%s] Auto-fetched initial balance: %.2f USDT", config.Name, foundBalance) } else { - return nil, fmt.Errorf("初始金额必须大于0,请在配置中设置InitialBalance或确保交易所账户有余额") + return nil, fmt.Errorf("initial balance must be greater than 0, please set InitialBalance in config or ensure exchange account has balance") } } - // 获取最后的周期编号(用于恢复) + // Get last cycle number (for recovery) var cycleNumber int if st != nil { cycleNumber, _ = st.Decision().GetLastCycleNumber(config.ID) - logger.Infof("📊 [%s] 决策记录将存储到数据库", config.Name) + logger.Infof("📊 [%s] Decision records will be stored to database", config.Name) } - // 创建策略引擎(必须有策略配置) + // Create strategy engine (must have strategy config) if config.StrategyConfig == nil { - return nil, fmt.Errorf("[%s] 未配置策略", config.Name) + return nil, fmt.Errorf("[%s] strategy not configured", config.Name) } strategyEngine := decision.NewStrategyEngine(config.StrategyConfig) - logger.Infof("✓ [%s] 使用策略引擎(策略配置已加载)", config.Name) + logger.Infof("✓ [%s] Using strategy engine (strategy configuration loaded)", config.Name) return &AutoTrader{ id: config.ID, @@ -285,38 +285,38 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au }, nil } -// Run 运行自动交易主循环 +// Run runs the automatic trading main loop func (at *AutoTrader) Run() error { at.isRunning = true at.stopMonitorCh = make(chan struct{}) at.startTime = time.Now() - logger.Info("🚀 AI驱动自动交易系统启动") - logger.Infof("💰 初始余额: %.2f USDT", at.initialBalance) - logger.Infof("⚙️ 扫描间隔: %v", at.config.ScanInterval) - logger.Info("🤖 AI将全权决定杠杆、仓位大小、止损止盈等参数") + logger.Info("🚀 AI-driven automatic trading system started") + logger.Infof("💰 Initial balance: %.2f USDT", at.initialBalance) + logger.Infof("⚙️ Scan interval: %v", at.config.ScanInterval) + logger.Info("🤖 AI will make full decisions on leverage, position size, stop loss/take profit, etc.") at.monitorWg.Add(1) defer at.monitorWg.Done() - // 启动回撤监控 + // Start drawdown monitoring at.startDrawdownMonitor() ticker := time.NewTicker(at.config.ScanInterval) defer ticker.Stop() - // 首次立即执行 + // Execute immediately on first run if err := at.runCycle(); err != nil { - logger.Infof("❌ 执行失败: %v", err) + logger.Infof("❌ Execution failed: %v", err) } for at.isRunning { select { case <-ticker.C: if err := at.runCycle(); err != nil { - logger.Infof("❌ 执行失败: %v", err) + logger.Infof("❌ Execution failed: %v", err) } case <-at.stopMonitorCh: - logger.Infof("[%s] ⏹ 收到停止信号,退出自动交易主循环", at.name) + logger.Infof("[%s] ⏹ Stop signal received, exiting automatic trading main loop", at.name) return nil } } @@ -324,58 +324,58 @@ func (at *AutoTrader) Run() error { return nil } -// Stop 停止自动交易 +// Stop stops the automatic trading func (at *AutoTrader) Stop() { if !at.isRunning { return } at.isRunning = false - close(at.stopMonitorCh) // 通知监控goroutine停止 - at.monitorWg.Wait() // 等待监控goroutine结束 - logger.Info("⏹ 自动交易系统停止") + close(at.stopMonitorCh) // Notify monitoring goroutine to stop + at.monitorWg.Wait() // Wait for monitoring goroutine to finish + logger.Info("⏹ Automatic trading system stopped") } -// runCycle 运行一个交易周期(使用AI全权决策) +// runCycle runs one trading cycle (using AI full decision-making) func (at *AutoTrader) runCycle() error { at.callCount++ logger.Info("\n" + strings.Repeat("=", 70) + "\n") - logger.Infof("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount) + logger.Infof("⏰ %s - AI decision cycle #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount) logger.Info(strings.Repeat("=", 70)) - // 创建决策记录 + // Create decision record record := &store.DecisionRecord{ ExecutionLog: []string{}, Success: true, } - // 1. 检查是否需要停止交易 + // 1. Check if trading needs to be stopped if time.Now().Before(at.stopUntil) { remaining := at.stopUntil.Sub(time.Now()) - logger.Infof("⏸ 风险控制:暂停交易中,剩余 %.0f 分钟", remaining.Minutes()) + logger.Infof("⏸ Risk control: Trading paused, remaining %.0f minutes", remaining.Minutes()) record.Success = false - record.ErrorMessage = fmt.Sprintf("风险控制暂停中,剩余 %.0f 分钟", remaining.Minutes()) + record.ErrorMessage = fmt.Sprintf("Risk control paused, remaining %.0f minutes", remaining.Minutes()) at.saveDecision(record) return nil } - // 2. 重置日盈亏(每天重置) + // 2. Reset daily P&L (reset every day) if time.Since(at.lastResetTime) > 24*time.Hour { at.dailyPnL = 0 at.lastResetTime = time.Now() - logger.Info("📅 日盈亏已重置") + logger.Info("📅 Daily P&L reset") } - // 4. 收集交易上下文 + // 4. Collect trading context ctx, err := at.buildTradingContext() if err != nil { record.Success = false - record.ErrorMessage = fmt.Sprintf("构建交易上下文失败: %v", err) + record.ErrorMessage = fmt.Sprintf("Failed to build trading context: %v", err) at.saveDecision(record) - return fmt.Errorf("构建交易上下文失败: %w", err) + return fmt.Errorf("failed to build trading context: %w", err) } - // 独立保存净值快照(与 AI 决策解耦,用于绘制收益曲线) + // Save equity snapshot independently (decoupled from AI decision, used for drawing profit curve) at.saveEquitySnapshot(ctx) logger.Info(strings.Repeat("=", 70)) @@ -383,23 +383,23 @@ func (at *AutoTrader) runCycle() error { record.CandidateCoins = append(record.CandidateCoins, coin.Symbol) } - logger.Infof("📊 账户净值: %.2f USDT | 可用: %.2f USDT | 持仓: %d", + logger.Infof("📊 Account equity: %.2f USDT | Available: %.2f USDT | Positions: %d", ctx.Account.TotalEquity, ctx.Account.AvailableBalance, ctx.Account.PositionCount) - // 5. 使用策略引擎调用AI获取决策 - logger.Infof("🤖 正在请求AI分析并决策... [策略引擎]") + // 5. Use strategy engine to call AI for decision + logger.Infof("🤖 Requesting AI analysis and decision... [Strategy Engine]") aiDecision, err := decision.GetFullDecisionWithStrategy(ctx, at.mcpClient, at.strategyEngine, "balanced") if aiDecision != nil && aiDecision.AIRequestDurationMs > 0 { record.AIRequestDurationMs = aiDecision.AIRequestDurationMs - logger.Infof("⏱️ AI调用耗时: %.2f 秒", float64(record.AIRequestDurationMs)/1000) + logger.Infof("⏱️ AI call duration: %.2f seconds", float64(record.AIRequestDurationMs)/1000) record.ExecutionLog = append(record.ExecutionLog, - fmt.Sprintf("AI调用耗时: %d ms", record.AIRequestDurationMs)) + fmt.Sprintf("AI call duration: %d ms", record.AIRequestDurationMs)) } - // 即使有错误,也保存思维链、决策和输入prompt(用于debug) + // Save chain of thought, decisions, and input prompt even if there's an error (for debugging) if aiDecision != nil { - record.SystemPrompt = aiDecision.SystemPrompt // 保存系统提示词 + record.SystemPrompt = aiDecision.SystemPrompt // Save system prompt record.InputPrompt = aiDecision.UserPrompt record.CoTTrace = aiDecision.CoTTrace if len(aiDecision.Decisions) > 0 { @@ -410,19 +410,19 @@ func (at *AutoTrader) runCycle() error { if err != nil { record.Success = false - record.ErrorMessage = fmt.Sprintf("获取AI决策失败: %v", err) + record.ErrorMessage = fmt.Sprintf("Failed to get AI decision: %v", err) - // 打印系统提示词和AI思维链(即使有错误,也要输出以便调试) + // Print system prompt and AI chain of thought (output even with errors for debugging) if aiDecision != nil { logger.Info("\n" + strings.Repeat("=", 70) + "\n") - logger.Infof("📋 系统提示词 (错误情况)") + logger.Infof("📋 System prompt (error case)") logger.Info(strings.Repeat("=", 70)) logger.Info(aiDecision.SystemPrompt) logger.Info(strings.Repeat("=", 70)) if aiDecision.CoTTrace != "" { logger.Info("\n" + strings.Repeat("-", 70) + "\n") - logger.Info("💭 AI思维链分析(错误情况):") + logger.Info("💭 AI chain of thought analysis (error case):") logger.Info(strings.Repeat("-", 70)) logger.Info(aiDecision.CoTTrace) logger.Info(strings.Repeat("-", 70)) @@ -430,47 +430,47 @@ func (at *AutoTrader) runCycle() error { } at.saveDecision(record) - return fmt.Errorf("获取AI决策失败: %w", err) + return fmt.Errorf("failed to get AI decision: %w", err) } - // // 5. 打印系统提示词 + // // 5. Print system prompt // logger.Infof("\n" + strings.Repeat("=", 70)) - // logger.Infof("📋 系统提示词 [模板: %s]", at.systemPromptTemplate) + // logger.Infof("📋 System prompt [template: %s]", at.systemPromptTemplate) // logger.Info(strings.Repeat("=", 70)) // logger.Info(decision.SystemPrompt) // logger.Infof(strings.Repeat("=", 70) + "\n") - // 6. 打印AI思维链 + // 6. Print AI chain of thought // logger.Infof("\n" + strings.Repeat("-", 70)) - // logger.Info("💭 AI思维链分析:") + // logger.Info("💭 AI chain of thought analysis:") // logger.Info(strings.Repeat("-", 70)) // logger.Info(decision.CoTTrace) // logger.Infof(strings.Repeat("-", 70) + "\n") - // 7. 打印AI决策 - // logger.Infof("📋 AI决策列表 (%d 个):\n", len(decision.Decisions)) + // 7. Print AI decisions + // logger.Infof("📋 AI decision list (%d items):\n", len(decision.Decisions)) // for i, d := range decision.Decisions { // logger.Infof(" [%d] %s: %s - %s", i+1, d.Symbol, d.Action, d.Reasoning) // if d.Action == "open_long" || d.Action == "open_short" { - // logger.Infof(" 杠杆: %dx | 仓位: %.2f USDT | 止损: %.4f | 止盈: %.4f", + // logger.Infof(" Leverage: %dx | Position: %.2f USDT | Stop loss: %.4f | Take profit: %.4f", // d.Leverage, d.PositionSizeUSD, d.StopLoss, d.TakeProfit) // } // } logger.Info() logger.Info(strings.Repeat("-", 70)) - // 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) + // 8. Sort decisions: ensure close positions first, then open positions (prevent position stacking overflow) logger.Info(strings.Repeat("-", 70)) - // 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) + // 8. Sort decisions: ensure close positions first, then open positions (prevent position stacking overflow) sortedDecisions := sortDecisionsByPriority(aiDecision.Decisions) - logger.Info("🔄 执行顺序(已优化): 先平仓→后开仓") + logger.Info("🔄 Execution order (optimized): Close positions first → Open positions later") for i, d := range sortedDecisions { logger.Infof(" [%d] %s %s", i+1, d.Symbol, d.Action) } logger.Info() - // 执行决策并记录结果 + // Execute decisions and record results for _, d := range sortedDecisions { actionRecord := store.DecisionAction{ Action: d.Action, @@ -483,36 +483,36 @@ func (at *AutoTrader) runCycle() error { } if err := at.executeDecisionWithRecord(&d, &actionRecord); err != nil { - logger.Infof("❌ 执行决策失败 (%s %s): %v", d.Symbol, d.Action, err) + logger.Infof("❌ Failed to execute decision (%s %s): %v", d.Symbol, d.Action, err) actionRecord.Error = err.Error() - record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("❌ %s %s 失败: %v", d.Symbol, d.Action, err)) + record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("❌ %s %s failed: %v", d.Symbol, d.Action, err)) } else { actionRecord.Success = true - record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("✓ %s %s 成功", d.Symbol, d.Action)) - // 成功执行后短暂延迟 + record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("✓ %s %s succeeded", d.Symbol, d.Action)) + // Brief delay after successful execution time.Sleep(1 * time.Second) } record.Decisions = append(record.Decisions, actionRecord) } - // 9. 保存决策记录 + // 9. Save decision record if err := at.saveDecision(record); err != nil { - logger.Infof("⚠ 保存决策记录失败: %v", err) + logger.Infof("⚠ Failed to save decision record: %v", err) } return nil } -// buildTradingContext 构建交易上下文 +// buildTradingContext builds trading context func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { - // 1. 获取账户信息 + // 1. Get account information balance, err := at.trader.GetBalance() if err != nil { - return nil, fmt.Errorf("获取账户余额失败: %w", err) + return nil, fmt.Errorf("failed to get account balance: %w", err) } - // 获取账户字段 + // Get account fields totalWalletBalance := 0.0 totalUnrealizedProfit := 0.0 availableBalance := 0.0 @@ -527,19 +527,19 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { availableBalance = avail } - // Total Equity = 钱包余额 + 未实现盈亏 + // Total Equity = Wallet balance + Unrealized profit totalEquity := totalWalletBalance + totalUnrealizedProfit - // 2. 获取持仓信息 + // 2. Get position information positions, err := at.trader.GetPositions() if err != nil { - return nil, fmt.Errorf("获取持仓失败: %w", err) + return nil, fmt.Errorf("failed to get positions: %w", err) } var positionInfos []decision.PositionInfo totalMarginUsed := 0.0 - // 当前持仓的key集合(用于清理已平仓的记录) + // Current position key set (for cleaning up closed position records) currentPositionKeys := make(map[string]bool) for _, pos := range positions { @@ -549,10 +549,10 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { markPrice := pos["markPrice"].(float64) quantity := pos["positionAmt"].(float64) if quantity < 0 { - quantity = -quantity // 空仓数量为负,转为正数 + quantity = -quantity // Short position quantity is negative, convert to positive } - // 跳过已平仓的持仓(quantity = 0),防止"幽灵持仓"传递给AI + // Skip closed positions (quantity = 0), prevent "ghost positions" from being passed to AI if quantity == 0 { continue } @@ -560,27 +560,27 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { unrealizedPnl := pos["unRealizedProfit"].(float64) liquidationPrice := pos["liquidationPrice"].(float64) - // 计算占用保证金(估算) - leverage := 10 // 默认值,实际应该从持仓信息获取 + // Calculate margin used (estimated) + leverage := 10 // Default value, should actually be fetched from position info if lev, ok := pos["leverage"].(float64); ok { leverage = int(lev) } marginUsed := (quantity * markPrice) / float64(leverage) totalMarginUsed += marginUsed - // 计算盈亏百分比(基于保证金,考虑杠杆) + // Calculate P&L percentage (based on margin, considering leverage) pnlPct := calculatePnLPercentage(unrealizedPnl, marginUsed) - // 跟踪持仓首次出现时间 + // Track position first seen time posKey := symbol + "_" + side currentPositionKeys[posKey] = true if _, exists := at.positionFirstSeenTime[posKey]; !exists { - // 新持仓,记录当前时间 + // New position, record current time at.positionFirstSeenTime[posKey] = time.Now().UnixMilli() } updateTime := at.positionFirstSeenTime[posKey] - // 获取该持仓的历史最高收益率 + // Get peak profit rate for this position at.peakPnLCacheMutex.RLock() peakPnlPct := at.peakPnLCache[posKey] at.peakPnLCacheMutex.RUnlock() @@ -601,24 +601,24 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { }) } - // 清理已平仓的持仓记录 + // Clean up closed position records for key := range at.positionFirstSeenTime { if !currentPositionKeys[key] { delete(at.positionFirstSeenTime, key) } } - // 3. 使用策略引擎获取候选币种(必须有策略引擎) + // 3. Use strategy engine to get candidate coins (must have strategy engine) if at.strategyEngine == nil { - return nil, fmt.Errorf("交易员未配置策略引擎") + return nil, fmt.Errorf("trader has no strategy engine configured") } candidateCoins, err := at.strategyEngine.GetCandidateCoins() if err != nil { - return nil, fmt.Errorf("获取候选币种失败: %w", err) + return nil, fmt.Errorf("failed to get candidate coins: %w", err) } - logger.Infof("📋 [%s] 策略引擎获取候选币种: %d个", at.name, len(candidateCoins)) + logger.Infof("📋 [%s] Strategy engine fetched candidate coins: %d", at.name, len(candidateCoins)) - // 4. 计算总盈亏 + // 4. Calculate total P&L totalPnL := totalEquity - at.initialBalance totalPnLPct := 0.0 if at.initialBalance > 0 { @@ -630,13 +630,13 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { marginUsedPct = (totalMarginUsed / totalEquity) * 100 } - // 5. 从策略配置获取杠杆 + // 5. Get leverage from strategy config strategyConfig := at.strategyEngine.GetConfig() btcEthLeverage := strategyConfig.RiskControl.BTCETHMaxLeverage altcoinLeverage := strategyConfig.RiskControl.AltcoinMaxLeverage - logger.Infof("📋 [%s] 策略杠杆配置: BTC/ETH=%dx, 山寨币=%dx", at.name, btcEthLeverage, altcoinLeverage) + logger.Infof("📋 [%s] Strategy leverage config: BTC/ETH=%dx, Altcoin=%dx", at.name, btcEthLeverage, altcoinLeverage) - // 6. 构建上下文 + // 6. Build context ctx := &decision.Context{ CurrentTime: time.Now().Format("2006-01-02 15:04:05"), RuntimeMinutes: int(time.Since(at.startTime).Minutes()), @@ -657,9 +657,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { CandidateCoins: candidateCoins, } - // 7. 添加交易统计和历史订单(如果store可用) + // 7. Add trading statistics and historical orders (if store is available) if at.store != nil { - // 获取交易统计(使用新的 positions 表) + // Get trading statistics (using new positions table) if stats, err := at.store.Position().GetFullStats(at.id); err == nil { ctx.TradingStats = &decision.TradingStats{ TotalTrades: stats.TotalTrades, @@ -673,7 +673,7 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { } } - // 获取最近10条已平仓交易(使用新的 positions 表) + // Get recent 10 closed trades (using new positions table) if recentTrades, err := at.store.Position().GetRecentTrades(at.id, 10); err == nil { for _, trade := range recentTrades { ctx.RecentOrders = append(ctx.RecentOrders, decision.RecentOrder{ @@ -689,9 +689,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { } } - // 8. 获取量化数据(如果策略配置启用) + // 8. Get quantitative data (if enabled in strategy config) if strategyConfig.Indicators.EnableQuantData && strategyConfig.Indicators.QuantDataAPIURL != "" { - // 收集需要查询的币种(候选币种 + 持仓币种) + // Collect symbols to query (candidate coins + position coins) symbolsToQuery := make(map[string]bool) for _, coin := range candidateCoins { symbolsToQuery[coin.Symbol] = true @@ -705,15 +705,15 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { symbols = append(symbols, sym) } - logger.Infof("📊 [%s] 正在获取 %d 个币种的量化数据...", at.name, len(symbols)) + logger.Infof("📊 [%s] Fetching quantitative data for %d symbols...", at.name, len(symbols)) ctx.QuantDataMap = at.strategyEngine.FetchQuantDataBatch(symbols) - logger.Infof("📊 [%s] 成功获取 %d 个币种的量化数据", at.name, len(ctx.QuantDataMap)) + logger.Infof("📊 [%s] Successfully fetched quantitative data for %d symbols", at.name, len(ctx.QuantDataMap)) } return ctx, nil } -// executeDecisionWithRecord 执行AI决策并记录详细信息 +// executeDecisionWithRecord executes AI decision and records detailed information func (at *AutoTrader) executeDecisionWithRecord(decision *decision.Decision, actionRecord *store.DecisionAction) error { switch decision.Action { case "open_long": @@ -725,191 +725,191 @@ func (at *AutoTrader) executeDecisionWithRecord(decision *decision.Decision, act case "close_short": return at.executeCloseShortWithRecord(decision, actionRecord) case "hold", "wait": - // 无需执行,仅记录 + // No execution needed, just record return nil default: - return fmt.Errorf("未知的action: %s", decision.Action) + return fmt.Errorf("unknown action: %s", decision.Action) } } -// executeOpenLongWithRecord 执行开多仓并记录详细信息 +// executeOpenLongWithRecord executes open long position and records detailed information func (at *AutoTrader) executeOpenLongWithRecord(decision *decision.Decision, actionRecord *store.DecisionAction) error { - logger.Infof(" 📈 开多仓: %s", decision.Symbol) + logger.Infof(" 📈 Open long: %s", decision.Symbol) - // ⚠️ 关键:检查是否已有同币种同方向持仓,如果有则拒绝开仓(防止仓位叠加超限) + // ⚠️ Critical: Check if there's already a position in the same symbol and direction, reject if exists (prevent position stacking overflow) positions, err := at.trader.GetPositions() if err == nil { for _, pos := range positions { if pos["symbol"] == decision.Symbol && pos["side"] == "long" { - return fmt.Errorf("❌ %s 已有多仓,拒绝开仓以防止仓位叠加超限。如需换仓,请先给出 close_long 决策", decision.Symbol) + return fmt.Errorf("❌ %s already has long position, rejecting to prevent position stacking overflow. If changing position, please give close_long decision first", decision.Symbol) } } } - // 获取当前价格 + // Get current price marketData, err := market.Get(decision.Symbol) if err != nil { return err } - // 计算数量 + // Calculate quantity quantity := decision.PositionSizeUSD / marketData.CurrentPrice actionRecord.Quantity = quantity actionRecord.Price = marketData.CurrentPrice - // ⚠️ 保证金验证:防止保证金不足错误(code=-2019) + // ⚠️ Margin validation: prevent insufficient margin error (code=-2019) requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage) balance, err := at.trader.GetBalance() if err != nil { - return fmt.Errorf("获取账户余额失败: %w", err) + return fmt.Errorf("failed to get account balance: %w", err) } availableBalance := 0.0 if avail, ok := balance["availableBalance"].(float64); ok { availableBalance = avail } - // 手续费估算(Taker费率 0.04%) + // Fee estimation (Taker fee rate 0.04%) estimatedFee := decision.PositionSizeUSD * 0.0004 totalRequired := requiredMargin + estimatedFee if totalRequired > availableBalance { - return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT(保证金 %.2f + 手续费 %.2f),可用 %.2f USDT", + return fmt.Errorf("❌ Insufficient margin: required %.2f USDT (margin %.2f + fee %.2f), available %.2f USDT", totalRequired, requiredMargin, estimatedFee, availableBalance) } - // 设置仓位模式 + // Set margin mode if err := at.trader.SetMarginMode(decision.Symbol, at.config.IsCrossMargin); err != nil { - logger.Infof(" ⚠️ 设置仓位模式失败: %v", err) - // 继续执行,不影响交易 + logger.Infof(" ⚠️ Failed to set margin mode: %v", err) + // Continue execution, doesn't affect trading } - // 开仓 + // Open position order, err := at.trader.OpenLong(decision.Symbol, quantity, decision.Leverage) if err != nil { return err } - // 记录订单ID + // Record order ID if orderID, ok := order["orderId"].(int64); ok { actionRecord.OrderID = orderID } - logger.Infof(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity) + logger.Infof(" ✓ Position opened successfully, order ID: %v, quantity: %.4f", order["orderId"], quantity) - // 记录订单到数据库并轮询确认 + // Record order to database and poll for confirmation at.recordAndConfirmOrder(order, decision.Symbol, "open_long", quantity, marketData.CurrentPrice, decision.Leverage, 0) - // 记录开仓时间 + // Record position opening time posKey := decision.Symbol + "_long" at.positionFirstSeenTime[posKey] = time.Now().UnixMilli() - // 设置止损止盈 + // Set stop loss and take profit if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil { - logger.Infof(" ⚠ 设置止损失败: %v", err) + logger.Infof(" ⚠ Failed to set stop loss: %v", err) } if err := at.trader.SetTakeProfit(decision.Symbol, "LONG", quantity, decision.TakeProfit); err != nil { - logger.Infof(" ⚠ 设置止盈失败: %v", err) + logger.Infof(" ⚠ Failed to set take profit: %v", err) } return nil } -// executeOpenShortWithRecord 执行开空仓并记录详细信息 +// executeOpenShortWithRecord executes open short position and records detailed information func (at *AutoTrader) executeOpenShortWithRecord(decision *decision.Decision, actionRecord *store.DecisionAction) error { - logger.Infof(" 📉 开空仓: %s", decision.Symbol) + logger.Infof(" 📉 Open short: %s", decision.Symbol) - // ⚠️ 关键:检查是否已有同币种同方向持仓,如果有则拒绝开仓(防止仓位叠加超限) + // ⚠️ Critical: Check if there's already a position in the same symbol and direction, reject if exists (prevent position stacking overflow) positions, err := at.trader.GetPositions() if err == nil { for _, pos := range positions { if pos["symbol"] == decision.Symbol && pos["side"] == "short" { - return fmt.Errorf("❌ %s 已有空仓,拒绝开仓以防止仓位叠加超限。如需换仓,请先给出 close_short 决策", decision.Symbol) + return fmt.Errorf("❌ %s already has short position, rejecting to prevent position stacking overflow. If changing position, please give close_short decision first", decision.Symbol) } } } - // 获取当前价格 + // Get current price marketData, err := market.Get(decision.Symbol) if err != nil { return err } - // 计算数量 + // Calculate quantity quantity := decision.PositionSizeUSD / marketData.CurrentPrice actionRecord.Quantity = quantity actionRecord.Price = marketData.CurrentPrice - // ⚠️ 保证金验证:防止保证金不足错误(code=-2019) + // ⚠️ Margin validation: prevent insufficient margin error (code=-2019) requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage) balance, err := at.trader.GetBalance() if err != nil { - return fmt.Errorf("获取账户余额失败: %w", err) + return fmt.Errorf("failed to get account balance: %w", err) } availableBalance := 0.0 if avail, ok := balance["availableBalance"].(float64); ok { availableBalance = avail } - // 手续费估算(Taker费率 0.04%) + // Fee estimation (Taker fee rate 0.04%) estimatedFee := decision.PositionSizeUSD * 0.0004 totalRequired := requiredMargin + estimatedFee if totalRequired > availableBalance { - return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT(保证金 %.2f + 手续费 %.2f),可用 %.2f USDT", + return fmt.Errorf("❌ Insufficient margin: required %.2f USDT (margin %.2f + fee %.2f), available %.2f USDT", totalRequired, requiredMargin, estimatedFee, availableBalance) } - // 设置仓位模式 + // Set margin mode if err := at.trader.SetMarginMode(decision.Symbol, at.config.IsCrossMargin); err != nil { - logger.Infof(" ⚠️ 设置仓位模式失败: %v", err) - // 继续执行,不影响交易 + logger.Infof(" ⚠️ Failed to set margin mode: %v", err) + // Continue execution, doesn't affect trading } - // 开仓 + // Open position order, err := at.trader.OpenShort(decision.Symbol, quantity, decision.Leverage) if err != nil { return err } - // 记录订单ID + // Record order ID if orderID, ok := order["orderId"].(int64); ok { actionRecord.OrderID = orderID } - logger.Infof(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity) + logger.Infof(" ✓ Position opened successfully, order ID: %v, quantity: %.4f", order["orderId"], quantity) - // 记录订单到数据库并轮询确认 + // Record order to database and poll for confirmation at.recordAndConfirmOrder(order, decision.Symbol, "open_short", quantity, marketData.CurrentPrice, decision.Leverage, 0) - // 记录开仓时间 + // Record position opening time posKey := decision.Symbol + "_short" at.positionFirstSeenTime[posKey] = time.Now().UnixMilli() - // 设置止损止盈 + // Set stop loss and take profit if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil { - logger.Infof(" ⚠ 设置止损失败: %v", err) + logger.Infof(" ⚠ Failed to set stop loss: %v", err) } if err := at.trader.SetTakeProfit(decision.Symbol, "SHORT", quantity, decision.TakeProfit); err != nil { - logger.Infof(" ⚠ 设置止盈失败: %v", err) + logger.Infof(" ⚠ Failed to set take profit: %v", err) } return nil } -// executeCloseLongWithRecord 执行平多仓并记录详细信息 +// executeCloseLongWithRecord executes close long position and records detailed information func (at *AutoTrader) executeCloseLongWithRecord(decision *decision.Decision, actionRecord *store.DecisionAction) error { - logger.Infof(" 🔄 平多仓: %s", decision.Symbol) + logger.Infof(" 🔄 Close long: %s", decision.Symbol) - // 获取当前价格 + // Get current price marketData, err := market.Get(decision.Symbol) if err != nil { return err } actionRecord.Price = marketData.CurrentPrice - // 获取开仓价格(用于计算盈亏) + // Get entry price (for P&L calculation) var entryPrice float64 var quantity float64 if at.store != nil { @@ -919,36 +919,36 @@ func (at *AutoTrader) executeCloseLongWithRecord(decision *decision.Decision, ac } } - // 平仓 - order, err := at.trader.CloseLong(decision.Symbol, 0) // 0 = 全部平仓 + // Close position + order, err := at.trader.CloseLong(decision.Symbol, 0) // 0 = close all if err != nil { return err } - // 记录订单ID + // Record order ID if orderID, ok := order["orderId"].(int64); ok { actionRecord.OrderID = orderID } - // 记录订单到数据库并轮询确认 + // Record order to database and poll for confirmation at.recordAndConfirmOrder(order, decision.Symbol, "close_long", quantity, marketData.CurrentPrice, 0, entryPrice) - logger.Infof(" ✓ 平仓成功") + logger.Infof(" ✓ Position closed successfully") return nil } -// executeCloseShortWithRecord 执行平空仓并记录详细信息 +// executeCloseShortWithRecord executes close short position and records detailed information func (at *AutoTrader) executeCloseShortWithRecord(decision *decision.Decision, actionRecord *store.DecisionAction) error { - logger.Infof(" 🔄 平空仓: %s", decision.Symbol) + logger.Infof(" 🔄 Close short: %s", decision.Symbol) - // 获取当前价格 + // Get current price marketData, err := market.Get(decision.Symbol) if err != nil { return err } actionRecord.Price = marketData.CurrentPrice - // 获取开仓价格(用于计算盈亏) + // Get entry price (for P&L calculation) var entryPrice float64 var quantity float64 if at.store != nil { @@ -958,55 +958,55 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *decision.Decision, a } } - // 平仓 - order, err := at.trader.CloseShort(decision.Symbol, 0) // 0 = 全部平仓 + // Close position + order, err := at.trader.CloseShort(decision.Symbol, 0) // 0 = close all if err != nil { return err } - // 记录订单ID + // Record order ID if orderID, ok := order["orderId"].(int64); ok { actionRecord.OrderID = orderID } - // 记录订单到数据库并轮询确认 + // Record order to database and poll for confirmation at.recordAndConfirmOrder(order, decision.Symbol, "close_short", quantity, marketData.CurrentPrice, 0, entryPrice) - logger.Infof(" ✓ 平仓成功") + logger.Infof(" ✓ Position closed successfully") return nil } -// GetID 获取trader ID +// GetID gets trader ID func (at *AutoTrader) GetID() string { return at.id } -// GetName 获取trader名称 +// GetName gets trader name func (at *AutoTrader) GetName() string { return at.name } -// GetAIModel 获取AI模型 +// GetAIModel gets AI model func (at *AutoTrader) GetAIModel() string { return at.aiModel } -// GetExchange 获取交易所 +// GetExchange gets exchange func (at *AutoTrader) GetExchange() string { return at.exchange } -// SetCustomPrompt 设置自定义交易策略prompt +// SetCustomPrompt sets custom trading strategy prompt func (at *AutoTrader) SetCustomPrompt(prompt string) { at.customPrompt = prompt } -// SetOverrideBasePrompt 设置是否覆盖基础prompt +// SetOverrideBasePrompt sets whether to override base prompt func (at *AutoTrader) SetOverrideBasePrompt(override bool) { at.overrideBasePrompt = override } -// GetSystemPromptTemplate 获取当前系统提示词模板名称(从策略配置获取) +// GetSystemPromptTemplate gets current system prompt template name (from strategy config) func (at *AutoTrader) GetSystemPromptTemplate() string { if at.strategyEngine != nil { config := at.strategyEngine.GetConfig() @@ -1017,7 +1017,7 @@ func (at *AutoTrader) GetSystemPromptTemplate() string { return "strategy" } -// saveEquitySnapshot 独立保存净值快照(用于绘制收益曲线,与 AI 决策解耦) +// saveEquitySnapshot saves equity snapshot independently (for drawing profit curve, decoupled from AI decision) func (at *AutoTrader) saveEquitySnapshot(ctx *decision.Context) { if at.store == nil || ctx == nil { return @@ -1034,11 +1034,11 @@ func (at *AutoTrader) saveEquitySnapshot(ctx *decision.Context) { } if err := at.store.Equity().Save(snapshot); err != nil { - logger.Infof("⚠️ 保存净值快照失败: %v", err) + logger.Infof("⚠️ Failed to save equity snapshot: %v", err) } } -// saveDecision 保存 AI 决策日志到数据库(仅记录 AI 输入输出,用于调试) +// saveDecision saves AI decision log to database (only records AI input/output, for debugging) func (at *AutoTrader) saveDecision(record *store.DecisionRecord) error { if at.store == nil { return nil @@ -1053,20 +1053,20 @@ func (at *AutoTrader) saveDecision(record *store.DecisionRecord) error { } if err := at.store.Decision().LogDecision(record); err != nil { - logger.Infof("⚠️ 保存决策记录失败: %v", err) + logger.Infof("⚠️ Failed to save decision record: %v", err) return err } - logger.Infof("📝 决策记录已保存: trader=%s, cycle=%d", at.id, at.cycleNumber) + logger.Infof("📝 Decision record saved: trader=%s, cycle=%d", at.id, at.cycleNumber) return nil } -// GetStore 获取数据存储(用于外部访问决策记录等) +// GetStore gets data store (for external access to decision records, etc.) func (at *AutoTrader) GetStore() *store.Store { return at.store } -// GetStatus 获取系统状态(用于API) +// GetStatus gets system status (for API) func (at *AutoTrader) GetStatus() map[string]interface{} { aiProvider := "DeepSeek" if at.config.UseQwen { @@ -1090,14 +1090,14 @@ func (at *AutoTrader) GetStatus() map[string]interface{} { } } -// GetAccountInfo 获取账户信息(用于API) +// GetAccountInfo gets account information (for API) func (at *AutoTrader) GetAccountInfo() (map[string]interface{}, error) { balance, err := at.trader.GetBalance() if err != nil { - return nil, fmt.Errorf("获取余额失败: %w", err) + return nil, fmt.Errorf("failed to get balance: %w", err) } - // 获取账户字段 + // Get account fields totalWalletBalance := 0.0 totalUnrealizedProfit := 0.0 availableBalance := 0.0 @@ -1112,13 +1112,13 @@ func (at *AutoTrader) GetAccountInfo() (map[string]interface{}, error) { availableBalance = avail } - // Total Equity = 钱包余额 + 未实现盈亏 + // Total Equity = Wallet balance + Unrealized profit totalEquity := totalWalletBalance + totalUnrealizedProfit - // 获取持仓计算总保证金 + // Get positions to calculate total margin positions, err := at.trader.GetPositions() if err != nil { - return nil, fmt.Errorf("获取持仓失败: %w", err) + return nil, fmt.Errorf("failed to get positions: %w", err) } totalMarginUsed := 0.0 @@ -1140,10 +1140,10 @@ func (at *AutoTrader) GetAccountInfo() (map[string]interface{}, error) { totalMarginUsed += marginUsed } - // 验证未实现盈亏的一致性(API值 vs 从持仓计算) + // Verify unrealized P&L consistency (API value vs calculated from positions) diff := math.Abs(totalUnrealizedProfit - totalUnrealizedPnLCalculated) - if diff > 0.1 { // 允许0.01 USDT的误差 - logger.Infof("⚠️ 未实现盈亏不一致: API=%.4f, 计算=%.4f, 差异=%.4f", + if diff > 0.1 { // Allow 0.01 USDT error margin + logger.Infof("⚠️ Unrealized P&L inconsistency: API=%.4f, Calculated=%.4f, Diff=%.4f", totalUnrealizedProfit, totalUnrealizedPnLCalculated, diff) } @@ -1152,7 +1152,7 @@ func (at *AutoTrader) GetAccountInfo() (map[string]interface{}, error) { if at.initialBalance > 0 { totalPnLPct = (totalPnL / at.initialBalance) * 100 } else { - logger.Infof("⚠️ Initial Balance异常: %.2f,无法计算PNL百分比", at.initialBalance) + logger.Infof("⚠️ Initial Balance abnormal: %.2f, cannot calculate P&L percentage", at.initialBalance) } marginUsedPct := 0.0 @@ -1161,30 +1161,30 @@ func (at *AutoTrader) GetAccountInfo() (map[string]interface{}, error) { } return map[string]interface{}{ - // 核心字段 - "total_equity": totalEquity, // 账户净值 = wallet + unrealized - "wallet_balance": totalWalletBalance, // 钱包余额(不含未实现盈亏) - "unrealized_profit": totalUnrealizedProfit, // 未实现盈亏(交易所API官方值) - "available_balance": availableBalance, // 可用余额 + // Core fields + "total_equity": totalEquity, // Account equity = wallet + unrealized + "wallet_balance": totalWalletBalance, // Wallet balance (excluding unrealized P&L) + "unrealized_profit": totalUnrealizedProfit, // Unrealized P&L (official value from exchange API) + "available_balance": availableBalance, // Available balance - // 盈亏统计 - "total_pnl": totalPnL, // 总盈亏 = equity - initial - "total_pnl_pct": totalPnLPct, // 总盈亏百分比 - "initial_balance": at.initialBalance, // 初始余额 - "daily_pnl": at.dailyPnL, // 日盈亏 + // P&L statistics + "total_pnl": totalPnL, // Total P&L = equity - initial + "total_pnl_pct": totalPnLPct, // Total P&L percentage + "initial_balance": at.initialBalance, // Initial balance + "daily_pnl": at.dailyPnL, // Daily P&L - // 持仓信息 - "position_count": len(positions), // 持仓数量 - "margin_used": totalMarginUsed, // 保证金占用 - "margin_used_pct": marginUsedPct, // 保证金使用率 + // Position information + "position_count": len(positions), // Position count + "margin_used": totalMarginUsed, // Margin used + "margin_used_pct": marginUsedPct, // Margin usage rate }, nil } -// GetPositions 获取持仓列表(用于API) +// GetPositions gets position list (for API) func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) { positions, err := at.trader.GetPositions() if err != nil { - return nil, fmt.Errorf("获取持仓失败: %w", err) + return nil, fmt.Errorf("failed to get positions: %w", err) } var result []map[string]interface{} @@ -1205,10 +1205,10 @@ func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) { leverage = int(lev) } - // 计算占用保证金 + // Calculate margin used marginUsed := (quantity * markPrice) / float64(leverage) - // 计算盈亏百分比(基于保证金) + // Calculate P&L percentage (based on margin) pnlPct := calculatePnLPercentage(unrealizedPnl, marginUsed) result = append(result, map[string]interface{}{ @@ -1228,8 +1228,8 @@ func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) { return result, nil } -// calculatePnLPercentage 计算盈亏百分比(基于保证金,自动考虑杠杆) -// 收益率 = 未实现盈亏 / 保证金 × 100% +// calculatePnLPercentage calculates P&L percentage (based on margin, automatically considers leverage) +// Return rate = Unrealized P&L / Margin × 100% func calculatePnLPercentage(unrealizedPnl, marginUsed float64) float64 { if marginUsed > 0 { return (unrealizedPnl / marginUsed) * 100 @@ -1237,32 +1237,32 @@ func calculatePnLPercentage(unrealizedPnl, marginUsed float64) float64 { return 0.0 } -// sortDecisionsByPriority 对决策排序:先平仓,再开仓,最后hold/wait -// 这样可以避免换仓时仓位叠加超限 +// sortDecisionsByPriority sorts decisions: close positions first, then open positions, finally hold/wait +// This avoids position stacking overflow when changing positions func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision { if len(decisions) <= 1 { return decisions } - // 定义优先级 + // Define priority getActionPriority := func(action string) int { switch action { case "close_long", "close_short": - return 1 // 最高优先级:先平仓 + return 1 // Highest priority: close positions first case "open_long", "open_short": - return 2 // 次优先级:后开仓 + return 2 // Second priority: open positions later case "hold", "wait": - return 3 // 最低优先级:观望 + return 3 // Lowest priority: wait default: - return 999 // 未知动作放最后 + return 999 // Unknown actions at the end } } - // 复制决策列表 + // Copy decision list sorted := make([]decision.Decision, len(decisions)) copy(sorted, decisions) - // 按优先级排序 + // Sort by priority for i := 0; i < len(sorted)-1; i++ { for j := i + 1; j < len(sorted); j++ { if getActionPriority(sorted[i].Action) > getActionPriority(sorted[j].Action) { @@ -1274,35 +1274,35 @@ func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision return sorted } -// 启动回撤监控 +// startDrawdownMonitor starts drawdown monitoring func (at *AutoTrader) startDrawdownMonitor() { at.monitorWg.Add(1) go func() { defer at.monitorWg.Done() - ticker := time.NewTicker(1 * time.Minute) // 每分钟检查一次 + ticker := time.NewTicker(1 * time.Minute) // Check every minute defer ticker.Stop() - logger.Info("📊 启动持仓回撤监控(每分钟检查一次)") + logger.Info("📊 Started position drawdown monitoring (check every minute)") for { select { case <-ticker.C: at.checkPositionDrawdown() case <-at.stopMonitorCh: - logger.Info("⏹ 停止持仓回撤监控") + logger.Info("⏹ Stopped position drawdown monitoring") return } } }() } -// 检查持仓回撤情况 +// checkPositionDrawdown checks position drawdown situation func (at *AutoTrader) checkPositionDrawdown() { - // 获取当前持仓 + // Get current positions positions, err := at.trader.GetPositions() if err != nil { - logger.Infof("❌ 回撤监控:获取持仓失败: %v", err) + logger.Infof("❌ Drawdown monitoring: failed to get positions: %v", err) return } @@ -1313,11 +1313,11 @@ func (at *AutoTrader) checkPositionDrawdown() { markPrice := pos["markPrice"].(float64) quantity := pos["positionAmt"].(float64) if quantity < 0 { - quantity = -quantity // 空仓数量为负,转为正数 + quantity = -quantity // Short position quantity is negative, convert to positive } - // 计算当前盈亏百分比 - leverage := 10 // 默认值 + // Calculate current P&L percentage + leverage := 10 // Default value if lev, ok := pos["leverage"].(float64); ok { leverage = int(lev) } @@ -1329,78 +1329,78 @@ func (at *AutoTrader) checkPositionDrawdown() { currentPnLPct = ((entryPrice - markPrice) / entryPrice) * float64(leverage) * 100 } - // 构造持仓唯一标识(区分多空) + // Construct unique position identifier (distinguish long/short) posKey := symbol + "_" + side - // 获取该持仓的历史最高收益 + // Get historical peak profit for this position at.peakPnLCacheMutex.RLock() peakPnLPct, exists := at.peakPnLCache[posKey] at.peakPnLCacheMutex.RUnlock() if !exists { - // 如果没有历史最高记录,使用当前盈亏作为初始值 + // If no historical peak record, use current P&L as initial value peakPnLPct = currentPnLPct at.UpdatePeakPnL(symbol, side, currentPnLPct) } else { - // 更新峰值缓存 + // Update peak cache at.UpdatePeakPnL(symbol, side, currentPnLPct) } - // 计算回撤(从最高点下跌的幅度) + // Calculate drawdown (magnitude of decline from peak) var drawdownPct float64 if peakPnLPct > 0 && currentPnLPct < peakPnLPct { drawdownPct = ((peakPnLPct - currentPnLPct) / peakPnLPct) * 100 } - // 检查平仓条件:收益大于5%且回撤超过40% + // Check close position condition: profit > 5% and drawdown >= 40% if currentPnLPct > 5.0 && drawdownPct >= 40.0 { - logger.Infof("🚨 触发回撤平仓条件: %s %s | 当前收益: %.2f%% | 最高收益: %.2f%% | 回撤: %.2f%%", + logger.Infof("🚨 Drawdown close position condition triggered: %s %s | Current profit: %.2f%% | Peak profit: %.2f%% | Drawdown: %.2f%%", symbol, side, currentPnLPct, peakPnLPct, drawdownPct) - // 执行平仓 + // Execute close position if err := at.emergencyClosePosition(symbol, side); err != nil { - logger.Infof("❌ 回撤平仓失败 (%s %s): %v", symbol, side, err) + logger.Infof("❌ Drawdown close position failed (%s %s): %v", symbol, side, err) } else { - logger.Infof("✅ 回撤平仓成功: %s %s", symbol, side) - // 平仓后清理该持仓的缓存 + logger.Infof("✅ Drawdown close position succeeded: %s %s", symbol, side) + // Clear cache for this position after closing at.ClearPeakPnLCache(symbol, side) } } else if currentPnLPct > 5.0 { - // 记录接近平仓条件的情况(用于调试) - logger.Infof("📊 回撤监控: %s %s | 收益: %.2f%% | 最高: %.2f%% | 回撤: %.2f%%", + // Record situations close to close position condition (for debugging) + logger.Infof("📊 Drawdown monitoring: %s %s | Profit: %.2f%% | Peak: %.2f%% | Drawdown: %.2f%%", symbol, side, currentPnLPct, peakPnLPct, drawdownPct) } } } -// 紧急平仓函数 +// emergencyClosePosition emergency close position function func (at *AutoTrader) emergencyClosePosition(symbol, side string) error { switch side { case "long": - order, err := at.trader.CloseLong(symbol, 0) // 0 = 全部平仓 + order, err := at.trader.CloseLong(symbol, 0) // 0 = close all if err != nil { return err } - logger.Infof("✅ 紧急平多仓成功,订单ID: %v", order["orderId"]) + logger.Infof("✅ Emergency close long position succeeded, order ID: %v", order["orderId"]) case "short": - order, err := at.trader.CloseShort(symbol, 0) // 0 = 全部平仓 + order, err := at.trader.CloseShort(symbol, 0) // 0 = close all if err != nil { return err } - logger.Infof("✅ 紧急平空仓成功,订单ID: %v", order["orderId"]) + logger.Infof("✅ Emergency close short position succeeded, order ID: %v", order["orderId"]) default: - return fmt.Errorf("未知的持仓方向: %s", side) + return fmt.Errorf("unknown position direction: %s", side) } return nil } -// GetPeakPnLCache 获取最高收益缓存 +// GetPeakPnLCache gets peak profit cache func (at *AutoTrader) GetPeakPnLCache() map[string]float64 { at.peakPnLCacheMutex.RLock() defer at.peakPnLCacheMutex.RUnlock() - // 返回缓存的副本 + // Return a copy of the cache cache := make(map[string]float64) for k, v := range at.peakPnLCache { cache[k] = v @@ -1408,24 +1408,24 @@ func (at *AutoTrader) GetPeakPnLCache() map[string]float64 { return cache } -// UpdatePeakPnL 更新最高收益缓存 +// UpdatePeakPnL updates peak profit cache func (at *AutoTrader) UpdatePeakPnL(symbol, side string, currentPnLPct float64) { at.peakPnLCacheMutex.Lock() defer at.peakPnLCacheMutex.Unlock() posKey := symbol + "_" + side if peak, exists := at.peakPnLCache[posKey]; exists { - // 更新峰值(如果是多头,取较大值;如果是空头,currentPnLPct为负,也要比较) + // Update peak (if long, take larger value; if short, currentPnLPct is negative, also compare) if currentPnLPct > peak { at.peakPnLCache[posKey] = currentPnLPct } } else { - // 首次记录 + // First time recording at.peakPnLCache[posKey] = currentPnLPct } } -// ClearPeakPnLCache 清除指定持仓的峰值缓存 +// ClearPeakPnLCache clears peak cache for specified position func (at *AutoTrader) ClearPeakPnLCache(symbol, side string) { at.peakPnLCacheMutex.Lock() defer at.peakPnLCacheMutex.Unlock() @@ -1434,15 +1434,15 @@ func (at *AutoTrader) ClearPeakPnLCache(symbol, side string) { delete(at.peakPnLCache, posKey) } -// recordAndConfirmOrder 记录订单并轮询确认状态 +// recordAndConfirmOrder records order and polls for confirmation status // action: open_long, open_short, close_long, close_short -// entryPrice: 平仓时的开仓价(开仓时为0) +// entryPrice: entry price when closing (0 when opening) func (at *AutoTrader) recordAndConfirmOrder(orderResult map[string]interface{}, symbol, action string, quantity float64, price float64, leverage int, entryPrice float64) { if at.store == nil { return } - // 获取订单ID(支持多种类型) + // Get order ID (supports multiple types) var orderID string switch v := orderResult["orderId"].(type) { case int64: @@ -1456,11 +1456,11 @@ func (at *AutoTrader) recordAndConfirmOrder(orderResult map[string]interface{}, } if orderID == "" || orderID == "0" { - logger.Infof(" ⚠️ 订单ID为空,跳过记录") + logger.Infof(" ⚠️ Order ID is empty, skipping record") return } - // 确定 side 和 positionSide + // Determine side and positionSide var side, positionSide string switch action { case "open_long": @@ -1477,7 +1477,7 @@ func (at *AutoTrader) recordAndConfirmOrder(orderResult map[string]interface{}, positionSide = "SHORT" } - // 创建订单记录 + // Create order record order := &store.TraderOrder{ TraderID: at.id, OrderID: orderID, @@ -1493,19 +1493,19 @@ func (at *AutoTrader) recordAndConfirmOrder(orderResult map[string]interface{}, EntryPrice: entryPrice, } - // 保存到数据库 + // Save to database if err := at.store.Order().Create(order); err != nil { - logger.Infof(" ⚠️ 记录订单失败: %v", err) + logger.Infof(" ⚠️ Failed to record order: %v", err) return } - logger.Infof(" 📝 订单已记录 (ID: %s, action: %s)", orderID, action) + logger.Infof(" 📝 Order recorded (ID: %s, action: %s)", orderID, action) - // 记录仓位变化 + // Record position change at.recordPositionChange(orderID, symbol, positionSide, action, quantity, price, leverage, entryPrice) } -// recordPositionChange 记录仓位变化(开仓创建记录,平仓更新记录) +// recordPositionChange records position change (create record on open, update record on close) func (at *AutoTrader) recordPositionChange(orderID, symbol, side, action string, quantity, price float64, leverage int, entryPrice float64) { if at.store == nil { return @@ -1513,10 +1513,10 @@ func (at *AutoTrader) recordPositionChange(orderID, symbol, side, action string, switch action { case "open_long", "open_short": - // 开仓:创建新的仓位记录 + // Open position: create new position record pos := &store.TraderPosition{ TraderID: at.id, - ExchangeID: at.exchange, // 记录具体的交易所ID + ExchangeID: at.exchange, // Record specific exchange ID Symbol: symbol, Side: side, // LONG or SHORT Quantity: quantity, @@ -1527,20 +1527,20 @@ func (at *AutoTrader) recordPositionChange(orderID, symbol, side, action string, Status: "OPEN", } if err := at.store.Position().Create(pos); err != nil { - logger.Infof(" ⚠️ 记录仓位失败: %v", err) + logger.Infof(" ⚠️ Failed to record position: %v", err) } else { - logger.Infof(" 📊 仓位已记录 [%s] %s %s @ %.4f", at.id[:8], symbol, side, price) + logger.Infof(" 📊 Position recorded [%s] %s %s @ %.4f", at.id[:8], symbol, side, price) } case "close_long", "close_short": - // 平仓:找到对应的开仓记录并更新 + // Close position: find corresponding open position record and update openPos, err := at.store.Position().GetOpenPositionBySymbol(at.id, symbol, side) if err != nil || openPos == nil { - logger.Infof(" ⚠️ 找不到对应的开仓记录 (%s %s)", symbol, side) + logger.Infof(" ⚠️ Cannot find corresponding open position record (%s %s)", symbol, side) return } - // 计算盈亏 + // Calculate P&L var realizedPnL float64 if side == "LONG" { realizedPnL = (price - openPos.EntryPrice) * openPos.Quantity @@ -1548,19 +1548,19 @@ func (at *AutoTrader) recordPositionChange(orderID, symbol, side, action string, realizedPnL = (openPos.EntryPrice - price) * openPos.Quantity } - // 更新仓位记录 + // Update position record err = at.store.Position().ClosePosition( openPos.ID, price, // exitPrice orderID, // exitOrderID realizedPnL, - 0, // fee (暂不计算) + 0, // fee (not calculated yet) "ai_decision", ) if err != nil { - logger.Infof(" ⚠️ 更新仓位失败: %v", err) + logger.Infof(" ⚠️ Failed to update position: %v", err) } else { - logger.Infof(" 📊 仓位已平仓 [%s] %s %s @ %.4f → %.4f, PnL: %.2f", + logger.Infof(" 📊 Position closed [%s] %s %s @ %.4f → %.4f, P&L: %.2f", at.id[:8], symbol, side, openPos.EntryPrice, price, realizedPnL) } } diff --git a/trader/auto_trader_test.go b/trader/auto_trader_test.go index 8981ca81..8a47d12a 100644 --- a/trader/auto_trader_test.go +++ b/trader/auto_trader_test.go @@ -17,44 +17,44 @@ import ( ) // ============================================================ -// AutoTraderTestSuite - 使用 testify/suite 进行结构化测试 +// AutoTraderTestSuite - Structured testing using testify/suite // ============================================================ -// AutoTraderTestSuite 是 AutoTrader 的测试套件 -// 使用 testify/suite 来组织测试,提供统一的 setup/teardown 和 mock 管理 +// AutoTraderTestSuite Test suite for AutoTrader +// Uses testify/suite to organize tests, providing unified setup/teardown and mock management type AutoTraderTestSuite struct { suite.Suite - // 测试对象 + // Test subject autoTrader *AutoTrader - // Mock 依赖 + // Mock dependencies mockTrader *MockTrader mockStore *store.Store // gomonkey patches patches *gomonkey.Patches - // 测试配置 + // Test configuration config AutoTraderConfig } -// SetupSuite 在整个测试套件开始前执行一次 +// SetupSuite Executed once before the entire test suite starts func (s *AutoTraderTestSuite) SetupSuite() { - // 可以在这里初始化一些全局资源 + // Can initialize some global resources here } -// TearDownSuite 在整个测试套件结束后执行一次 +// TearDownSuite Executed once after the entire test suite ends func (s *AutoTraderTestSuite) TearDownSuite() { - // 清理全局资源 + // Clean up global resources } -// SetupTest 在每个测试用例开始前执行 +// SetupTest Executed before each test case starts func (s *AutoTraderTestSuite) SetupTest() { - // 初始化 patches + // Initialize patches s.patches = gomonkey.NewPatches() - // 创建 mock 对象 + // Create mock objects s.mockTrader = &MockTrader{ balance: map[string]interface{}{ "totalWalletBalance": 10000.0, @@ -65,10 +65,10 @@ func (s *AutoTraderTestSuite) SetupTest() { } - // 创建临时store(使用nil表示测试中不需要实际的store) + // Create temporary store (using nil means no actual store needed in test) s.mockStore = nil - // 设置默认配置 + // Set default configuration s.config = AutoTraderConfig{ ID: "test_trader", Name: "Test Trader", @@ -82,7 +82,7 @@ func (s *AutoTraderTestSuite) SetupTest() { IsCrossMargin: true, } - // 创建 AutoTrader 实例(直接构造,不调用 NewAutoTrader 以避免外部依赖) + // Create AutoTrader instance (direct construction, don't call NewAutoTrader to avoid external dependencies) s.autoTrader = &AutoTrader{ id: s.config.ID, name: s.config.Name, @@ -90,7 +90,7 @@ func (s *AutoTraderTestSuite) SetupTest() { exchange: s.config.Exchange, config: s.config, trader: s.mockTrader, - mcpClient: nil, // 测试中不需要实际的 MCP Client + mcpClient: nil, // No actual MCP Client needed in tests store: s.mockStore, initialBalance: s.config.InitialBalance, systemPromptTemplate: s.config.SystemPromptTemplate, @@ -108,16 +108,16 @@ func (s *AutoTraderTestSuite) SetupTest() { } } -// TearDownTest 在每个测试用例结束后执行 +// TearDownTest Executed after each test case ends func (s *AutoTraderTestSuite) TearDownTest() { - // 重置 gomonkey patches + // Reset gomonkey patches if s.patches != nil { s.patches.Reset() } } // ============================================================ -// 层次 1: 工具函数测试 +// Level 1: Utility function tests // ============================================================ func (s *AutoTraderTestSuite) TestSortDecisionsByPriority() { @@ -126,7 +126,7 @@ func (s *AutoTraderTestSuite) TestSortDecisionsByPriority() { input []decision.Decision }{ { - name: "混合决策_验证优先级排序", + name: "Mixed decisions - verify priority sorting", input: []decision.Decision{ {Action: "open_long", Symbol: "BTCUSDT"}, {Action: "close_short", Symbol: "ETHUSDT"}, @@ -141,9 +141,9 @@ func (s *AutoTraderTestSuite) TestSortDecisionsByPriority() { s.Run(tt.name, func() { result := sortDecisionsByPriority(tt.input) - s.Equal(len(tt.input), len(result), "结果长度应该相同") + s.Equal(len(tt.input), len(result), "Result length should be the same") - // 验证优先级是否递增 + // Verify priority is increasing getActionPriority := func(action string) int { switch action { case "close_long", "close_short": @@ -160,7 +160,7 @@ func (s *AutoTraderTestSuite) TestSortDecisionsByPriority() { for i := 0; i < len(result)-1; i++ { currentPriority := getActionPriority(result[i].Action) nextPriority := getActionPriority(result[i+1].Action) - s.LessOrEqual(currentPriority, nextPriority, "优先级应该递增") + s.LessOrEqual(currentPriority, nextPriority, "Priority should be increasing") } }) } @@ -172,10 +172,10 @@ func (s *AutoTraderTestSuite) TestNormalizeSymbol() { input string expected string }{ - {"已经是标准格式", "BTCUSDT", "BTCUSDT"}, - {"小写转大写", "btcusdt", "BTCUSDT"}, - {"只有币种名称_添加USDT", "BTC", "BTCUSDT"}, - {"带空格_去除空格", " BTC ", "BTCUSDT"}, + {"Already standard format", "BTCUSDT", "BTCUSDT"}, + {"Lowercase to uppercase", "btcusdt", "BTCUSDT"}, + {"Coin name only - add USDT", "BTC", "BTCUSDT"}, + {"With spaces - remove spaces", " BTC ", "BTCUSDT"}, } for _, tt := range tests { @@ -187,7 +187,7 @@ func (s *AutoTraderTestSuite) TestNormalizeSymbol() { } // ============================================================ -// 层次 2: Getter/Setter 测试 +// Level 2: Getter/Setter tests // ============================================================ func (s *AutoTraderTestSuite) TestGettersAndSetters() { @@ -211,38 +211,38 @@ func (s *AutoTraderTestSuite) TestGettersAndSetters() { } // ============================================================ -// 层次 3: PeakPnL 缓存测试 +// Level 3: PeakPnL cache tests // ============================================================ func (s *AutoTraderTestSuite) TestPeakPnLCache() { - s.Run("UpdatePeakPnL_首次记录", func() { + s.Run("UpdatePeakPnL_first record", func() { s.autoTrader.UpdatePeakPnL("BTCUSDT", "long", 10.5) cache := s.autoTrader.GetPeakPnLCache() s.Equal(10.5, cache["BTCUSDT_long"]) }) - s.Run("UpdatePeakPnL_更新为更高值", func() { + s.Run("UpdatePeakPnL_update to higher value", func() { s.autoTrader.UpdatePeakPnL("BTCUSDT", "long", 15.0) cache := s.autoTrader.GetPeakPnLCache() s.Equal(15.0, cache["BTCUSDT_long"]) }) - s.Run("UpdatePeakPnL_不更新为更低值", func() { + s.Run("UpdatePeakPnL_do not update to lower value", func() { s.autoTrader.UpdatePeakPnL("BTCUSDT", "long", 12.0) cache := s.autoTrader.GetPeakPnLCache() - s.Equal(15.0, cache["BTCUSDT_long"], "峰值应保持不变") + s.Equal(15.0, cache["BTCUSDT_long"], "Peak value should remain unchanged") }) s.Run("ClearPeakPnLCache", func() { s.autoTrader.ClearPeakPnLCache("BTCUSDT", "long") cache := s.autoTrader.GetPeakPnLCache() _, exists := cache["BTCUSDT_long"] - s.False(exists, "应该被清除") + s.False(exists, "Should be cleared") }) } // ============================================================ -// 层次 4: GetStatus 测试 +// Level 4: GetStatus tests // ============================================================ func (s *AutoTraderTestSuite) TestGetStatus() { @@ -261,7 +261,7 @@ func (s *AutoTraderTestSuite) TestGetStatus() { } // ============================================================ -// 层次 5: GetAccountInfo 测试 +// Level 5: GetAccountInfo tests // ============================================================ func (s *AutoTraderTestSuite) TestGetAccountInfo() { @@ -270,29 +270,29 @@ func (s *AutoTraderTestSuite) TestGetAccountInfo() { s.NoError(err) s.NotNil(accountInfo) - // 验证核心字段和数值 + // Verify core fields and values s.Equal(10100.0, accountInfo["total_equity"]) // 10000 + 100 s.Equal(8000.0, accountInfo["available_balance"]) s.Equal(100.0, accountInfo["total_pnl"]) // 10100 - 10000 } // ============================================================ -// 层次 6: GetPositions 测试 +// Level 6: GetPositions tests // ============================================================ func (s *AutoTraderTestSuite) TestGetPositions() { - s.Run("空持仓", func() { + s.Run("No positions", func() { positions, err := s.autoTrader.GetPositions() s.NoError(err) - // positions 可能是 nil 或空数组,两者都是有效的 + // positions may be nil or empty array, both are valid if positions != nil { s.Equal(0, len(positions)) } }) - s.Run("有持仓", func() { - // 设置 mock 持仓 + s.Run("Has positions", func() { + // Set mock positions s.mockTrader.positions = []map[string]interface{}{ { "symbol": "BTCUSDT", @@ -320,13 +320,13 @@ func (s *AutoTraderTestSuite) TestGetPositions() { } // ============================================================ -// 层次 7: getCandidateCoins 测试 +// Level 7: getCandidateCoins tests // ============================================================ func (s *AutoTraderTestSuite) TestGetCandidateCoins() { - s.Run("使用数据库默认币种", func() { + s.Run("Use database default coins", func() { s.autoTrader.defaultCoins = []string{"BTC", "ETH", "BNB"} - s.autoTrader.tradingCoins = []string{} // 空的自定义币种 + s.autoTrader.tradingCoins = []string{} // Empty custom coins coins, err := s.autoTrader.getCandidateCoins() @@ -338,7 +338,7 @@ func (s *AutoTraderTestSuite) TestGetCandidateCoins() { s.Contains(coins[0].Sources, "default") }) - s.Run("使用自定义币种", func() { + s.Run("Use custom coins", func() { s.autoTrader.tradingCoins = []string{"SOL", "AVAX"} coins, err := s.autoTrader.getCandidateCoins() @@ -350,9 +350,9 @@ func (s *AutoTraderTestSuite) TestGetCandidateCoins() { s.Contains(coins[0].Sources, "custom") }) - s.Run("使用AI500+OI作为fallback", func() { - s.autoTrader.defaultCoins = []string{} // 空的默认币种 - s.autoTrader.tradingCoins = []string{} // 空的自定义币种 + s.Run("Use AI500+OI as fallback", func() { + s.autoTrader.defaultCoins = []string{} // Empty default coins + s.autoTrader.tradingCoins = []string{} // Empty custom coins // Mock pool.GetMergedCoinPool s.patches.ApplyFunc(pool.GetMergedCoinPool, func(ai500Limit int) (*pool.MergedCoinPool, error) { @@ -373,7 +373,7 @@ func (s *AutoTraderTestSuite) TestGetCandidateCoins() { } // ============================================================ -// 层次 8: buildTradingContext 测试 +// Level 8: buildTradingContext tests // ============================================================ func (s *AutoTraderTestSuite) TestBuildTradingContext() { @@ -387,7 +387,7 @@ func (s *AutoTraderTestSuite) TestBuildTradingContext() { s.NoError(err) s.NotNil(ctx) - // 验证核心字段 + // Verify core fields s.Equal(10100.0, ctx.Account.TotalEquity) // 10000 + 100 s.Equal(8000.0, ctx.Account.AvailableBalance) s.Equal(10, ctx.BTCETHLeverage) @@ -395,10 +395,10 @@ func (s *AutoTraderTestSuite) TestBuildTradingContext() { } // ============================================================ -// 层次 9: 交易执行测试 +// Level 9: Trade execution tests // ============================================================ -// TestExecuteOpenPosition 测试开仓操作(多空通用) +// TestExecuteOpenPosition Test open position operation (common for long and short) func (s *AutoTraderTestSuite) TestExecuteOpenPosition() { tests := []struct { name string @@ -410,7 +410,7 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() { executeFn func(*decision.Decision, *store.DecisionAction) error }{ { - name: "成功开多仓", + name: "Successfully open long", action: "open_long", expectedOrder: 123456, availBalance: 8000.0, @@ -419,7 +419,7 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() { }, }, { - name: "成功开空仓", + name: "Successfully open short", action: "open_short", expectedOrder: 123457, availBalance: 8000.0, @@ -428,39 +428,39 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() { }, }, { - name: "多仓_保证金不足", + name: "Long - insufficient margin", action: "open_long", availBalance: 0.0, - expectedErr: "保证金不足", + expectedErr: "Insufficient margin", executeFn: func(d *decision.Decision, a *store.DecisionAction) error { return s.autoTrader.executeOpenLongWithRecord(d, a) }, }, { - name: "空仓_保证金不足", + name: "Short - insufficient margin", action: "open_short", availBalance: 0.0, - expectedErr: "保证金不足", + expectedErr: "Insufficient margin", executeFn: func(d *decision.Decision, a *store.DecisionAction) error { return s.autoTrader.executeOpenShortWithRecord(d, a) }, }, { - name: "多仓_已有同方向持仓", + name: "Long - already has same side position", action: "open_long", existingSide: "long", availBalance: 8000.0, - expectedErr: "已有多仓", + expectedErr: "Already has long position", executeFn: func(d *decision.Decision, a *store.DecisionAction) error { return s.autoTrader.executeOpenLongWithRecord(d, a) }, }, { - name: "空仓_已有同方向持仓", + name: "Short - already has same side position", action: "open_short", existingSide: "short", availBalance: 8000.0, - expectedErr: "已有空仓", + expectedErr: "Already has short position", executeFn: func(d *decision.Decision, a *store.DecisionAction) error { return s.autoTrader.executeOpenShortWithRecord(d, a) }, @@ -496,14 +496,14 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() { s.Equal(50000.0, actionRecord.Price) } - // 恢复默认状态 + // Restore default state s.mockTrader.balance["availableBalance"] = 8000.0 s.mockTrader.positions = []map[string]interface{}{} }) } } -// TestExecuteClosePosition 测试平仓操作(多空通用) +// TestExecuteClosePosition Test close position operation (common for long and short) func (s *AutoTraderTestSuite) TestExecuteClosePosition() { tests := []struct { name string @@ -513,7 +513,7 @@ func (s *AutoTraderTestSuite) TestExecuteClosePosition() { executeFn func(*decision.Decision, *store.DecisionAction) error }{ { - name: "成功平多仓", + name: "Successfully close long", action: "close_long", currentPrice: 51000.0, expectedOrder: 123458, @@ -522,7 +522,7 @@ func (s *AutoTraderTestSuite) TestExecuteClosePosition() { }, }, { - name: "成功平空仓", + name: "Successfully close short", action: "close_short", currentPrice: 49000.0, expectedOrder: 123459, @@ -552,7 +552,7 @@ func (s *AutoTraderTestSuite) TestExecuteClosePosition() { } // ============================================================ -// 层次 10: executeDecisionWithRecord 路由测试 +// Level 10: executeDecisionWithRecord routing tests // ============================================================ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() { @@ -564,7 +564,7 @@ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() { }, nil }) - s.Run("路由到open_long", func() { + s.Run("Route to open_long", func() { decision := &decision.Decision{ Action: "open_long", Symbol: "BTCUSDT", @@ -577,7 +577,7 @@ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() { s.NoError(err) }) - s.Run("路由到close_long", func() { + s.Run("Route to close_long", func() { decision := &decision.Decision{ Action: "close_long", Symbol: "BTCUSDT", @@ -588,7 +588,7 @@ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() { s.NoError(err) }) - s.Run("路由到hold_不执行", func() { + s.Run("Route to hold - no execution", func() { decision := &decision.Decision{ Action: "hold", Symbol: "BTCUSDT", @@ -599,7 +599,7 @@ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() { s.NoError(err) }) - s.Run("未知action返回错误", func() { + s.Run("Unknown action returns error", func() { decision := &decision.Decision{ Action: "unknown_action", Symbol: "BTCUSDT", @@ -608,7 +608,7 @@ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() { err := s.autoTrader.executeDecisionWithRecord(decision, actionRecord) s.Error(err) - s.Contains(err.Error(), "未知的action") + s.Contains(err.Error(), "Unknown action") }) } @@ -624,18 +624,18 @@ func (s *AutoTraderTestSuite) TestCheckPositionDrawdown() { skipCacheCheck bool }{ { - name: "获取持仓失败_不panic", + name: "Get positions failed - no panic", setupFailures: func() { s.mockTrader.shouldFailPositions = true }, cleanupFailures: func() { s.mockTrader.shouldFailPositions = false }, skipCacheCheck: true, }, { - name: "无持仓_不panic", + name: "No positions - no panic", setupPositions: func() { s.mockTrader.positions = []map[string]interface{}{} }, skipCacheCheck: true, }, { - name: "收益不足5%_不触发平仓", + name: "Profit less than 5% - no close", setupPositions: func() { s.mockTrader.positions = []map[string]interface{}{ {"symbol": "BTCUSDT", "side": "long", "positionAmt": 0.1, "entryPrice": 50000.0, "markPrice": 50150.0, "leverage": 10.0}, @@ -645,7 +645,7 @@ func (s *AutoTraderTestSuite) TestCheckPositionDrawdown() { skipCacheCheck: true, }, { - name: "回撤不足40%_不触发平仓", + name: "Drawdown less than 40% - no close", setupPositions: func() { s.mockTrader.positions = []map[string]interface{}{ {"symbol": "BTCUSDT", "side": "long", "positionAmt": 0.1, "entryPrice": 50000.0, "markPrice": 50400.0, "leverage": 10.0}, @@ -655,7 +655,7 @@ func (s *AutoTraderTestSuite) TestCheckPositionDrawdown() { skipCacheCheck: true, }, { - name: "多头_触发回撤平仓", + name: "Long - trigger drawdown close", setupPositions: func() { s.mockTrader.positions = []map[string]interface{}{ {"symbol": "BTCUSDT", "side": "long", "positionAmt": 0.1, "entryPrice": 50000.0, "markPrice": 50300.0, "leverage": 10.0}, @@ -666,7 +666,7 @@ func (s *AutoTraderTestSuite) TestCheckPositionDrawdown() { shouldClearCache: true, }, { - name: "空头_触发回撤平仓", + name: "Short - trigger drawdown close", setupPositions: func() { s.mockTrader.positions = []map[string]interface{}{ {"symbol": "ETHUSDT", "side": "short", "positionAmt": -0.5, "entryPrice": 3000.0, "markPrice": 2982.0, "leverage": 10.0}, @@ -677,7 +677,7 @@ func (s *AutoTraderTestSuite) TestCheckPositionDrawdown() { shouldClearCache: true, }, { - name: "多头_平仓失败_保留缓存", + name: "Long - close failed - keep cache", setupPositions: func() { s.mockTrader.positions = []map[string]interface{}{ {"symbol": "BTCUSDT", "side": "long", "positionAmt": 0.1, "entryPrice": 50000.0, "markPrice": 50300.0, "leverage": 10.0}, @@ -690,7 +690,7 @@ func (s *AutoTraderTestSuite) TestCheckPositionDrawdown() { shouldClearCache: false, }, { - name: "空头_平仓失败_保留缓存", + name: "Short - close failed - keep cache", setupPositions: func() { s.mockTrader.positions = []map[string]interface{}{ {"symbol": "ETHUSDT", "side": "short", "positionAmt": -0.5, "entryPrice": 3000.0, "markPrice": 2982.0, "leverage": 10.0}, @@ -725,23 +725,23 @@ func (s *AutoTraderTestSuite) TestCheckPositionDrawdown() { cache := s.autoTrader.GetPeakPnLCache() _, exists := cache[tt.expectedCacheKey] if tt.shouldClearCache { - s.False(exists, "峰值缓存应该被清理") + s.False(exists, "Peak PnL cache should be cleared") } else { - s.True(exists, "峰值缓存不应该被清理") + s.True(exists, "Peak PnL cache should not be cleared") } } - // 清理状态 + // Clean up state s.mockTrader.positions = []map[string]interface{}{} }) } } // ============================================================ -// Mock 实现 +// Mock implementations // ============================================================ -// MockDatabase 模拟数据库 +// MockDatabase Mock database type MockDatabase struct { shouldFail bool } @@ -753,7 +753,7 @@ func (m *MockDatabase) UpdateTraderInitialBalance(userID, traderID string, newBa return nil } -// MockTrader 增强版(添加错误控制) +// MockTrader Enhanced version (with error control) type MockTrader struct { balance map[string]interface{} positions []map[string]interface{} @@ -866,16 +866,16 @@ func (m *MockTrader) FormatQuantity(symbol string, quantity float64) (string, er } // ============================================================ -// 测试套件入口 +// Test suite entry point // ============================================================ -// TestAutoTraderTestSuite 运行 AutoTrader 测试套件 +// TestAutoTraderTestSuite Run AutoTrader test suite func TestAutoTraderTestSuite(t *testing.T) { suite.Run(t, new(AutoTraderTestSuite)) } // ============================================================ -// 独立的单元测试 - calculatePnLPercentage 函数测试 +// Independent unit tests - calculatePnLPercentage function tests // ============================================================ func TestCalculatePnLPercentage(t *testing.T) { @@ -886,58 +886,58 @@ func TestCalculatePnLPercentage(t *testing.T) { expected float64 }{ { - name: "正常盈利 - 10倍杠杆", - unrealizedPnl: 100.0, // 盈利 100 USDT - marginUsed: 1000.0, // 保证金 1000 USDT - expected: 10.0, // 10% 收益率 + name: "Normal profit - 10x leverage", + unrealizedPnl: 100.0, // 100 USDT profit + marginUsed: 1000.0, // 1000 USDT margin + expected: 10.0, // 10% return }, { - name: "正常亏损 - 10倍杠杆", - unrealizedPnl: -50.0, // 亏损 50 USDT - marginUsed: 1000.0, // 保证金 1000 USDT - expected: -5.0, // -5% 收益率 + name: "Normal loss - 10x leverage", + unrealizedPnl: -50.0, // 50 USDT loss + marginUsed: 1000.0, // 1000 USDT margin + expected: -5.0, // -5% return }, { - name: "高杠杆盈利 - 价格上涨1%,20倍杠杆", - unrealizedPnl: 200.0, // 盈利 200 USDT - marginUsed: 1000.0, // 保证金 1000 USDT - expected: 20.0, // 20% 收益率 + name: "High leverage profit - 1% price increase, 20x leverage", + unrealizedPnl: 200.0, // 200 USDT profit + marginUsed: 1000.0, // 1000 USDT margin + expected: 20.0, // 20% return }, { - name: "保证金为0 - 边界情况", + name: "Zero margin - edge case", unrealizedPnl: 100.0, marginUsed: 0.0, - expected: 0.0, // 应该返回 0 而不是除以零错误 + expected: 0.0, // Should return 0 instead of division by zero error }, { - name: "负保证金 - 边界情况", + name: "Negative margin - edge case", unrealizedPnl: 100.0, marginUsed: -1000.0, - expected: 0.0, // 应该返回 0(异常情况) + expected: 0.0, // Should return 0 (abnormal case) }, { - name: "盈亏为0", + name: "Zero PnL", unrealizedPnl: 0.0, marginUsed: 1000.0, expected: 0.0, }, { - name: "小额交易", + name: "Small trade", unrealizedPnl: 0.5, marginUsed: 10.0, expected: 5.0, }, { - name: "大额盈利", + name: "Large profit", unrealizedPnl: 5000.0, marginUsed: 10000.0, expected: 50.0, }, { - name: "极小保证金", + name: "Tiny margin", unrealizedPnl: 1.0, marginUsed: 0.01, - expected: 10000.0, // 100倍收益率 + expected: 10000.0, // 100x return }, } @@ -945,7 +945,7 @@ func TestCalculatePnLPercentage(t *testing.T) { t.Run(tt.name, func(t *testing.T) { result := calculatePnLPercentage(tt.unrealizedPnl, tt.marginUsed) - // 使用精度比较,避免浮点数误差 + // Use precision comparison to avoid floating point errors if math.Abs(result-tt.expected) > 0.0001 { t.Errorf("calculatePnLPercentage(%v, %v) = %v, want %v", tt.unrealizedPnl, tt.marginUsed, result, tt.expected) @@ -954,38 +954,38 @@ func TestCalculatePnLPercentage(t *testing.T) { } } -// TestCalculatePnLPercentage_RealWorldScenarios 真实场景测试 +// TestCalculatePnLPercentage_RealWorldScenarios Real world scenario tests func TestCalculatePnLPercentage_RealWorldScenarios(t *testing.T) { - t.Run("BTC 10倍杠杆,价格上涨2%", func(t *testing.T) { - // 开仓:1000 USDT 保证金,10倍杠杆 = 10000 USDT 仓位 - // 价格上涨 2% = 200 USDT 盈利 - // 收益率 = 200 / 1000 = 20% + t.Run("BTC 10x leverage, 2% price increase", func(t *testing.T) { + // Open: 1000 USDT margin, 10x leverage = 10000 USDT position + // 2% price increase = 200 USDT profit + // Return = 200 / 1000 = 20% result := calculatePnLPercentage(200.0, 1000.0) expected := 20.0 if math.Abs(result-expected) > 0.0001 { - t.Errorf("BTC场景: got %v, want %v", result, expected) + t.Errorf("BTC scenario: got %v, want %v", result, expected) } }) - t.Run("ETH 5倍杠杆,价格下跌3%", func(t *testing.T) { - // 开仓:2000 USDT 保证金,5倍杠杆 = 10000 USDT 仓位 - // 价格下跌 3% = -300 USDT 亏损 - // 收益率 = -300 / 2000 = -15% + t.Run("ETH 5x leverage, 3% price decrease", func(t *testing.T) { + // Open: 2000 USDT margin, 5x leverage = 10000 USDT position + // 3% price decrease = -300 USDT loss + // Return = -300 / 2000 = -15% result := calculatePnLPercentage(-300.0, 2000.0) expected := -15.0 if math.Abs(result-expected) > 0.0001 { - t.Errorf("ETH场景: got %v, want %v", result, expected) + t.Errorf("ETH scenario: got %v, want %v", result, expected) } }) - t.Run("SOL 20倍杠杆,价格上涨0.5%", func(t *testing.T) { - // 开仓:500 USDT 保证金,20倍杠杆 = 10000 USDT 仓位 - // 价格上涨 0.5% = 50 USDT 盈利 - // 收益率 = 50 / 500 = 10% + t.Run("SOL 20x leverage, 0.5% price increase", func(t *testing.T) { + // Open: 500 USDT margin, 20x leverage = 10000 USDT position + // 0.5% price increase = 50 USDT profit + // Return = 50 / 500 = 10% result := calculatePnLPercentage(50.0, 500.0) expected := 10.0 if math.Abs(result-expected) > 0.0001 { - t.Errorf("SOL场景: got %v, want %v", result, expected) + t.Errorf("SOL scenario: got %v, want %v", result, expected) } }) } diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 1d5a256e..cb001955 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -15,27 +15,27 @@ import ( "github.com/adshao/go-binance/v2/futures" ) -// getBrOrderID 生成唯一订单ID(合约专用) -// 格式: x-{BR_ID}{TIMESTAMP}{RANDOM} -// 合约限制32字符,统一使用此限制以保持一致性 -// 使用纳秒时间戳+随机数确保全局唯一性(冲突概率 < 10^-20) +// getBrOrderID generates unique order ID (for futures contracts) +// Format: x-{BR_ID}{TIMESTAMP}{RANDOM} +// Futures limit is 32 characters, use this limit consistently +// Uses nanosecond timestamp + random number to ensure global uniqueness (collision probability < 10^-20) func getBrOrderID() string { - brID := "KzrpZaP9" // 合约br ID + brID := "KzrpZaP9" // Futures br ID - // 计算可用空间: 32 - len("x-KzrpZaP9") = 32 - 11 = 21字符 - // 分配: 13位时间戳 + 8位随机数 = 21字符(完美利用) - timestamp := time.Now().UnixNano() % 10000000000000 // 13位纳秒时间戳 + // Calculate available space: 32 - len("x-KzrpZaP9") = 32 - 11 = 21 characters + // Allocation: 13-digit timestamp + 8-digit random = 21 characters (perfect utilization) + timestamp := time.Now().UnixNano() % 10000000000000 // 13-digit nanosecond timestamp - // 生成4字节随机数(8位十六进制) + // Generate 4-byte random number (8 hex digits) randomBytes := make([]byte, 4) rand.Read(randomBytes) randomHex := hex.EncodeToString(randomBytes) - // 格式: x-KzrpZaP9{13位时间戳}{8位随机} - // 示例: x-KzrpZaP91234567890123abcdef12 (正好31字符) + // Format: x-KzrpZaP9{13-digit timestamp}{8-digit random} + // Example: x-KzrpZaP91234567890123abcdef12 (exactly 31 characters) orderID := fmt.Sprintf("x-%s%d%s", brID, timestamp, randomHex) - // 确保不超过32字符限制(理论上正好31字符) + // Ensure not exceeding 32-character limit (theoretically exactly 31 characters) if len(orderID) > 32 { orderID = orderID[:32] } @@ -43,25 +43,25 @@ func getBrOrderID() string { return orderID } -// FuturesTrader 币安合约交易器 +// FuturesTrader Binance futures trader type FuturesTrader struct { client *futures.Client - // 余额缓存 + // Balance cache cachedBalance map[string]interface{} balanceCacheTime time.Time balanceCacheMutex sync.RWMutex - // 持仓缓存 + // Position cache cachedPositions []map[string]interface{} positionsCacheTime time.Time positionsCacheMutex sync.RWMutex - // 缓存有效期(15秒) + // Cache validity period (15 seconds) cacheDuration time.Duration } -// NewFuturesTrader 创建合约交易器 +// NewFuturesTrader creates futures trader func NewFuturesTrader(apiKey, secretKey string, userId string) *FuturesTrader { client := futures.NewClient(apiKey, secretKey) @@ -70,76 +70,76 @@ func NewFuturesTrader(apiKey, secretKey string, userId string) *FuturesTrader { client = hookRes.GetResult() } - // 同步时间,避免 Timestamp ahead 错误 + // Sync time to avoid "Timestamp ahead" error syncBinanceServerTime(client) trader := &FuturesTrader{ client: client, - cacheDuration: 15 * time.Second, // 15秒缓存 + cacheDuration: 15 * time.Second, // 15-second cache } - // 设置双向持仓模式(Hedge Mode) - // 这是必需的,因为代码中使用了 PositionSide (LONG/SHORT) + // Set dual-side position mode (Hedge Mode) + // This is required because the code uses PositionSide (LONG/SHORT) if err := trader.setDualSidePosition(); err != nil { - logger.Infof("⚠️ 设置双向持仓模式失败: %v (如果已是双向模式则忽略此警告)", err) + logger.Infof("⚠️ Failed to set dual-side position mode: %v (ignore this warning if already in dual-side mode)", err) } return trader } -// setDualSidePosition 设置双向持仓模式(初始化时调用) +// setDualSidePosition sets dual-side position mode (called during initialization) func (t *FuturesTrader) setDualSidePosition() error { - // 尝试设置双向持仓模式 + // Try to set dual-side position mode err := t.client.NewChangePositionModeService(). - DualSide(true). // true = 双向持仓(Hedge Mode) + DualSide(true). // true = dual-side position (Hedge Mode) Do(context.Background()) if err != nil { - // 如果错误信息包含"No need to change",说明已经是双向持仓模式 + // If error message contains "No need to change", it means already in dual-side position mode if strings.Contains(err.Error(), "No need to change position side") { - logger.Infof(" ✓ 账户已是双向持仓模式(Hedge Mode)") + logger.Infof(" ✓ Account is already in dual-side position mode (Hedge Mode)") return nil } - // 其他错误则返回(但在调用方不会中断初始化) + // Other errors are returned (but won't interrupt initialization in the caller) return err } - logger.Infof(" ✓ 账户已切换为双向持仓模式(Hedge Mode)") - logger.Infof(" ℹ️ 双向持仓模式允许同时持有多单和空单") + logger.Infof(" ✓ Account has been switched to dual-side position mode (Hedge Mode)") + logger.Infof(" ℹ️ Dual-side position mode allows holding both long and short positions simultaneously") return nil } -// syncBinanceServerTime 同步币安服务器时间,确保请求时间戳合法 +// syncBinanceServerTime syncs Binance server time to ensure request timestamps are valid func syncBinanceServerTime(client *futures.Client) { serverTime, err := client.NewServerTimeService().Do(context.Background()) if err != nil { - logger.Infof("⚠️ 同步币安服务器时间失败: %v", err) + logger.Infof("⚠️ Failed to sync Binance server time: %v", err) return } now := time.Now().UnixMilli() offset := now - serverTime client.TimeOffset = offset - logger.Infof("⏱ 已同步币安服务器时间,偏移 %dms", offset) + logger.Infof("⏱ Binance server time synced, offset %dms", offset) } -// GetBalance 获取账户余额(带缓存) +// GetBalance gets account balance (with cache) func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) { - // 先检查缓存是否有效 + // First check if cache is valid t.balanceCacheMutex.RLock() if t.cachedBalance != nil && time.Since(t.balanceCacheTime) < t.cacheDuration { cacheAge := time.Since(t.balanceCacheTime) t.balanceCacheMutex.RUnlock() - logger.Infof("✓ 使用缓存的账户余额(缓存时间: %.1f秒前)", cacheAge.Seconds()) + logger.Infof("✓ Using cached account balance (cache age: %.1f seconds ago)", cacheAge.Seconds()) return t.cachedBalance, nil } t.balanceCacheMutex.RUnlock() - // 缓存过期或不存在,调用API - logger.Infof("🔄 缓存过期,正在调用币安API获取账户余额...") + // Cache expired or doesn't exist, call API + logger.Infof("🔄 Cache expired, calling Binance API to get account balance...") account, err := t.client.NewGetAccountService().Do(context.Background()) if err != nil { - logger.Infof("❌ 币安API调用失败: %v", err) - return nil, fmt.Errorf("获取账户信息失败: %w", err) + logger.Infof("❌ Binance API call failed: %v", err) + return nil, fmt.Errorf("failed to get account info: %w", err) } result := make(map[string]interface{}) @@ -147,12 +147,12 @@ func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) { result["availableBalance"], _ = strconv.ParseFloat(account.AvailableBalance, 64) result["totalUnrealizedProfit"], _ = strconv.ParseFloat(account.TotalUnrealizedProfit, 64) - logger.Infof("✓ 币安API返回: 总余额=%s, 可用=%s, 未实现盈亏=%s", + logger.Infof("✓ Binance API returned: total balance=%s, available=%s, unrealized PnL=%s", account.TotalWalletBalance, account.AvailableBalance, account.TotalUnrealizedProfit) - // 更新缓存 + // Update cache t.balanceCacheMutex.Lock() t.cachedBalance = result t.balanceCacheTime = time.Now() @@ -161,30 +161,30 @@ func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) { return result, nil } -// GetPositions 获取所有持仓(带缓存) +// GetPositions gets all positions (with cache) func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) { - // 先检查缓存是否有效 + // First check if cache is valid t.positionsCacheMutex.RLock() if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration { cacheAge := time.Since(t.positionsCacheTime) t.positionsCacheMutex.RUnlock() - logger.Infof("✓ 使用缓存的持仓信息(缓存时间: %.1f秒前)", cacheAge.Seconds()) + logger.Infof("✓ Using cached position information (cache age: %.1f seconds ago)", cacheAge.Seconds()) return t.cachedPositions, nil } t.positionsCacheMutex.RUnlock() - // 缓存过期或不存在,调用API - logger.Infof("🔄 缓存过期,正在调用币安API获取持仓信息...") + // Cache expired or doesn't exist, call API + logger.Infof("🔄 Cache expired, calling Binance API to get position information...") positions, err := t.client.NewGetPositionRiskService().Do(context.Background()) if err != nil { - return nil, fmt.Errorf("获取持仓失败: %w", err) + return nil, fmt.Errorf("failed to get positions: %w", err) } var result []map[string]interface{} for _, pos := range positions { posAmt, _ := strconv.ParseFloat(pos.PositionAmt, 64) if posAmt == 0 { - continue // 跳过无持仓的 + continue // Skip positions with zero amount } posMap := make(map[string]interface{}) @@ -196,7 +196,7 @@ func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) { posMap["leverage"], _ = strconv.ParseFloat(pos.Leverage, 64) posMap["liquidationPrice"], _ = strconv.ParseFloat(pos.LiquidationPrice, 64) - // 判断方向 + // Determine direction if posAmt > 0 { posMap["side"] = "long" } else { @@ -206,7 +206,7 @@ func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) { result = append(result, posMap) } - // 更新缓存 + // Update cache t.positionsCacheMutex.Lock() t.cachedPositions = result t.positionsCacheTime = time.Now() @@ -215,7 +215,7 @@ func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) { return result, nil } -// SetMarginMode 设置仓位模式 +// SetMarginMode sets margin mode func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error { var marginType futures.MarginType if isCrossMargin { @@ -224,51 +224,51 @@ func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error { marginType = futures.MarginTypeIsolated } - // 尝试设置仓位模式 + // Try to set margin mode err := t.client.NewChangeMarginTypeService(). Symbol(symbol). MarginType(marginType). Do(context.Background()) - marginModeStr := "全仓" + marginModeStr := "Cross Margin" if !isCrossMargin { - marginModeStr = "逐仓" + marginModeStr = "Isolated Margin" } if err != nil { - // 如果错误信息包含"No need to change",说明仓位模式已经是目标值 + // If error message contains "No need to change", margin mode is already set to target value if contains(err.Error(), "No need to change margin type") { - logger.Infof(" ✓ %s 仓位模式已是 %s", symbol, marginModeStr) + logger.Infof(" ✓ %s margin mode is already %s", symbol, marginModeStr) return nil } - // 如果有持仓,无法更改仓位模式,但不影响交易 + // If there is an open position, margin mode cannot be changed, but this doesn't affect trading if contains(err.Error(), "Margin type cannot be changed if there exists position") { - logger.Infof(" ⚠️ %s 有持仓,无法更改仓位模式,继续使用当前模式", symbol) + logger.Infof(" ⚠️ %s has open positions, cannot change margin mode, continuing with current mode", symbol) return nil } - // 检测多资产模式(错误码 -4168) + // Detect Multi-Assets mode (error code -4168) if contains(err.Error(), "Multi-Assets mode") || contains(err.Error(), "-4168") || contains(err.Error(), "4168") { - logger.Infof(" ⚠️ %s 检测到多资产模式,强制使用全仓模式", symbol) - logger.Infof(" 💡 提示:如需使用逐仓模式,请在币安关闭多资产模式") + logger.Infof(" ⚠️ %s detected Multi-Assets mode, forcing Cross Margin mode", symbol) + logger.Infof(" 💡 Tip: To use Isolated Margin mode, please disable Multi-Assets mode in Binance") return nil } - // 检测统一账户 API(Portfolio Margin) + // Detect Unified Account API (Portfolio Margin) if contains(err.Error(), "unified") || contains(err.Error(), "portfolio") || contains(err.Error(), "Portfolio") { - logger.Infof(" ❌ %s 检测到统一账户 API,无法进行合约交易", symbol) - return fmt.Errorf("请使用「现货与合约交易」API 权限,不要使用「统一账户 API」") + logger.Infof(" ❌ %s detected Unified Account API, unable to trade futures", symbol) + return fmt.Errorf("please use 'Spot & Futures Trading' API permission, do not use 'Unified Account API'") } - logger.Infof(" ⚠️ 设置仓位模式失败: %v", err) - // 不返回错误,让交易继续 + logger.Infof(" ⚠️ Failed to set margin mode: %v", err) + // Don't return error, let trading continue return nil } - logger.Infof(" ✓ %s 仓位模式已设置为 %s", symbol, marginModeStr) + logger.Infof(" ✓ %s margin mode set to %s", symbol, marginModeStr) return nil } -// SetLeverage 设置杠杆(智能判断+冷却期) +// SetLeverage sets leverage (with smart detection and cooldown period) func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error { - // 先尝试获取当前杠杆(从持仓信息) + // First try to get current leverage (from position information) currentLeverage := 0 positions, err := t.GetPositions() if err == nil { @@ -282,68 +282,68 @@ func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error { } } - // 如果当前杠杆已经是目标杠杆,跳过 + // If current leverage is already the target leverage, skip if currentLeverage == leverage && currentLeverage > 0 { - logger.Infof(" ✓ %s 杠杆已是 %dx,无需切换", symbol, leverage) + logger.Infof(" ✓ %s leverage is already %dx, no need to change", symbol, leverage) return nil } - // 切换杠杆 + // Change leverage _, err = t.client.NewChangeLeverageService(). Symbol(symbol). Leverage(leverage). Do(context.Background()) if err != nil { - // 如果错误信息包含"No need to change",说明杠杆已经是目标值 + // If error message contains "No need to change", leverage is already the target value if contains(err.Error(), "No need to change") { - logger.Infof(" ✓ %s 杠杆已是 %dx", symbol, leverage) + logger.Infof(" ✓ %s leverage is already %dx", symbol, leverage) return nil } - return fmt.Errorf("设置杠杆失败: %w", err) + return fmt.Errorf("failed to set leverage: %w", err) } - logger.Infof(" ✓ %s 杠杆已切换为 %dx", symbol, leverage) + logger.Infof(" ✓ %s leverage changed to %dx", symbol, leverage) - // 切换杠杆后等待5秒(避免冷却期错误) - logger.Infof(" ⏱ 等待5秒冷却期...") + // Wait 5 seconds after changing leverage (to avoid cooldown period errors) + logger.Infof(" ⏱ Waiting 5 seconds for cooldown period...") time.Sleep(5 * time.Second) return nil } -// OpenLong 开多仓 +// OpenLong opens a long position func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 先取消该币种的所有委托单(清理旧的止损止盈单) + // First cancel all pending orders for this symbol (clean up old stop-loss and take-profit orders) if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err) + logger.Infof(" ⚠ Failed to cancel old pending orders (may not have any): %v", err) } - // 设置杠杆 + // Set leverage if err := t.SetLeverage(symbol, leverage); err != nil { return nil, err } - // 注意:仓位模式应该由调用方(AutoTrader)在开仓前通过 SetMarginMode 设置 + // Note: Margin mode should be set by the caller (AutoTrader) before opening position via SetMarginMode - // 格式化数量到正确精度 + // Format quantity to correct precision quantityStr, err := t.FormatQuantity(symbol, quantity) if err != nil { return nil, err } - // ✅ 检查格式化后的数量是否为 0(防止四舍五入导致的错误) + // Check if formatted quantity is 0 (prevent rounding errors) quantityFloat, parseErr := strconv.ParseFloat(quantityStr, 64) if parseErr != nil || quantityFloat <= 0 { - return nil, fmt.Errorf("开仓数量过小,格式化后为 0 (原始: %.8f → 格式化: %s)。建议增加开仓金额或选择价格更低的币种", quantity, quantityStr) + return nil, fmt.Errorf("position size too small, rounded to 0 (original: %.8f → formatted: %s). Suggest increasing position amount or selecting a lower-priced coin", quantity, quantityStr) } - // ✅ 检查最小名义价值(Binance 要求至少 10 USDT) + // Check minimum notional value (Binance requires at least 10 USDT) if err := t.CheckMinNotional(symbol, quantityFloat); err != nil { return nil, err } - // 创建市价买入订单(使用br ID) + // Create market buy order (using br ID) order, err := t.client.NewCreateOrderService(). Symbol(symbol). Side(futures.SideTypeBuy). @@ -354,11 +354,11 @@ func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int) Do(context.Background()) if err != nil { - return nil, fmt.Errorf("开多仓失败: %w", err) + return nil, fmt.Errorf("failed to open long position: %w", err) } - logger.Infof("✓ 开多仓成功: %s 数量: %s", symbol, quantityStr) - logger.Infof(" 订单ID: %d", order.OrderID) + logger.Infof("✓ Opened long position successfully: %s quantity: %s", symbol, quantityStr) + logger.Infof(" Order ID: %d", order.OrderID) result := make(map[string]interface{}) result["orderId"] = order.OrderID @@ -367,38 +367,38 @@ func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int) return result, nil } -// OpenShort 开空仓 +// OpenShort opens a short position func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 先取消该币种的所有委托单(清理旧的止损止盈单) + // First cancel all pending orders for this symbol (clean up old stop-loss and take-profit orders) if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err) + logger.Infof(" ⚠ Failed to cancel old pending orders (may not have any): %v", err) } - // 设置杠杆 + // Set leverage if err := t.SetLeverage(symbol, leverage); err != nil { return nil, err } - // 注意:仓位模式应该由调用方(AutoTrader)在开仓前通过 SetMarginMode 设置 + // Note: Margin mode should be set by the caller (AutoTrader) before opening position via SetMarginMode - // 格式化数量到正确精度 + // Format quantity to correct precision quantityStr, err := t.FormatQuantity(symbol, quantity) if err != nil { return nil, err } - // ✅ 检查格式化后的数量是否为 0(防止四舍五入导致的错误) + // Check if formatted quantity is 0 (prevent rounding errors) quantityFloat, parseErr := strconv.ParseFloat(quantityStr, 64) if parseErr != nil || quantityFloat <= 0 { - return nil, fmt.Errorf("开仓数量过小,格式化后为 0 (原始: %.8f → 格式化: %s)。建议增加开仓金额或选择价格更低的币种", quantity, quantityStr) + return nil, fmt.Errorf("position size too small, rounded to 0 (original: %.8f → formatted: %s). Suggest increasing position amount or selecting a lower-priced coin", quantity, quantityStr) } - // ✅ 检查最小名义价值(Binance 要求至少 10 USDT) + // Check minimum notional value (Binance requires at least 10 USDT) if err := t.CheckMinNotional(symbol, quantityFloat); err != nil { return nil, err } - // 创建市价卖出订单(使用br ID) + // Create market sell order (using br ID) order, err := t.client.NewCreateOrderService(). Symbol(symbol). Side(futures.SideTypeSell). @@ -409,11 +409,11 @@ func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int) Do(context.Background()) if err != nil { - return nil, fmt.Errorf("开空仓失败: %w", err) + return nil, fmt.Errorf("failed to open short position: %w", err) } - logger.Infof("✓ 开空仓成功: %s 数量: %s", symbol, quantityStr) - logger.Infof(" 订单ID: %d", order.OrderID) + logger.Infof("✓ Opened short position successfully: %s quantity: %s", symbol, quantityStr) + logger.Infof(" Order ID: %d", order.OrderID) result := make(map[string]interface{}) result["orderId"] = order.OrderID @@ -422,9 +422,9 @@ func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int) return result, nil } -// CloseLong 平多仓 +// CloseLong closes a long position func (t *FuturesTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果数量为0,获取当前持仓数量 + // If quantity is 0, get current position quantity if quantity == 0 { positions, err := t.GetPositions() if err != nil { @@ -439,17 +439,17 @@ func (t *FuturesTrader) CloseLong(symbol string, quantity float64) (map[string]i } if quantity == 0 { - return nil, fmt.Errorf("没有找到 %s 的多仓", symbol) + return nil, fmt.Errorf("no long position found for %s", symbol) } } - // 格式化数量 + // Format quantity quantityStr, err := t.FormatQuantity(symbol, quantity) if err != nil { return nil, err } - // 创建市价卖出订单(平多,使用br ID) + // Create market sell order (close long, using br ID) order, err := t.client.NewCreateOrderService(). Symbol(symbol). Side(futures.SideTypeSell). @@ -460,14 +460,14 @@ func (t *FuturesTrader) CloseLong(symbol string, quantity float64) (map[string]i Do(context.Background()) if err != nil { - return nil, fmt.Errorf("平多仓失败: %w", err) + return nil, fmt.Errorf("failed to close long position: %w", err) } - logger.Infof("✓ 平多仓成功: %s 数量: %s", symbol, quantityStr) + logger.Infof("✓ Closed long position successfully: %s quantity: %s", symbol, quantityStr) - // 平仓后取消该币种的所有挂单(止损止盈单) + // After closing position, cancel all pending orders for this symbol (stop-loss and take-profit orders) if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } result := make(map[string]interface{}) @@ -477,9 +477,9 @@ func (t *FuturesTrader) CloseLong(symbol string, quantity float64) (map[string]i return result, nil } -// CloseShort 平空仓 +// CloseShort closes a short position func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果数量为0,获取当前持仓数量 + // If quantity is 0, get current position quantity if quantity == 0 { positions, err := t.GetPositions() if err != nil { @@ -488,23 +488,23 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string] for _, pos := range positions { if pos["symbol"] == symbol && pos["side"] == "short" { - quantity = -pos["positionAmt"].(float64) // 空仓数量是负的,取绝对值 + quantity = -pos["positionAmt"].(float64) // Short position quantity is negative, take absolute value break } } if quantity == 0 { - return nil, fmt.Errorf("没有找到 %s 的空仓", symbol) + return nil, fmt.Errorf("no short position found for %s", symbol) } } - // 格式化数量 + // Format quantity quantityStr, err := t.FormatQuantity(symbol, quantity) if err != nil { return nil, err } - // 创建市价买入订单(平空,使用br ID) + // Create market buy order (close short, using br ID) order, err := t.client.NewCreateOrderService(). Symbol(symbol). Side(futures.SideTypeBuy). @@ -515,14 +515,14 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string] Do(context.Background()) if err != nil { - return nil, fmt.Errorf("平空仓失败: %w", err) + return nil, fmt.Errorf("failed to close short position: %w", err) } - logger.Infof("✓ 平空仓成功: %s 数量: %s", symbol, quantityStr) + logger.Infof("✓ Closed short position successfully: %s quantity: %s", symbol, quantityStr) - // 平仓后取消该币种的所有挂单(止损止盈单) + // After closing position, cancel all pending orders for this symbol (stop-loss and take-profit orders) if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } result := make(map[string]interface{}) @@ -532,24 +532,24 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string] return result, nil } -// CancelStopLossOrders 仅取消止损单(不影响止盈单) +// CancelStopLossOrders cancels only stop-loss orders (doesn't affect take-profit orders) func (t *FuturesTrader) CancelStopLossOrders(symbol string) error { - // 获取该币种的所有未完成订单 + // Get all open orders for this symbol orders, err := t.client.NewListOpenOrdersService(). Symbol(symbol). Do(context.Background()) if err != nil { - return fmt.Errorf("获取未完成订单失败: %w", err) + return fmt.Errorf("failed to get open orders: %w", err) } - // 过滤出止损单并取消(取消所有方向的止损单,包括LONG和SHORT) + // Filter out stop-loss orders and cancel them (cancel all directions including LONG and SHORT) canceledCount := 0 var cancelErrors []error for _, order := range orders { orderType := order.Type - // 只取消止损订单(不取消止盈订单) + // Only cancel stop-loss orders (don't cancel take-profit orders) if orderType == futures.OrderTypeStopMarket || orderType == futures.OrderTypeStop { _, err := t.client.NewCancelOrderService(). Symbol(symbol). @@ -557,49 +557,49 @@ func (t *FuturesTrader) CancelStopLossOrders(symbol string) error { Do(context.Background()) if err != nil { - errMsg := fmt.Sprintf("订单ID %d: %v", order.OrderID, err) + errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err) cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg)) - logger.Infof(" ⚠ 取消止损单失败: %s", errMsg) + logger.Infof(" ⚠ Failed to cancel stop-loss order: %s", errMsg) continue } canceledCount++ - logger.Infof(" ✓ 已取消止损单 (订单ID: %d, 类型: %s, 方向: %s)", order.OrderID, orderType, order.PositionSide) + logger.Infof(" ✓ Canceled stop-loss order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide) } } if canceledCount == 0 && len(cancelErrors) == 0 { - logger.Infof(" ℹ %s 没有止损单需要取消", symbol) + logger.Infof(" ℹ %s has no stop-loss orders to cancel", symbol) } else if canceledCount > 0 { - logger.Infof(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount) + logger.Infof(" ✓ Canceled %d stop-loss order(s) for %s", canceledCount, symbol) } - // 如果所有取消都失败了,返回错误 + // If all cancellations failed, return error if len(cancelErrors) > 0 && canceledCount == 0 { - return fmt.Errorf("取消止损单失败: %v", cancelErrors) + return fmt.Errorf("failed to cancel stop-loss orders: %v", cancelErrors) } return nil } -// CancelTakeProfitOrders 仅取消止盈单(不影响止损单) +// CancelTakeProfitOrders cancels only take-profit orders (doesn't affect stop-loss orders) func (t *FuturesTrader) CancelTakeProfitOrders(symbol string) error { - // 获取该币种的所有未完成订单 + // Get all open orders for this symbol orders, err := t.client.NewListOpenOrdersService(). Symbol(symbol). Do(context.Background()) if err != nil { - return fmt.Errorf("获取未完成订单失败: %w", err) + return fmt.Errorf("failed to get open orders: %w", err) } - // 过滤出止盈单并取消(取消所有方向的止盈单,包括LONG和SHORT) + // Filter out take-profit orders and cancel them (cancel all directions including LONG and SHORT) canceledCount := 0 var cancelErrors []error for _, order := range orders { orderType := order.Type - // 只取消止盈订单(不取消止损订单) + // Only cancel take-profit orders (don't cancel stop-loss orders) if orderType == futures.OrderTypeTakeProfitMarket || orderType == futures.OrderTypeTakeProfit { _, err := t.client.NewCancelOrderService(). Symbol(symbol). @@ -607,62 +607,62 @@ func (t *FuturesTrader) CancelTakeProfitOrders(symbol string) error { Do(context.Background()) if err != nil { - errMsg := fmt.Sprintf("订单ID %d: %v", order.OrderID, err) + errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err) cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg)) - logger.Infof(" ⚠ 取消止盈单失败: %s", errMsg) + logger.Infof(" ⚠ Failed to cancel take-profit order: %s", errMsg) continue } canceledCount++ - logger.Infof(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s, 方向: %s)", order.OrderID, orderType, order.PositionSide) + logger.Infof(" ✓ Canceled take-profit order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide) } } if canceledCount == 0 && len(cancelErrors) == 0 { - logger.Infof(" ℹ %s 没有止盈单需要取消", symbol) + logger.Infof(" ℹ %s has no take-profit orders to cancel", symbol) } else if canceledCount > 0 { - logger.Infof(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount) + logger.Infof(" ✓ Canceled %d take-profit order(s) for %s", canceledCount, symbol) } - // 如果所有取消都失败了,返回错误 + // If all cancellations failed, return error if len(cancelErrors) > 0 && canceledCount == 0 { - return fmt.Errorf("取消止盈单失败: %v", cancelErrors) + return fmt.Errorf("failed to cancel take-profit orders: %v", cancelErrors) } return nil } -// CancelAllOrders 取消该币种的所有挂单 +// CancelAllOrders cancels all pending orders for this symbol func (t *FuturesTrader) CancelAllOrders(symbol string) error { err := t.client.NewCancelAllOpenOrdersService(). Symbol(symbol). Do(context.Background()) if err != nil { - return fmt.Errorf("取消挂单失败: %w", err) + return fmt.Errorf("failed to cancel pending orders: %w", err) } - logger.Infof(" ✓ 已取消 %s 的所有挂单", symbol) + logger.Infof(" ✓ Canceled all pending orders for %s", symbol) return nil } -// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +// CancelStopOrders cancels take-profit/stop-loss orders for this symbol (used to adjust TP/SL positions) func (t *FuturesTrader) CancelStopOrders(symbol string) error { - // 获取该币种的所有未完成订单 + // Get all open orders for this symbol orders, err := t.client.NewListOpenOrdersService(). Symbol(symbol). Do(context.Background()) if err != nil { - return fmt.Errorf("获取未完成订单失败: %w", err) + return fmt.Errorf("failed to get open orders: %w", err) } - // 过滤出止盈止损单并取消 + // Filter out take-profit and stop-loss orders and cancel them canceledCount := 0 for _, order := range orders { orderType := order.Type - // 只取消止损和止盈订单 + // Only cancel stop-loss and take-profit orders if orderType == futures.OrderTypeStopMarket || orderType == futures.OrderTypeTakeProfitMarket || orderType == futures.OrderTypeStop || @@ -674,34 +674,34 @@ func (t *FuturesTrader) CancelStopOrders(symbol string) error { Do(context.Background()) if err != nil { - logger.Infof(" ⚠ 取消订单 %d 失败: %v", order.OrderID, err) + logger.Infof(" ⚠ Failed to cancel order %d: %v", order.OrderID, err) continue } canceledCount++ - logger.Infof(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)", + logger.Infof(" ✓ Canceled take-profit/stop-loss order for %s (Order ID: %d, Type: %s)", symbol, order.OrderID, orderType) } } if canceledCount == 0 { - logger.Infof(" ℹ %s 没有止盈/止损单需要取消", symbol) + logger.Infof(" ℹ %s has no take-profit/stop-loss orders to cancel", symbol) } else { - logger.Infof(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + logger.Infof(" ✓ Canceled %d take-profit/stop-loss order(s) for %s", canceledCount, symbol) } return nil } -// GetMarketPrice 获取市场价格 +// GetMarketPrice gets market price func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) { prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background()) if err != nil { - return 0, fmt.Errorf("获取价格失败: %w", err) + return 0, fmt.Errorf("failed to get price: %w", err) } if len(prices) == 0 { - return 0, fmt.Errorf("未找到价格") + return 0, fmt.Errorf("price not found") } price, err := strconv.ParseFloat(prices[0].Price, 64) @@ -712,7 +712,7 @@ func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) { return price, nil } -// CalculatePositionSize 计算仓位大小 +// CalculatePositionSize calculates position size func (t *FuturesTrader) CalculatePositionSize(balance, riskPercent, price float64, leverage int) float64 { riskAmount := balance * (riskPercent / 100.0) positionValue := riskAmount * float64(leverage) @@ -720,7 +720,7 @@ func (t *FuturesTrader) CalculatePositionSize(balance, riskPercent, price float6 return quantity } -// SetStopLoss 设置止损单 +// SetStopLoss sets stop-loss order func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { var side futures.SideType var posSide futures.PositionSideType @@ -733,7 +733,7 @@ func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity posSide = futures.PositionSideTypeShort } - // 格式化数量 + // Format quantity quantityStr, err := t.FormatQuantity(symbol, quantity) if err != nil { return err @@ -752,14 +752,14 @@ func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity Do(context.Background()) if err != nil { - return fmt.Errorf("设置止损失败: %w", err) + return fmt.Errorf("failed to set stop-loss: %w", err) } - logger.Infof(" 止损价设置: %.4f", stopPrice) + logger.Infof(" Stop-loss price set: %.4f", stopPrice) return nil } -// SetTakeProfit 设置止盈单 +// SetTakeProfit sets take-profit order func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { var side futures.SideType var posSide futures.PositionSideType @@ -772,7 +772,7 @@ func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quanti posSide = futures.PositionSideTypeShort } - // 格式化数量 + // Format quantity quantityStr, err := t.FormatQuantity(symbol, quantity) if err != nil { return err @@ -791,24 +791,24 @@ func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quanti Do(context.Background()) if err != nil { - return fmt.Errorf("设置止盈失败: %w", err) + return fmt.Errorf("failed to set take-profit: %w", err) } - logger.Infof(" 止盈价设置: %.4f", takeProfitPrice) + logger.Infof(" Take-profit price set: %.4f", takeProfitPrice) return nil } -// GetMinNotional 获取最小名义价值(Binance要求) +// GetMinNotional gets minimum notional value (Binance requirement) func (t *FuturesTrader) GetMinNotional(symbol string) float64 { - // 使用保守的默认值 10 USDT,确保订单能够通过交易所验证 + // Use conservative default value of 10 USDT to ensure order passes exchange validation return 10.0 } -// CheckMinNotional 检查订单是否满足最小名义价值要求 +// CheckMinNotional checks if order meets minimum notional value requirement func (t *FuturesTrader) CheckMinNotional(symbol string, quantity float64) error { price, err := t.GetMarketPrice(symbol) if err != nil { - return fmt.Errorf("获取市价失败: %w", err) + return fmt.Errorf("failed to get market price: %w", err) } notionalValue := quantity * price @@ -816,7 +816,7 @@ func (t *FuturesTrader) CheckMinNotional(symbol string, quantity float64) error if notionalValue < minNotional { return fmt.Errorf( - "订单金额 %.2f USDT 低于最小要求 %.2f USDT (数量: %.4f, 价格: %.4f)", + "order amount %.2f USDT is below minimum requirement %.2f USDT (quantity: %.4f, price: %.4f)", notionalValue, minNotional, quantity, price, ) } @@ -824,37 +824,37 @@ func (t *FuturesTrader) CheckMinNotional(symbol string, quantity float64) error return nil } -// GetSymbolPrecision 获取交易对的数量精度 +// GetSymbolPrecision gets the quantity precision for a trading pair func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) { exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background()) if err != nil { - return 0, fmt.Errorf("获取交易规则失败: %w", err) + return 0, fmt.Errorf("failed to get trading rules: %w", err) } for _, s := range exchangeInfo.Symbols { if s.Symbol == symbol { - // 从LOT_SIZE filter获取精度 + // Get precision from LOT_SIZE filter for _, filter := range s.Filters { if filter["filterType"] == "LOT_SIZE" { stepSize := filter["stepSize"].(string) precision := calculatePrecision(stepSize) - logger.Infof(" %s 数量精度: %d (stepSize: %s)", symbol, precision, stepSize) + logger.Infof(" %s quantity precision: %d (stepSize: %s)", symbol, precision, stepSize) return precision, nil } } } } - logger.Infof(" ⚠ %s 未找到精度信息,使用默认精度3", symbol) - return 3, nil // 默认精度为3 + logger.Infof(" ⚠ %s precision information not found, using default precision 3", symbol) + return 3, nil // Default precision is 3 } -// calculatePrecision 从stepSize计算精度 +// calculatePrecision calculates precision from stepSize func calculatePrecision(stepSize string) int { - // 去除尾部的0 + // Remove trailing zeros stepSize = trimTrailingZeros(stepSize) - // 查找小数点 + // Find decimal point dotIndex := -1 for i := 0; i < len(stepSize); i++ { if stepSize[i] == '.' { @@ -863,28 +863,28 @@ func calculatePrecision(stepSize string) int { } } - // 如果没有小数点或小数点在最后,精度为0 + // If no decimal point or decimal point is at the end, precision is 0 if dotIndex == -1 || dotIndex == len(stepSize)-1 { return 0 } - // 返回小数点后的位数 + // Return number of digits after decimal point return len(stepSize) - dotIndex - 1 } -// trimTrailingZeros 去除尾部的0 +// trimTrailingZeros removes trailing zeros func trimTrailingZeros(s string) string { - // 如果没有小数点,直接返回 + // If no decimal point, return directly if !stringContains(s, ".") { return s } - // 从后向前遍历,去除尾部的0 + // Iterate backwards to remove trailing zeros for len(s) > 0 && s[len(s)-1] == '0' { s = s[:len(s)-1] } - // 如果最后一位是小数点,也去掉 + // If last character is decimal point, remove it too if len(s) > 0 && s[len(s)-1] == '.' { s = s[:len(s)-1] } @@ -892,11 +892,11 @@ func trimTrailingZeros(s string) string { return s } -// FormatQuantity 格式化数量到正确的精度 +// FormatQuantity formats quantity to correct precision func (t *FuturesTrader) FormatQuantity(symbol string, quantity float64) (string, error) { precision, err := t.GetSymbolPrecision(symbol) if err != nil { - // 如果获取失败,使用默认格式 + // If retrieval fails, use default format return fmt.Sprintf("%.3f", quantity), nil } @@ -904,7 +904,7 @@ func (t *FuturesTrader) FormatQuantity(symbol string, quantity float64) (string, return fmt.Sprintf(format, quantity), nil } -// 辅助函数 +// Helper functions func contains(s, substr string) bool { return len(s) >= len(substr) && stringContains(s, substr) } @@ -918,12 +918,12 @@ func stringContains(s, substr string) bool { return false } -// GetOrderStatus 获取订单状态 +// GetOrderStatus gets order status func (t *FuturesTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { - // 将 orderID 转换为 int64 + // Convert orderID to int64 orderIDInt, err := strconv.ParseInt(orderID, 10, 64) if err != nil { - return nil, fmt.Errorf("无效的订单ID: %s", orderID) + return nil, fmt.Errorf("invalid order ID: %s", orderID) } order, err := t.client.NewGetOrderService(). @@ -931,10 +931,10 @@ func (t *FuturesTrader) GetOrderStatus(symbol string, orderID string) (map[strin OrderID(orderIDInt). Do(context.Background()) if err != nil { - return nil, fmt.Errorf("获取订单状态失败: %w", err) + return nil, fmt.Errorf("failed to get order status: %w", err) } - // 解析成交价格 + // Parse execution price avgPrice, _ := strconv.ParseFloat(order.AvgPrice, 64) executedQty, _ := strconv.ParseFloat(order.ExecutedQuantity, 64) @@ -950,8 +950,8 @@ func (t *FuturesTrader) GetOrderStatus(symbol string, orderID string) (map[strin "updateTime": order.UpdateTime, } - // 币安合约的手续费需要通过 GetUserTrades 获取,这里暂时不获取 - // 后续可以通过 WebSocket 或单独查询获取 + // Binance futures commission fee needs to be obtained through GetUserTrades, not retrieved here for now + // Can be obtained later through WebSocket or separate query result["commission"] = 0.0 return result, nil diff --git a/trader/binance_futures_test.go b/trader/binance_futures_test.go index 6f9e2987..f6a166b3 100644 --- a/trader/binance_futures_test.go +++ b/trader/binance_futures_test.go @@ -14,21 +14,21 @@ import ( ) // ============================================================ -// 一、BinanceFuturesTestSuite - 继承 base test suite +// 1. BinanceFuturesTestSuite - Inherits base test suite // ============================================================ -// BinanceFuturesTestSuite 币安合约交易器测试套件 -// 继承 TraderTestSuite 并添加 Binance Futures 特定的 mock 逻辑 +// BinanceFuturesTestSuite Binance Futures trader test suite +// Inherits TraderTestSuite and adds Binance Futures specific mock logic type BinanceFuturesTestSuite struct { - *TraderTestSuite // 嵌入基础测试套件 + *TraderTestSuite // Embeds base test suite mockServer *httptest.Server } -// NewBinanceFuturesTestSuite 创建币安合约测试套件 +// NewBinanceFuturesTestSuite Creates Binance Futures test suite func NewBinanceFuturesTestSuite(t *testing.T) *BinanceFuturesTestSuite { - // 创建 mock HTTP 服务器 + // Create mock HTTP server mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 根据不同的 URL 路径返回不同的 mock 响应 + // Return different mock responses based on URL path path := r.URL.Path var respBody interface{} @@ -91,13 +91,13 @@ func NewBinanceFuturesTestSuite(t *testing.T) *BinanceFuturesTestSuite { case path == "/fapi/v1/ticker/price" || path == "/fapi/v2/ticker/price": symbol := r.URL.Query().Get("symbol") if symbol == "" { - // 返回所有价格 + // Return all prices respBody = []map[string]interface{}{ {"Symbol": "BTCUSDT", "Price": "50000.00", "Time": 1234567890}, {"Symbol": "ETHUSDT", "Price": "3000.00", "Time": 1234567890}, } } else if symbol == "INVALIDUSDT" { - // 返回错误 + // Return error w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]interface{}{ "code": -1121, @@ -105,7 +105,7 @@ func NewBinanceFuturesTestSuite(t *testing.T) *BinanceFuturesTestSuite { }) return } else { - // 返回单个价格(注意:即使有 symbol 参数,也要返回数组) + // Return single price (note: even with symbol parameter, return array) price := "50000.00" if symbol == "ETHUSDT" { price = "3000.00" @@ -221,11 +221,11 @@ func NewBinanceFuturesTestSuite(t *testing.T) *BinanceFuturesTestSuite { // Mock SetLeverage - /fapi/v1/leverage case path == "/fapi/v1/leverage": - // 将字符串转换为整数 + // Convert string to integer leverageStr := r.FormValue("leverage") - leverage := 10 // 默认值 + leverage := 10 // default value if leverageStr != "" { - // 注意:这里我们直接返回整数,而不是字符串 + // Note: here we return an integer directly, not a string fmt.Sscanf(leverageStr, "%d", &leverage) } respBody = map[string]interface{}{ @@ -259,23 +259,23 @@ func NewBinanceFuturesTestSuite(t *testing.T) *BinanceFuturesTestSuite { respBody = map[string]interface{}{} } - // 序列化响应 + // Serialize response w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(respBody) })) - // 创建 futures.Client 并设置为使用 mock 服务器 + // Create futures.Client and configure to use mock server client := futures.NewClient("test_api_key", "test_secret_key") client.BaseURL = mockServer.URL client.HTTPClient = mockServer.Client() - // 创建 FuturesTrader + // Create FuturesTrader trader := &FuturesTrader{ client: client, - cacheDuration: 0, // 禁用缓存以便测试 + cacheDuration: 0, // disable cache for testing } - // 创建基础套件 + // Create base suite baseSuite := NewTraderTestSuite(t, trader) return &BinanceFuturesTestSuite{ @@ -284,7 +284,7 @@ func NewBinanceFuturesTestSuite(t *testing.T) *BinanceFuturesTestSuite { } } -// Cleanup 清理资源 +// Cleanup cleans up resources func (s *BinanceFuturesTestSuite) Cleanup() { if s.mockServer != nil { s.mockServer.Close() @@ -293,31 +293,31 @@ func (s *BinanceFuturesTestSuite) Cleanup() { } // ============================================================ -// 二、使用 BinanceFuturesTestSuite 运行通用测试 +// 2. Run common tests using BinanceFuturesTestSuite // ============================================================ -// TestFuturesTrader_InterfaceCompliance 测试接口兼容性 +// TestFuturesTrader_InterfaceCompliance tests interface compliance func TestFuturesTrader_InterfaceCompliance(t *testing.T) { var _ Trader = (*FuturesTrader)(nil) } -// TestFuturesTrader_CommonInterface 使用测试套件运行所有通用接口测试 +// TestFuturesTrader_CommonInterface runs all common interface tests using test suite func TestFuturesTrader_CommonInterface(t *testing.T) { - // 创建测试套件 + // Create test suite suite := NewBinanceFuturesTestSuite(t) defer suite.Cleanup() - // 运行所有通用接口测试 + // Run all common interface tests suite.RunAllTests() } // ============================================================ -// 三、币安合约特定功能的单元测试 +// 3. Binance Futures specific unit tests // ============================================================ -// TestNewFuturesTrader 测试创建币安合约交易器 +// TestNewFuturesTrader tests creating Binance Futures trader func TestNewFuturesTrader(t *testing.T) { - // 创建 mock HTTP 服务器 + // Create mock HTTP server mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path @@ -342,10 +342,10 @@ func TestNewFuturesTrader(t *testing.T) { })) defer mockServer.Close() - // 测试成功创建 + // Test successful creation trader := NewFuturesTrader("test_api_key", "test_secret_key", "test_user") - // 修改 client 使用 mock server + // Modify client to use mock server trader.client.BaseURL = mockServer.URL trader.client.HTTPClient = mockServer.Client() @@ -354,7 +354,7 @@ func TestNewFuturesTrader(t *testing.T) { assert.Equal(t, 15*time.Second, trader.cacheDuration) } -// TestCalculatePositionSize 测试仓位计算 +// TestCalculatePositionSize tests position size calculation func TestCalculatePositionSize(t *testing.T) { trader := &FuturesTrader{} @@ -367,7 +367,7 @@ func TestCalculatePositionSize(t *testing.T) { wantQuantity float64 }{ { - name: "正常计算", + name: "normal calculation", balance: 10000, riskPercent: 2, price: 50000, @@ -375,7 +375,7 @@ func TestCalculatePositionSize(t *testing.T) { wantQuantity: 0.04, // (10000 * 0.02 * 10) / 50000 = 0.04 }, { - name: "高杠杆", + name: "high leverage", balance: 10000, riskPercent: 1, price: 3000, @@ -383,7 +383,7 @@ func TestCalculatePositionSize(t *testing.T) { wantQuantity: 0.6667, // (10000 * 0.01 * 20) / 3000 = 0.6667 }, { - name: "低风险", + name: "low risk", balance: 5000, riskPercent: 0.5, price: 50000, @@ -395,26 +395,26 @@ func TestCalculatePositionSize(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { quantity := trader.CalculatePositionSize(tt.balance, tt.riskPercent, tt.price, tt.leverage) - assert.InDelta(t, tt.wantQuantity, quantity, 0.0001, "计算的仓位数量不正确") + assert.InDelta(t, tt.wantQuantity, quantity, 0.0001, "calculated position size is incorrect") }) } } -// TestGetBrOrderID 测试订单ID生成 +// TestGetBrOrderID tests order ID generation func TestGetBrOrderID(t *testing.T) { - // 测试3次,确保每次生成的ID都不同 + // Test 3 times to ensure each generated ID is unique ids := make(map[string]bool) for i := 0; i < 3; i++ { id := getBrOrderID() - // 检查格式 - assert.True(t, strings.HasPrefix(id, "x-KzrpZaP9"), "订单ID应以x-KzrpZaP9开头") + // Check format + assert.True(t, strings.HasPrefix(id, "x-KzrpZaP9"), "order ID should start with x-KzrpZaP9") - // 检查长度(应该 <= 32) - assert.LessOrEqual(t, len(id), 32, "订单ID长度不应超过32字符") + // Check length (should be <= 32) + assert.LessOrEqual(t, len(id), 32, "order ID length should not exceed 32 characters") - // 检查唯一性 - assert.False(t, ids[id], "订单ID应该唯一") + // Check uniqueness + assert.False(t, ids[id], "order ID should be unique") ids[id] = true } } diff --git a/trader/bybit_trader.go b/trader/bybit_trader.go index 4ec9b374..4bf54818 100644 --- a/trader/bybit_trader.go +++ b/trader/bybit_trader.go @@ -16,35 +16,35 @@ import ( bybit "github.com/bybit-exchange/bybit.go.api" ) -// BybitTrader Bybit USDT 永續合約交易器 +// BybitTrader Bybit USDT Perpetual Futures Trader type BybitTrader struct { client *bybit.Client - // 余额缓存 + // Balance cache cachedBalance map[string]interface{} balanceCacheTime time.Time balanceCacheMutex sync.RWMutex - // 持仓缓存 + // Position cache cachedPositions []map[string]interface{} positionsCacheTime time.Time positionsCacheMutex sync.RWMutex - // 交易对精度缓存 (symbol -> qtyStep) + // Trading pair precision cache (symbol -> qtyStep) qtyStepCache map[string]float64 qtyStepCacheMutex sync.RWMutex - // 缓存有效期(15秒) + // Cache duration (15 seconds) cacheDuration time.Duration } -// NewBybitTrader 创建 Bybit 交易器 +// NewBybitTrader creates a Bybit trader func NewBybitTrader(apiKey, secretKey string) *BybitTrader { const src = "Up000938" client := bybit.NewBybitHttpClient(apiKey, secretKey, bybit.WithBaseURL(bybit.MAINNET)) - // 设置 HTTP 传输 + // Set HTTP transport if client != nil && client.HTTPClient != nil { defaultTransport := client.HTTPClient.Transport if defaultTransport == nil { @@ -63,12 +63,12 @@ func NewBybitTrader(apiKey, secretKey string) *BybitTrader { qtyStepCache: make(map[string]float64), } - logger.Infof("🔵 [Bybit] 交易器已初始化") + logger.Infof("🔵 [Bybit] Trader initialized") return trader } -// headerRoundTripper 用于添加自定义 header 的 HTTP RoundTripper +// headerRoundTripper HTTP RoundTripper for adding custom headers type headerRoundTripper struct { base http.RoundTripper refererID string @@ -79,9 +79,9 @@ func (h *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error return h.base.RoundTrip(req) } -// GetBalance 获取账户余额 +// GetBalance retrieves account balance func (t *BybitTrader) GetBalance() (map[string]interface{}, error) { - // 检查缓存 + // Check cache t.balanceCacheMutex.RLock() if t.cachedBalance != nil && time.Since(t.balanceCacheTime) < t.cacheDuration { balance := t.cachedBalance @@ -90,24 +90,24 @@ func (t *BybitTrader) GetBalance() (map[string]interface{}, error) { } t.balanceCacheMutex.RUnlock() - // 调用 API + // Call API params := map[string]interface{}{ "accountType": "UNIFIED", } result, err := t.client.NewUtaBybitServiceWithParams(params).GetAccountWallet(context.Background()) if err != nil { - return nil, fmt.Errorf("获取 Bybit 余额失败: %w", err) + return nil, fmt.Errorf("failed to get Bybit balance: %w", err) } if result.RetCode != 0 { - return nil, fmt.Errorf("Bybit API 错误: %s", result.RetMsg) + return nil, fmt.Errorf("Bybit API error: %s", result.RetMsg) } - // 提取余额信息 + // Extract balance information resultData, ok := result.Result.(map[string]interface{}) if !ok { - return nil, fmt.Errorf("Bybit 余额返回格式错误") + return nil, fmt.Errorf("Bybit balance return format error") } list, _ := resultData["list"].([]interface{}) @@ -122,17 +122,17 @@ func (t *BybitTrader) GetBalance() (map[string]interface{}, error) { if availStr, ok := account["totalAvailableBalance"].(string); ok { availableBalance, _ = strconv.ParseFloat(availStr, 64) } - // Bybit UNIFIED 账户的钱包余额字段 + // Bybit UNIFIED account wallet balance field if walletStr, ok := account["totalWalletBalance"].(string); ok { totalWalletBalance, _ = strconv.ParseFloat(walletStr, 64) } - // Bybit 永续合约未实现盈亏 + // Bybit perpetual contract unrealized PnL if uplStr, ok := account["totalPerpUPL"].(string); ok { totalPerpUPL, _ = strconv.ParseFloat(uplStr, 64) } } - // 如果没有 totalWalletBalance,使用 totalEquity + // If no totalWalletBalance, use totalEquity if totalWalletBalance == 0 { totalWalletBalance = totalEquity } @@ -142,10 +142,10 @@ func (t *BybitTrader) GetBalance() (map[string]interface{}, error) { "totalWalletBalance": totalWalletBalance, "availableBalance": availableBalance, "totalUnrealizedProfit": totalPerpUPL, - "balance": totalEquity, // 兼容其他交易所格式 + "balance": totalEquity, // Compatible with other exchange formats } - // 更新缓存 + // Update cache t.balanceCacheMutex.Lock() t.cachedBalance = balance t.balanceCacheTime = time.Now() @@ -154,9 +154,9 @@ func (t *BybitTrader) GetBalance() (map[string]interface{}, error) { return balance, nil } -// GetPositions 获取所有持仓 +// GetPositions retrieves all positions func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) { - // 检查缓存 + // Check cache t.positionsCacheMutex.RLock() if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration { positions := t.cachedPositions @@ -165,7 +165,7 @@ func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) { } t.positionsCacheMutex.RUnlock() - // 调用 API + // Call API params := map[string]interface{}{ "category": "linear", "settleCoin": "USDT", @@ -173,16 +173,16 @@ func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) { result, err := t.client.NewUtaBybitServiceWithParams(params).GetPositionList(context.Background()) if err != nil { - return nil, fmt.Errorf("获取 Bybit 持仓失败: %w", err) + return nil, fmt.Errorf("failed to get Bybit positions: %w", err) } if result.RetCode != 0 { - return nil, fmt.Errorf("Bybit API 错误: %s", result.RetMsg) + return nil, fmt.Errorf("Bybit API error: %s", result.RetMsg) } resultData, ok := result.Result.(map[string]interface{}) if !ok { - return nil, fmt.Errorf("Bybit 持仓返回格式错误") + return nil, fmt.Errorf("Bybit positions return format error") } list, _ := resultData["list"].([]interface{}) @@ -198,7 +198,7 @@ func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) { sizeStr, _ := pos["size"].(string) size, _ := strconv.ParseFloat(sizeStr, 64) - // 跳过空仓位 + // Skip empty positions if size == 0 { continue } @@ -212,17 +212,17 @@ func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) { leverageStr, _ := pos["leverage"].(string) leverage, _ := strconv.ParseFloat(leverageStr, 64) - // 标记价格 + // Mark price markPriceStr, _ := pos["markPrice"].(string) markPrice, _ := strconv.ParseFloat(markPriceStr, 64) - // 强平价格 + // Liquidation price liqPriceStr, _ := pos["liqPrice"].(string) liqPrice, _ := strconv.ParseFloat(liqPriceStr, 64) positionSide, _ := pos["side"].(string) // Buy = LONG, Sell = SHORT - // 转换为统一格式 + // Convert to unified format side := "LONG" positionAmt := size if positionSide == "Sell" { @@ -245,7 +245,7 @@ func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) { positions = append(positions, position) } - // 更新缓存 + // Update cache t.positionsCacheMutex.Lock() t.cachedPositions = positions t.positionsCacheTime = time.Now() @@ -254,14 +254,14 @@ func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) { return positions, nil } -// OpenLong 开多仓 +// OpenLong opens a long position func (t *BybitTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 先设置杠杆 + // Set leverage first if err := t.SetLeverage(symbol, leverage); err != nil { - logger.Infof("⚠️ [Bybit] 设置杠杆失败: %v", err) + logger.Infof("⚠️ [Bybit] Failed to set leverage: %v", err) } - // 使用 FormatQuantity 格式化数量 + // Use FormatQuantity to format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) params := map[string]interface{}{ @@ -270,28 +270,28 @@ func (t *BybitTrader) OpenLong(symbol string, quantity float64, leverage int) (m "side": "Buy", "orderType": "Market", "qty": qtyStr, - "positionIdx": 0, // 单向持仓模式 + "positionIdx": 0, // One-way position mode } result, err := t.client.NewUtaBybitServiceWithParams(params).PlaceOrder(context.Background()) if err != nil { - return nil, fmt.Errorf("Bybit 开多失败: %w", err) + return nil, fmt.Errorf("Bybit open long failed: %w", err) } - // 清除缓存 + // Clear cache t.clearCache() return t.parseOrderResult(result) } -// OpenShort 开空仓 +// OpenShort opens a short position func (t *BybitTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 先设置杠杆 + // Set leverage first if err := t.SetLeverage(symbol, leverage); err != nil { - logger.Infof("⚠️ [Bybit] 设置杠杆失败: %v", err) + logger.Infof("⚠️ [Bybit] Failed to set leverage: %v", err) } - // 使用 FormatQuantity 格式化数量 + // Use FormatQuantity to format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) params := map[string]interface{}{ @@ -300,23 +300,23 @@ func (t *BybitTrader) OpenShort(symbol string, quantity float64, leverage int) ( "side": "Sell", "orderType": "Market", "qty": qtyStr, - "positionIdx": 0, // 单向持仓模式 + "positionIdx": 0, // One-way position mode } result, err := t.client.NewUtaBybitServiceWithParams(params).PlaceOrder(context.Background()) if err != nil { - return nil, fmt.Errorf("Bybit 开空失败: %w", err) + return nil, fmt.Errorf("Bybit open short failed: %w", err) } - // 清除缓存 + // Clear cache t.clearCache() return t.parseOrderResult(result) } -// CloseLong 平多仓 +// CloseLong closes a long position func (t *BybitTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果 quantity = 0,获取当前持仓数量 + // If quantity = 0, get current position quantity if quantity == 0 { positions, err := t.GetPositions() if err != nil { @@ -331,16 +331,16 @@ func (t *BybitTrader) CloseLong(symbol string, quantity float64) (map[string]int } if quantity <= 0 { - return nil, fmt.Errorf("没有多仓可平") + return nil, fmt.Errorf("no long position to close") } - // 使用 FormatQuantity 格式化数量 + // Use FormatQuantity to format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) params := map[string]interface{}{ "category": "linear", "symbol": symbol, - "side": "Sell", // 平多用 Sell + "side": "Sell", // Close long with Sell "orderType": "Market", "qty": qtyStr, "positionIdx": 0, @@ -349,18 +349,18 @@ func (t *BybitTrader) CloseLong(symbol string, quantity float64) (map[string]int result, err := t.client.NewUtaBybitServiceWithParams(params).PlaceOrder(context.Background()) if err != nil { - return nil, fmt.Errorf("Bybit 平多失败: %w", err) + return nil, fmt.Errorf("Bybit close long failed: %w", err) } - // 清除缓存 + // Clear cache t.clearCache() return t.parseOrderResult(result) } -// CloseShort 平空仓 +// CloseShort closes a short position func (t *BybitTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果 quantity = 0,获取当前持仓数量 + // If quantity = 0, get current position quantity if quantity == 0 { positions, err := t.GetPositions() if err != nil { @@ -368,23 +368,23 @@ func (t *BybitTrader) CloseShort(symbol string, quantity float64) (map[string]in } for _, pos := range positions { if pos["symbol"] == symbol && pos["side"] == "SHORT" { - quantity = -pos["positionAmt"].(float64) // 空仓是负数 + quantity = -pos["positionAmt"].(float64) // Short position is negative break } } } if quantity <= 0 { - return nil, fmt.Errorf("没有空仓可平") + return nil, fmt.Errorf("no short position to close") } - // 使用 FormatQuantity 格式化数量 + // Use FormatQuantity to format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) params := map[string]interface{}{ "category": "linear", "symbol": symbol, - "side": "Buy", // 平空用 Buy + "side": "Buy", // Close short with Buy "orderType": "Market", "qty": qtyStr, "positionIdx": 0, @@ -393,16 +393,16 @@ func (t *BybitTrader) CloseShort(symbol string, quantity float64) (map[string]in result, err := t.client.NewUtaBybitServiceWithParams(params).PlaceOrder(context.Background()) if err != nil { - return nil, fmt.Errorf("Bybit 平空失败: %w", err) + return nil, fmt.Errorf("Bybit close short failed: %w", err) } - // 清除缓存 + // Clear cache t.clearCache() return t.parseOrderResult(result) } -// SetLeverage 设置杠杆 +// SetLeverage sets leverage func (t *BybitTrader) SetLeverage(symbol string, leverage int) error { params := map[string]interface{}{ "category": "linear", @@ -413,25 +413,25 @@ func (t *BybitTrader) SetLeverage(symbol string, leverage int) error { result, err := t.client.NewUtaBybitServiceWithParams(params).SetPositionLeverage(context.Background()) if err != nil { - // 如果杠杆已经是目标值,Bybit 会返回错误,忽略这种情况 + // If leverage is already at target value, Bybit will return an error, ignore this case if strings.Contains(err.Error(), "leverage not modified") { return nil } - return fmt.Errorf("设置杠杆失败: %w", err) + return fmt.Errorf("failed to set leverage: %w", err) } if result.RetCode != 0 && result.RetCode != 110043 { // 110043 = leverage not modified - return fmt.Errorf("设置杠杆失败: %s", result.RetMsg) + return fmt.Errorf("failed to set leverage: %s", result.RetMsg) } return nil } -// SetMarginMode 设置仓位模式 +// SetMarginMode sets position margin mode func (t *BybitTrader) SetMarginMode(symbol string, isCrossMargin bool) error { - tradeMode := 1 // 逐仓 + tradeMode := 1 // Isolated margin if isCrossMargin { - tradeMode = 0 // 全仓 + tradeMode = 0 // Cross margin } params := map[string]interface{}{ @@ -445,17 +445,17 @@ func (t *BybitTrader) SetMarginMode(symbol string, isCrossMargin bool) error { if strings.Contains(err.Error(), "Cross/isolated margin mode is not modified") { return nil } - return fmt.Errorf("设置保证金模式失败: %w", err) + return fmt.Errorf("failed to set margin mode: %w", err) } if result.RetCode != 0 && result.RetCode != 110026 { // already in target mode - return fmt.Errorf("设置保证金模式失败: %s", result.RetMsg) + return fmt.Errorf("failed to set margin mode: %s", result.RetMsg) } return nil } -// GetMarketPrice 获取市场价格 +// GetMarketPrice retrieves market price func (t *BybitTrader) GetMarketPrice(symbol string) (float64, error) { params := map[string]interface{}{ "category": "linear", @@ -464,53 +464,53 @@ func (t *BybitTrader) GetMarketPrice(symbol string) (float64, error) { result, err := t.client.NewUtaBybitServiceWithParams(params).GetMarketTickers(context.Background()) if err != nil { - return 0, fmt.Errorf("获取市场价格失败: %w", err) + return 0, fmt.Errorf("failed to get market price: %w", err) } if result.RetCode != 0 { - return 0, fmt.Errorf("API 错误: %s", result.RetMsg) + return 0, fmt.Errorf("API error: %s", result.RetMsg) } resultData, ok := result.Result.(map[string]interface{}) if !ok { - return 0, fmt.Errorf("返回格式错误") + return 0, fmt.Errorf("return format error") } list, _ := resultData["list"].([]interface{}) if len(list) == 0 { - return 0, fmt.Errorf("未找到 %s 的价格数据", symbol) + return 0, fmt.Errorf("price data not found for %s", symbol) } ticker, _ := list[0].(map[string]interface{}) lastPriceStr, _ := ticker["lastPrice"].(string) lastPrice, err := strconv.ParseFloat(lastPriceStr, 64) if err != nil { - return 0, fmt.Errorf("解析价格失败: %w", err) + return 0, fmt.Errorf("failed to parse price: %w", err) } return lastPrice, nil } -// SetStopLoss 设置止损单 +// SetStopLoss sets stop loss order func (t *BybitTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { - side := "Sell" // LONG 止损用 Sell + side := "Sell" // LONG stop loss uses Sell if positionSide == "SHORT" { - side = "Buy" // SHORT 止损用 Buy + side = "Buy" // SHORT stop loss uses Buy } - // 获取当前价格来确定 triggerDirection + // Get current price to determine triggerDirection currentPrice, err := t.GetMarketPrice(symbol) if err != nil { return err } - triggerDirection := 2 // 价格下跌触发(默认多单止损) + triggerDirection := 2 // Price fall trigger (default long stop loss) if stopPrice > currentPrice { - triggerDirection = 1 // 价格上涨触发(空单止损) + triggerDirection = 1 // Price rise trigger (short stop loss) } - // 使用 FormatQuantity 格式化数量 + // Use FormatQuantity to format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) params := map[string]interface{}{ @@ -527,36 +527,36 @@ func (t *BybitTrader) SetStopLoss(symbol string, positionSide string, quantity, result, err := t.client.NewUtaBybitServiceWithParams(params).PlaceOrder(context.Background()) if err != nil { - return fmt.Errorf("设置止损失败: %w", err) + return fmt.Errorf("failed to set stop loss: %w", err) } if result.RetCode != 0 { - return fmt.Errorf("设置止损失败: %s", result.RetMsg) + return fmt.Errorf("failed to set stop loss: %s", result.RetMsg) } - logger.Infof(" ✓ [Bybit] 止损单已设置: %s @ %.2f", symbol, stopPrice) + logger.Infof(" ✓ [Bybit] Stop loss order set: %s @ %.2f", symbol, stopPrice) return nil } -// SetTakeProfit 设置止盈单 +// SetTakeProfit sets take profit order func (t *BybitTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { - side := "Sell" // LONG 止盈用 Sell + side := "Sell" // LONG take profit uses Sell if positionSide == "SHORT" { - side = "Buy" // SHORT 止盈用 Buy + side = "Buy" // SHORT take profit uses Buy } - // 获取当前价格来确定 triggerDirection + // Get current price to determine triggerDirection currentPrice, err := t.GetMarketPrice(symbol) if err != nil { return err } - triggerDirection := 1 // 价格上涨触发(默认多单止盈) + triggerDirection := 1 // Price rise trigger (default long take profit) if takeProfitPrice < currentPrice { - triggerDirection = 2 // 价格下跌触发(空单止盈) + triggerDirection = 2 // Price fall trigger (short take profit) } - // 使用 FormatQuantity 格式化数量 + // Use FormatQuantity to format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) params := map[string]interface{}{ @@ -573,28 +573,28 @@ func (t *BybitTrader) SetTakeProfit(symbol string, positionSide string, quantity result, err := t.client.NewUtaBybitServiceWithParams(params).PlaceOrder(context.Background()) if err != nil { - return fmt.Errorf("设置止盈失败: %w", err) + return fmt.Errorf("failed to set take profit: %w", err) } if result.RetCode != 0 { - return fmt.Errorf("设置止盈失败: %s", result.RetMsg) + return fmt.Errorf("failed to set take profit: %s", result.RetMsg) } - logger.Infof(" ✓ [Bybit] 止盈单已设置: %s @ %.2f", symbol, takeProfitPrice) + logger.Infof(" ✓ [Bybit] Take profit order set: %s @ %.2f", symbol, takeProfitPrice) return nil } -// CancelStopLossOrders 取消止损单 +// CancelStopLossOrders cancels stop loss orders func (t *BybitTrader) CancelStopLossOrders(symbol string) error { return t.cancelConditionalOrders(symbol, "StopLoss") } -// CancelTakeProfitOrders 取消止盈单 +// CancelTakeProfitOrders cancels take profit orders func (t *BybitTrader) CancelTakeProfitOrders(symbol string) error { return t.cancelConditionalOrders(symbol, "TakeProfit") } -// CancelAllOrders 取消所有挂单 +// CancelAllOrders cancels all pending orders func (t *BybitTrader) CancelAllOrders(symbol string) error { params := map[string]interface{}{ "category": "linear", @@ -603,26 +603,26 @@ func (t *BybitTrader) CancelAllOrders(symbol string) error { _, err := t.client.NewUtaBybitServiceWithParams(params).CancelAllOrders(context.Background()) if err != nil { - return fmt.Errorf("取消所有订单失败: %w", err) + return fmt.Errorf("failed to cancel all orders: %w", err) } return nil } -// CancelStopOrders 取消所有止盈止损单 +// CancelStopOrders cancels all stop loss and take profit orders func (t *BybitTrader) CancelStopOrders(symbol string) error { if err := t.CancelStopLossOrders(symbol); err != nil { - logger.Infof("⚠️ [Bybit] 取消止损单失败: %v", err) + logger.Infof("⚠️ [Bybit] Failed to cancel stop loss orders: %v", err) } if err := t.CancelTakeProfitOrders(symbol); err != nil { - logger.Infof("⚠️ [Bybit] 取消止盈单失败: %v", err) + logger.Infof("⚠️ [Bybit] Failed to cancel take profit orders: %v", err) } return nil } -// getQtyStep 获取交易对的数量步长 +// getQtyStep retrieves the quantity step for a trading pair func (t *BybitTrader) getQtyStep(symbol string) float64 { - // 先检查缓存 + // Check cache first t.qtyStepCacheMutex.RLock() if step, ok := t.qtyStepCache[symbol]; ok { t.qtyStepCacheMutex.RUnlock() @@ -630,12 +630,12 @@ func (t *BybitTrader) getQtyStep(symbol string) float64 { } t.qtyStepCacheMutex.RUnlock() - // 直接调用公开 API 获取合约信息 + // Call public API directly to get contract information url := fmt.Sprintf("https://api.bybit.com/v5/market/instruments-info?category=linear&symbol=%s", symbol) resp, err := http.Get(url) if err != nil { - logger.Infof("⚠️ [Bybit] 获取 %s 精度信息失败: %v", symbol, err) - return 1 // 默认整数 + logger.Infof("⚠️ [Bybit] Failed to get precision info for %s: %v", symbol, err) + return 1 // Default to integer } defer resp.Body.Close() @@ -668,7 +668,7 @@ func (t *BybitTrader) getQtyStep(symbol string) float64 { qtyStep = 1 } - // 缓存结果 + // Cache result t.qtyStepCacheMutex.Lock() t.qtyStepCache[symbol] = qtyStep t.qtyStepCacheMutex.Unlock() @@ -678,15 +678,15 @@ func (t *BybitTrader) getQtyStep(symbol string) float64 { return qtyStep } -// FormatQuantity 格式化数量 +// FormatQuantity formats quantity func (t *BybitTrader) FormatQuantity(symbol string, quantity float64) (string, error) { - // 获取该币种的 qtyStep + // Get qtyStep for this symbol qtyStep := t.getQtyStep(symbol) - // 根据 qtyStep 对齐数量(向下取整到最近的 step) + // Align quantity according to qtyStep (round down to nearest step) alignedQty := math.Floor(quantity/qtyStep) * qtyStep - // 计算需要的小数位数 + // Calculate required decimal places decimals := 0 if qtyStep < 1 { stepStr := strconv.FormatFloat(qtyStep, 'f', -1, 64) @@ -695,14 +695,14 @@ func (t *BybitTrader) FormatQuantity(symbol string, quantity float64) (string, e } } - // 格式化 + // Format format := fmt.Sprintf("%%.%df", decimals) formatted := fmt.Sprintf(format, alignedQty) return formatted, nil } -// 辅助方法 +// Helper methods func (t *BybitTrader) clearCache() { t.balanceCacheMutex.Lock() @@ -716,12 +716,12 @@ func (t *BybitTrader) clearCache() { func (t *BybitTrader) parseOrderResult(result *bybit.ServerResponse) (map[string]interface{}, error) { if result.RetCode != 0 { - return nil, fmt.Errorf("下单失败: %s", result.RetMsg) + return nil, fmt.Errorf("order placement failed: %s", result.RetMsg) } resultData, ok := result.Result.(map[string]interface{}) if !ok { - return nil, fmt.Errorf("返回格式错误") + return nil, fmt.Errorf("return format error") } orderId, _ := resultData["orderId"].(string) @@ -732,7 +732,7 @@ func (t *BybitTrader) parseOrderResult(result *bybit.ServerResponse) (map[string }, nil } -// GetOrderStatus 获取订单状态 +// GetOrderStatus retrieves order status func (t *BybitTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { params := map[string]interface{}{ "category": "linear", @@ -742,26 +742,26 @@ func (t *BybitTrader) GetOrderStatus(symbol string, orderID string) (map[string] result, err := t.client.NewUtaBybitServiceWithParams(params).GetOrderHistory(context.Background()) if err != nil { - return nil, fmt.Errorf("获取订单状态失败: %w", err) + return nil, fmt.Errorf("failed to get order status: %w", err) } if result.RetCode != 0 { - return nil, fmt.Errorf("API 错误: %s", result.RetMsg) + return nil, fmt.Errorf("API error: %s", result.RetMsg) } resultData, ok := result.Result.(map[string]interface{}) if !ok { - return nil, fmt.Errorf("返回格式错误") + return nil, fmt.Errorf("return format error") } list, _ := resultData["list"].([]interface{}) if len(list) == 0 { - return nil, fmt.Errorf("未找到订单 %s", orderID) + return nil, fmt.Errorf("order %s not found", orderID) } order, _ := list[0].(map[string]interface{}) - // 解析订单数据 + // Parse order data status, _ := order["orderStatus"].(string) avgPriceStr, _ := order["avgPrice"].(string) cumExecQtyStr, _ := order["cumExecQty"].(string) @@ -771,7 +771,7 @@ func (t *BybitTrader) GetOrderStatus(symbol string, orderID string) (map[string] executedQty, _ := strconv.ParseFloat(cumExecQtyStr, 64) commission, _ := strconv.ParseFloat(cumExecFeeStr, 64) - // 转换状态为统一格式 + // Convert status to unified format unifiedStatus := status switch status { case "Filled": @@ -794,20 +794,20 @@ func (t *BybitTrader) GetOrderStatus(symbol string, orderID string) (map[string] } func (t *BybitTrader) cancelConditionalOrders(symbol string, orderType string) error { - // 先获取所有条件单 + // First get all conditional orders params := map[string]interface{}{ "category": "linear", "symbol": symbol, - "orderFilter": "StopOrder", // 条件单 + "orderFilter": "StopOrder", // Conditional orders } result, err := t.client.NewUtaBybitServiceWithParams(params).GetOpenOrders(context.Background()) if err != nil { - return fmt.Errorf("获取条件单失败: %w", err) + return fmt.Errorf("failed to get conditional orders: %w", err) } if result.RetCode != 0 { - return nil // 没有订单 + return nil // No orders } resultData, ok := result.Result.(map[string]interface{}) @@ -817,7 +817,7 @@ func (t *BybitTrader) cancelConditionalOrders(symbol string, orderType string) e list, _ := resultData["list"].([]interface{}) - // 取消匹配的订单 + // Cancel matching orders for _, item := range list { order, ok := item.(map[string]interface{}) if !ok { @@ -827,7 +827,7 @@ func (t *BybitTrader) cancelConditionalOrders(symbol string, orderType string) e orderId, _ := order["orderId"].(string) stopOrderType, _ := order["stopOrderType"].(string) - // 根据类型筛选 + // Filter by type shouldCancel := false if orderType == "StopLoss" && (stopOrderType == "StopLoss" || stopOrderType == "Stop") { shouldCancel = true diff --git a/trader/bybit_trader_test.go b/trader/bybit_trader_test.go index 3c816e91..39808014 100644 --- a/trader/bybit_trader_test.go +++ b/trader/bybit_trader_test.go @@ -12,21 +12,21 @@ import ( ) // ============================================================ -// 一、BybitTraderTestSuite - 继承 base test suite +// Part 1: BybitTraderTestSuite - Inherits base test suite // ============================================================ -// BybitTraderTestSuite Bybit交易器测试套件 -// 继承 TraderTestSuite 并添加 Bybit 特定的 mock 逻辑 +// BybitTraderTestSuite Bybit trader test suite +// Inherits TraderTestSuite and adds Bybit-specific mock logic type BybitTraderTestSuite struct { - *TraderTestSuite // 嵌入基础测试套件 + *TraderTestSuite // Embeds base test suite mockServer *httptest.Server } -// NewBybitTraderTestSuite 创建 Bybit 测试套件 -// 注意:由于 Bybit SDK 封装设计,无法轻松注入 mock HTTP client -// 因此这里的测试套件主要用于接口合规性验证,而非 API 调用测试 +// NewBybitTraderTestSuite Create Bybit test suite +// Note: Due to Bybit SDK encapsulation design, cannot easily inject mock HTTP client +// Therefore this test suite is mainly used for interface compliance verification, not API call testing func NewBybitTraderTestSuite(t *testing.T) *BybitTraderTestSuite { - // 创建 mock HTTP 服务器(用于验证响应格式) + // Create mock HTTP server (for response format verification) mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path var respBody interface{} @@ -65,10 +65,10 @@ func NewBybitTraderTestSuite(t *testing.T) *BybitTraderTestSuite { json.NewEncoder(w).Encode(respBody) })) - // 创建真实的 Bybit trader(用于接口合规性测试) + // Create real Bybit trader (for interface compliance testing) trader := NewBybitTrader("test_api_key", "test_secret_key") - // 创建基础套件 + // Create base suite baseSuite := NewTraderTestSuite(t, trader) return &BybitTraderTestSuite{ @@ -77,7 +77,7 @@ func NewBybitTraderTestSuite(t *testing.T) *BybitTraderTestSuite { } } -// Cleanup 清理资源 +// Cleanup Clean up resources func (s *BybitTraderTestSuite) Cleanup() { if s.mockServer != nil { s.mockServer.Close() @@ -86,19 +86,19 @@ func (s *BybitTraderTestSuite) Cleanup() { } // ============================================================ -// 二、接口兼容性测试 +// Part 2: Interface compliance tests // ============================================================ -// TestBybitTrader_InterfaceCompliance 测试接口兼容性 +// TestBybitTrader_InterfaceCompliance Test interface compliance func TestBybitTrader_InterfaceCompliance(t *testing.T) { var _ Trader = (*BybitTrader)(nil) } // ============================================================ -// 三、Bybit 特定功能的单元测试 +// Part 3: Bybit-specific feature unit tests // ============================================================ -// TestNewBybitTrader 测试创建 Bybit 交易器 +// TestNewBybitTrader Test creating Bybit trader func TestNewBybitTrader(t *testing.T) { tests := []struct { name string @@ -107,19 +107,19 @@ func TestNewBybitTrader(t *testing.T) { wantNil bool }{ { - name: "成功创建", + name: "Successfully create", apiKey: "test_api_key", secretKey: "test_secret_key", wantNil: false, }, { - name: "空API Key仍可创建", + name: "Empty API Key can still create", apiKey: "", secretKey: "test_secret_key", wantNil: false, }, { - name: "空Secret Key仍可创建", + name: "Empty Secret Key can still create", apiKey: "test_api_key", secretKey: "", wantNil: false, @@ -140,26 +140,26 @@ func TestNewBybitTrader(t *testing.T) { } } -// TestBybitTrader_SymbolFormat 测试符号格式 +// TestBybitTrader_SymbolFormat Test symbol format func TestBybitTrader_SymbolFormat(t *testing.T) { - // Bybit 使用大写符号格式(如 BTCUSDT) + // Bybit uses uppercase symbol format (e.g. BTCUSDT) tests := []struct { name string symbol string isValid bool }{ { - name: "标准USDT合约", + name: "Standard USDT contract", symbol: "BTCUSDT", isValid: true, }, { - name: "ETH合约", + name: "ETH contract", symbol: "ETHUSDT", isValid: true, }, { - name: "SOL合约", + name: "SOL contract", symbol: "SOLUSDT", isValid: true, }, @@ -167,14 +167,14 @@ func TestBybitTrader_SymbolFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // 验证符号格式正确(全大写,以USDT结尾) + // Verify symbol format is correct (all uppercase, ends with USDT) assert.True(t, tt.symbol == strings.ToUpper(tt.symbol)) assert.True(t, strings.HasSuffix(tt.symbol, "USDT")) }) } } -// TestBybitTrader_FormatQuantity 测试数量格式化 +// TestBybitTrader_FormatQuantity Test quantity formatting func TestBybitTrader_FormatQuantity(t *testing.T) { trader := NewBybitTrader("test", "test") @@ -186,21 +186,21 @@ func TestBybitTrader_FormatQuantity(t *testing.T) { hasError bool }{ { - name: "BTC数量格式化", + name: "BTC quantity formatting", symbol: "BTCUSDT", quantity: 0.12345, - expected: "0.123", // Bybit 默认使用 3 位小数 + expected: "0.123", // Bybit defaults to 3 decimal places hasError: false, }, { - name: "ETH数量格式化", + name: "ETH quantity formatting", symbol: "ETHUSDT", quantity: 1.2345, expected: "1.234", hasError: false, }, { - name: "整数数量", + name: "Integer quantity", symbol: "SOLUSDT", quantity: 10.0, expected: "10.000", @@ -221,7 +221,7 @@ func TestBybitTrader_FormatQuantity(t *testing.T) { } } -// TestBybitTrader_ParseResponse 测试响应解析 +// TestBybitTrader_ParseResponse Test response parsing func TestBybitTrader_ParseResponse(t *testing.T) { tests := []struct { name string @@ -231,20 +231,20 @@ func TestBybitTrader_ParseResponse(t *testing.T) { errContain string }{ { - name: "成功响应", + name: "Success response", retCode: 0, retMsg: "OK", expectErr: false, }, { - name: "API错误", + name: "API error", retCode: 10001, retMsg: "Invalid symbol", expectErr: true, errContain: "Invalid symbol", }, { - name: "权限错误", + name: "Permission error", retCode: 10003, retMsg: "Invalid API key", expectErr: true, @@ -267,7 +267,7 @@ func TestBybitTrader_ParseResponse(t *testing.T) { } } -// checkBybitResponse 检查 Bybit API 响应是否有错误 +// checkBybitResponse Check if Bybit API response has errors func checkBybitResponse(retCode int, retMsg string) error { if retCode != 0 { return &BybitAPIError{ @@ -278,7 +278,7 @@ func checkBybitResponse(retCode int, retMsg string) error { return nil } -// BybitAPIError Bybit API 错误类型 +// BybitAPIError Bybit API error type type BybitAPIError struct { Code int Message string @@ -288,7 +288,7 @@ func (e *BybitAPIError) Error() string { return e.Message } -// TestBybitTrader_PositionSideConversion 测试仓位方向转换 +// TestBybitTrader_PositionSideConversion Test position side conversion func TestBybitTrader_PositionSideConversion(t *testing.T) { tests := []struct { name string @@ -296,17 +296,17 @@ func TestBybitTrader_PositionSideConversion(t *testing.T) { expected string }{ { - name: "Buy转Long", + name: "Buy to Long", side: "Buy", expected: "long", }, { - name: "Sell转Short", + name: "Sell to Short", side: "Sell", expected: "short", }, { - name: "其他值保持不变", + name: "Other values remain unchanged", side: "Unknown", expected: "unknown", }, @@ -320,7 +320,7 @@ func TestBybitTrader_PositionSideConversion(t *testing.T) { } } -// convertBybitSide 转换 Bybit 仓位方向 +// convertBybitSide Convert Bybit position side func convertBybitSide(side string) string { switch side { case "Buy": @@ -332,29 +332,29 @@ func convertBybitSide(side string) string { } } -// TestBybitTrader_CategoryLinear 测试只使用 linear 类别 +// TestBybitTrader_CategoryLinear Test using only linear category func TestBybitTrader_CategoryLinear(t *testing.T) { - // Bybit trader 应该只使用 linear 类别(USDT永续合约) + // Bybit trader should only use linear category (USDT perpetual contracts) trader := NewBybitTrader("test", "test") assert.NotNil(t, trader) - // 验证默认配置 + // Verify default configuration assert.NotNil(t, trader.client) } -// TestBybitTrader_CacheDuration 测试缓存持续时间 +// TestBybitTrader_CacheDuration Test cache duration func TestBybitTrader_CacheDuration(t *testing.T) { trader := NewBybitTrader("test", "test") - // 验证默认缓存时间为15秒 + // Verify default cache time is 15 seconds assert.Equal(t, 15*time.Second, trader.cacheDuration) } // ============================================================ -// 四、Mock 服务器集成测试 +// Part 4: Mock server integration tests // ============================================================ -// TestBybitTrader_MockServerGetBalance 测试通过 Mock 服务器获取余额 +// TestBybitTrader_MockServerGetBalance Test getting balance through Mock server func TestBybitTrader_MockServerGetBalance(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v5/account/wallet-balance" { @@ -386,12 +386,12 @@ func TestBybitTrader_MockServerGetBalance(t *testing.T) { })) defer mockServer.Close() - // 由于 Bybit SDK 封装,无法直接注入 mock URL - // 这个测试验证 mock 服务器响应格式正确 + // Due to Bybit SDK encapsulation, cannot directly inject mock URL + // This test verifies mock server response format is correct assert.NotNil(t, mockServer) } -// TestBybitTrader_MockServerGetPositions 测试通过 Mock 服务器获取持仓 +// TestBybitTrader_MockServerGetPositions Test getting positions through Mock server func TestBybitTrader_MockServerGetPositions(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v5/position/list" { @@ -425,7 +425,7 @@ func TestBybitTrader_MockServerGetPositions(t *testing.T) { assert.NotNil(t, mockServer) } -// TestBybitTrader_MockServerPlaceOrder 测试通过 Mock 服务器下单 +// TestBybitTrader_MockServerPlaceOrder Test placing order through Mock server func TestBybitTrader_MockServerPlaceOrder(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v5/order/create" && r.Method == "POST" { @@ -448,7 +448,7 @@ func TestBybitTrader_MockServerPlaceOrder(t *testing.T) { assert.NotNil(t, mockServer) } -// TestBybitTrader_MockServerSetLeverage 测试通过 Mock 服务器设置杠杆 +// TestBybitTrader_MockServerSetLeverage Test setting leverage through Mock server func TestBybitTrader_MockServerSetLeverage(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v5/position/set-leverage" && r.Method == "POST" { diff --git a/trader/helpers.go b/trader/helpers.go index 2b71624d..47f69fa6 100644 --- a/trader/helpers.go +++ b/trader/helpers.go @@ -5,7 +5,7 @@ import ( "strconv" ) -// SafeFloat64 从map中安全提取float64值 +// SafeFloat64 Safely extract float64 value from map func SafeFloat64(data map[string]interface{}, key string) (float64, error) { value, ok := data[key] if !ok { @@ -22,7 +22,7 @@ func SafeFloat64(data map[string]interface{}, key string) (float64, error) { case int64: return float64(v), nil case string: - // 尝试解析字符串为float64 + // Try to parse string as float64 parsed, err := strconv.ParseFloat(v, 64) if err != nil { return 0, fmt.Errorf("cannot parse string '%s' as float64: %w", v, err) @@ -33,7 +33,7 @@ func SafeFloat64(data map[string]interface{}, key string) (float64, error) { } } -// SafeString 从map中安全提取字符串值 +// SafeString Safely extract string value from map func SafeString(data map[string]interface{}, key string) (string, error) { value, ok := data[key] if !ok { @@ -50,7 +50,7 @@ func SafeString(data map[string]interface{}, key string) (string, error) { } } -// SafeInt 从map中安全提取int值 +// SafeInt Safely extract int value from map func SafeInt(data map[string]interface{}, key string) (int, error) { value, ok := data[key] if !ok { diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index d075c54d..ffde1db2 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -14,28 +14,28 @@ import ( "github.com/sonirico/go-hyperliquid" ) -// HyperliquidTrader Hyperliquid交易器 +// HyperliquidTrader Hyperliquid trader type HyperliquidTrader struct { exchange *hyperliquid.Exchange ctx context.Context walletAddr string - meta *hyperliquid.Meta // 缓存meta信息(包含精度等) - metaMutex sync.RWMutex // 保护meta字段的并发访问 - isCrossMargin bool // 是否为全仓模式 + meta *hyperliquid.Meta // Cache meta information (including precision) + metaMutex sync.RWMutex // Protect concurrent access to meta field + isCrossMargin bool // Whether to use cross margin mode } -// NewHyperliquidTrader 创建Hyperliquid交易器 +// NewHyperliquidTrader creates a Hyperliquid trader func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) (*HyperliquidTrader, error) { - // 去掉私钥的 0x 前缀(如果有,不区分大小写) + // Remove 0x prefix from private key (if present, case-insensitive) privateKeyHex = strings.TrimPrefix(strings.ToLower(privateKeyHex), "0x") - // 解析私钥 + // Parse private key privateKey, err := crypto.HexToECDSA(privateKeyHex) if err != nil { - return nil, fmt.Errorf("解析私钥失败: %w", err) + return nil, fmt.Errorf("failed to parse private key: %w", err) } - // 选择API URL + // Select API URL apiURL := hyperliquid.MainnetAPIURL if testnet { apiURL = hyperliquid.TestnetAPIURL @@ -68,7 +68,7 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) ctx := context.Background() - // 创建Exchange客户端(Exchange包含Info功能) + // Create Exchange client (Exchange includes Info functionality) exchange := hyperliquid.NewExchange( ctx, privateKey, @@ -79,12 +79,12 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) nil, // SpotMeta will be fetched automatically ) - logger.Infof("✓ Hyperliquid交易器初始化成功 (testnet=%v, wallet=%s)", testnet, walletAddr) + logger.Infof("✓ Hyperliquid trader initialized successfully (testnet=%v, wallet=%s)", testnet, walletAddr) - // 获取meta信息(包含精度等配置) + // Get meta information (including precision and other configurations) meta, err := exchange.Info().Meta(ctx) if err != nil { - return nil, fmt.Errorf("获取meta信息失败: %w", err) + return nil, fmt.Errorf("failed to get meta information: %w", err) } // 🔍 Security check: Validate Agent wallet balance (should be close to 0) @@ -125,160 +125,160 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) ctx: ctx, walletAddr: walletAddr, meta: meta, - isCrossMargin: true, // 默认使用全仓模式 + isCrossMargin: true, // Use cross margin mode by default }, nil } -// GetBalance 获取账户余额 +// GetBalance gets account balance func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { - logger.Infof("🔄 正在调用Hyperliquid API获取账户余额...") + logger.Infof("🔄 Calling Hyperliquid API to get account balance...") - // ✅ Step 1: 查询 Spot 现货账户余额 + // ✅ Step 1: Query Spot account balance spotState, err := t.exchange.Info().SpotUserState(t.ctx, t.walletAddr) var spotUSDCBalance float64 = 0.0 if err != nil { - logger.Infof("⚠️ 查询 Spot 余额失败(可能无现货资产): %v", err) + logger.Infof("⚠️ Failed to query Spot balance (may have no spot assets): %v", err) } else if spotState != nil && len(spotState.Balances) > 0 { for _, balance := range spotState.Balances { if balance.Coin == "USDC" { spotUSDCBalance, _ = strconv.ParseFloat(balance.Total, 64) - logger.Infof("✓ 发现 Spot 现货余额: %.2f USDC", spotUSDCBalance) + logger.Infof("✓ Found Spot balance: %.2f USDC", spotUSDCBalance) break } } } - // ✅ Step 2: 查询 Perpetuals 合约账户状态 + // ✅ Step 2: Query Perpetuals contract account status accountState, err := t.exchange.Info().UserState(t.ctx, t.walletAddr) if err != nil { - logger.Infof("❌ Hyperliquid Perpetuals API调用失败: %v", err) - return nil, fmt.Errorf("获取账户信息失败: %w", err) + logger.Infof("❌ Hyperliquid Perpetuals API call failed: %v", err) + return nil, fmt.Errorf("failed to get account information: %w", err) } - // 解析余额信息(MarginSummary字段都是string) + // Parse balance information (MarginSummary fields are all strings) result := make(map[string]interface{}) - // ✅ Step 3: 根据保证金模式动态选择正确的摘要(CrossMarginSummary 或 MarginSummary) + // ✅ Step 3: Dynamically select correct summary based on margin mode (CrossMarginSummary or MarginSummary) var accountValue, totalMarginUsed float64 var summaryType string var summary interface{} if t.isCrossMargin { - // 全仓模式:使用 CrossMarginSummary + // Cross margin mode: use CrossMarginSummary accountValue, _ = strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) totalMarginUsed, _ = strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64) - summaryType = "CrossMarginSummary (全仓)" + summaryType = "CrossMarginSummary (cross margin)" summary = accountState.CrossMarginSummary } else { - // 逐仓模式:使用 MarginSummary + // Isolated margin mode: use MarginSummary accountValue, _ = strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64) totalMarginUsed, _ = strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64) - summaryType = "MarginSummary (逐仓)" + summaryType = "MarginSummary (isolated margin)" summary = accountState.MarginSummary } - // 🔍 调试:打印API返回的完整摘要结构 + // 🔍 Debug: Print complete summary structure returned by API summaryJSON, _ := json.MarshalIndent(summary, " ", " ") - logger.Infof("🔍 [DEBUG] Hyperliquid API %s 完整数据:", summaryType) + logger.Infof("🔍 [DEBUG] Hyperliquid API %s complete data:", summaryType) logger.Infof("%s", string(summaryJSON)) - // ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏 + // ⚠️ Critical fix: Accumulate actual unrealized PnL from all positions totalUnrealizedPnl := 0.0 for _, assetPos := range accountState.AssetPositions { unrealizedPnl, _ := strconv.ParseFloat(assetPos.Position.UnrealizedPnl, 64) totalUnrealizedPnl += unrealizedPnl } - // ✅ 正确理解Hyperliquid字段: - // AccountValue = 总账户净值(已包含空闲资金+持仓价值+未实现盈亏) - // TotalMarginUsed = 持仓占用的保证金(已包含在AccountValue中,仅用于显示) + // ✅ Correctly understand Hyperliquid fields: + // AccountValue = Total account equity (includes idle funds + position value + unrealized PnL) + // TotalMarginUsed = Margin used by positions (included in AccountValue, for display only) // - // 为了兼容auto_trader.go的计算逻辑(totalEquity = totalWalletBalance + totalUnrealizedProfit) - // 需要返回"不包含未实现盈亏的钱包余额" + // To be compatible with auto_trader.go calculation logic (totalEquity = totalWalletBalance + totalUnrealizedProfit) + // Need to return "wallet balance without unrealized PnL" walletBalanceWithoutUnrealized := accountValue - totalUnrealizedPnl - // ✅ Step 4: 使用 Withdrawable 欄位(PR #443) - // Withdrawable 是官方提供的真实可提现余额,比简单计算更可靠 + // ✅ Step 4: Use Withdrawable field (PR #443) + // Withdrawable is the official real withdrawable balance, more reliable than simple calculation availableBalance := 0.0 if accountState.Withdrawable != "" { withdrawable, err := strconv.ParseFloat(accountState.Withdrawable, 64) if err == nil && withdrawable > 0 { availableBalance = withdrawable - logger.Infof("✓ 使用 Withdrawable 作为可用余额: %.2f", availableBalance) + logger.Infof("✓ Using Withdrawable as available balance: %.2f", availableBalance) } } - // 降级方案:如果没有 Withdrawable,使用简单计算 + // Fallback: If no Withdrawable, use simple calculation if availableBalance == 0 && accountState.Withdrawable == "" { availableBalance = accountValue - totalMarginUsed if availableBalance < 0 { - logger.Infof("⚠️ 计算出的可用余额为负数 (%.2f),重置为 0", availableBalance) + logger.Infof("⚠️ Calculated available balance is negative (%.2f), reset to 0", availableBalance) availableBalance = 0 } } - // ✅ Step 5: 正确处理 Spot + Perpetuals 余额 - // 重要:Spot 只加到总资产,不加到可用余额 - // 原因:Spot 和 Perpetuals 是独立帐户,需手动 ClassTransfer 才能转账 + // ✅ Step 5: Correctly handle Spot + Perpetuals balance + // Important: Spot is only added to total assets, not to available balance + // Reason: Spot and Perpetuals are independent accounts, manual ClassTransfer required for transfers totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance - result["totalWalletBalance"] = totalWalletBalance // 总资产(Perp + Spot) - result["availableBalance"] = availableBalance // 可用余额(仅 Perpetuals,不含 Spot) - result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏(仅来自 Perpetuals) - result["spotBalance"] = spotUSDCBalance // Spot 现货余额(单独返回) + result["totalWalletBalance"] = totalWalletBalance // Total assets (Perp + Spot) + result["availableBalance"] = availableBalance // Available balance (Perpetuals only, excluding Spot) + result["totalUnrealizedProfit"] = totalUnrealizedPnl // Unrealized PnL (from Perpetuals only) + result["spotBalance"] = spotUSDCBalance // Spot balance (returned separately) - logger.Infof("✓ Hyperliquid 完整账户:") - logger.Infof(" • Spot 现货余额: %.2f USDC (需手动转账到 Perpetuals 才能开仓)", spotUSDCBalance) - logger.Infof(" • Perpetuals 合约净值: %.2f USDC (钱包%.2f + 未实现%.2f)", + logger.Infof("✓ Hyperliquid complete account:") + logger.Infof(" • Spot balance: %.2f USDC (manual transfer to Perpetuals required for opening positions)", spotUSDCBalance) + logger.Infof(" • Perpetuals equity: %.2f USDC (wallet %.2f + unrealized %.2f)", accountValue, walletBalanceWithoutUnrealized, totalUnrealizedPnl) - logger.Infof(" • Perpetuals 可用余额: %.2f USDC (可直接用于开仓)", availableBalance) - logger.Infof(" • 保证金占用: %.2f USDC", totalMarginUsed) - logger.Infof(" • 总资产 (Perp+Spot): %.2f USDC", totalWalletBalance) - logger.Infof(" ⭐ 总资产: %.2f USDC | Perp 可用: %.2f USDC | Spot 余额: %.2f USDC", + logger.Infof(" • Perpetuals available balance: %.2f USDC (directly usable for opening positions)", availableBalance) + logger.Infof(" • Margin used: %.2f USDC", totalMarginUsed) + logger.Infof(" • Total assets (Perp+Spot): %.2f USDC", totalWalletBalance) + logger.Infof(" ⭐ Total assets: %.2f USDC | Perp available: %.2f USDC | Spot balance: %.2f USDC", totalWalletBalance, availableBalance, spotUSDCBalance) return result, nil } -// GetPositions 获取所有持仓 +// GetPositions gets all positions func (t *HyperliquidTrader) GetPositions() ([]map[string]interface{}, error) { - // 获取账户状态 + // Get account status accountState, err := t.exchange.Info().UserState(t.ctx, t.walletAddr) if err != nil { - return nil, fmt.Errorf("获取持仓失败: %w", err) + return nil, fmt.Errorf("failed to get positions: %w", err) } var result []map[string]interface{} - // 遍历所有持仓 + // Iterate through all positions for _, assetPos := range accountState.AssetPositions { position := assetPos.Position - // 持仓数量(string类型) + // Position amount (string type) posAmt, _ := strconv.ParseFloat(position.Szi, 64) if posAmt == 0 { - continue // 跳过无持仓的 + continue // Skip positions with zero amount } posMap := make(map[string]interface{}) - // 标准化symbol格式(Hyperliquid使用如"BTC",我们转换为"BTCUSDT") + // Normalize symbol format (Hyperliquid uses "BTC", we convert to "BTCUSDT") symbol := position.Coin + "USDT" posMap["symbol"] = symbol - // 持仓数量和方向 + // Position amount and direction if posAmt > 0 { posMap["side"] = "long" posMap["positionAmt"] = posAmt } else { posMap["side"] = "short" - posMap["positionAmt"] = -posAmt // 转为正数 + posMap["positionAmt"] = -posAmt // Convert to positive number } - // 价格信息(EntryPx和LiquidationPx是指针类型) + // Price information (EntryPx and LiquidationPx are pointer types) var entryPrice, liquidationPx float64 if position.EntryPx != nil { entryPrice, _ = strconv.ParseFloat(*position.EntryPx, 64) @@ -290,7 +290,7 @@ func (t *HyperliquidTrader) GetPositions() ([]map[string]interface{}, error) { positionValue, _ := strconv.ParseFloat(position.PositionValue, 64) unrealizedPnl, _ := strconv.ParseFloat(position.UnrealizedPnl, 64) - // 计算mark price(positionValue / abs(posAmt)) + // Calculate mark price (positionValue / abs(posAmt)) var markPrice float64 if posAmt != 0 { markPrice = positionValue / absFloat(posAmt) @@ -308,107 +308,107 @@ func (t *HyperliquidTrader) GetPositions() ([]map[string]interface{}, error) { return result, nil } -// SetMarginMode 设置仓位模式 (在SetLeverage时一并设置) +// SetMarginMode sets margin mode (set together with SetLeverage) func (t *HyperliquidTrader) SetMarginMode(symbol string, isCrossMargin bool) error { - // Hyperliquid的仓位模式在SetLeverage时设置,这里只记录 + // Hyperliquid's margin mode is set in SetLeverage, only record here t.isCrossMargin = isCrossMargin - marginModeStr := "全仓" + marginModeStr := "cross margin" if !isCrossMargin { - marginModeStr = "逐仓" + marginModeStr = "isolated margin" } - logger.Infof(" ✓ %s 将使用 %s 模式", symbol, marginModeStr) + logger.Infof(" ✓ %s will use %s mode", symbol, marginModeStr) return nil } -// SetLeverage 设置杠杆 +// SetLeverage sets leverage func (t *HyperliquidTrader) SetLeverage(symbol string, leverage int) error { - // Hyperliquid symbol格式(去掉USDT后缀) + // Hyperliquid symbol format (remove USDT suffix) coin := convertSymbolToHyperliquid(symbol) - // 调用UpdateLeverage (leverage int, name string, isCross bool) - // 第三个参数: true=全仓模式, false=逐仓模式 + // Call UpdateLeverage (leverage int, name string, isCross bool) + // Third parameter: true=cross margin mode, false=isolated margin mode _, err := t.exchange.UpdateLeverage(t.ctx, leverage, coin, t.isCrossMargin) if err != nil { - return fmt.Errorf("设置杠杆失败: %w", err) + return fmt.Errorf("failed to set leverage: %w", err) } - logger.Infof(" ✓ %s 杠杆已切换为 %dx", symbol, leverage) + logger.Infof(" ✓ %s leverage switched to %dx", symbol, leverage) return nil } -// refreshMetaIfNeeded 当 Meta 信息失效时刷新(Asset ID 为 0 时触发) +// refreshMetaIfNeeded refreshes meta information when invalid (triggered when Asset ID is 0) func (t *HyperliquidTrader) refreshMetaIfNeeded(coin string) error { assetID := t.exchange.Info().NameToAsset(coin) if assetID != 0 { - return nil // Meta 正常,无需刷新 + return nil // Meta is normal, no refresh needed } - logger.Infof("⚠️ %s 的 Asset ID 为 0,尝试刷新 Meta 信息...", coin) + logger.Infof("⚠️ Asset ID for %s is 0, attempting to refresh Meta information...", coin) - // 刷新 Meta 信息 + // Refresh Meta information meta, err := t.exchange.Info().Meta(t.ctx) if err != nil { - return fmt.Errorf("刷新 Meta 信息失败: %w", err) + return fmt.Errorf("failed to refresh Meta information: %w", err) } - // ✅ 并发安全:使用写锁保护 meta 字段更新 + // ✅ Concurrency safe: Use write lock to protect meta field update t.metaMutex.Lock() t.meta = meta t.metaMutex.Unlock() - logger.Infof("✅ Meta 信息已刷新,包含 %d 个资产", len(meta.Universe)) + logger.Infof("✅ Meta information refreshed, contains %d assets", len(meta.Universe)) - // 验证刷新后的 Asset ID + // Verify Asset ID after refresh assetID = t.exchange.Info().NameToAsset(coin) if assetID == 0 { - return fmt.Errorf("❌ 即使在刷新 Meta 后,资产 %s 的 Asset ID 仍为 0。可能原因:\n"+ - " 1. 该币种未在 Hyperliquid 上市\n"+ - " 2. 币种名称错误(应为 BTC 而非 BTCUSDT)\n"+ - " 3. API 连接问题", coin) + return fmt.Errorf("❌ Even after refreshing Meta, Asset ID for %s is still 0. Possible reasons:\n"+ + " 1. This coin is not listed on Hyperliquid\n"+ + " 2. Coin name is incorrect (should be BTC not BTCUSDT)\n"+ + " 3. API connection issue", coin) } - logger.Infof("✅ 刷新后 Asset ID 检查通过: %s -> %d", coin, assetID) + logger.Infof("✅ Asset ID check passed after refresh: %s -> %d", coin, assetID) return nil } -// OpenLong 开多仓 +// OpenLong opens a long position func (t *HyperliquidTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 先取消该币种的所有委托单 + // First cancel all pending orders for this coin if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消旧委托单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel old pending orders: %v", err) } - // 设置杠杆 + // Set leverage if err := t.SetLeverage(symbol, leverage); err != nil { return nil, err } - // Hyperliquid symbol格式 + // Hyperliquid symbol format coin := convertSymbolToHyperliquid(symbol) - // 获取当前价格(用于市价单) + // Get current price (for market order) price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } - // ⚠️ 关键:根据币种精度要求,四舍五入数量 + // ⚠️ Critical: Round quantity according to coin precision requirements roundedQuantity := t.roundToSzDecimals(coin, quantity) - logger.Infof(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin)) + logger.Infof(" 📏 Quantity precision handling: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin)) - // ⚠️ 关键:价格也需要处理为5位有效数字 + // ⚠️ Critical: Price also needs to be processed to 5 significant figures aggressivePrice := t.roundPriceToSigfigs(price * 1.01) - logger.Infof(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*1.01, aggressivePrice) + logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*1.01, aggressivePrice) - // 创建市价买入订单(使用IOC limit order with aggressive price) + // Create market buy order (using IOC limit order with aggressive price) order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: true, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: aggressivePrice, // 使用处理后的价格 + Size: roundedQuantity, // Use rounded quantity + Price: aggressivePrice, // Use processed price OrderType: hyperliquid.OrderType{ Limit: &hyperliquid.LimitOrderType{ - Tif: hyperliquid.TifIoc, // Immediate or Cancel (类似市价单) + Tif: hyperliquid.TifIoc, // Immediate or Cancel (similar to market order) }, }, ReduceOnly: false, @@ -416,54 +416,54 @@ func (t *HyperliquidTrader) OpenLong(symbol string, quantity float64, leverage i _, err = t.exchange.Order(t.ctx, order, nil) if err != nil { - return nil, fmt.Errorf("开多仓失败: %w", err) + return nil, fmt.Errorf("failed to open long position: %w", err) } - logger.Infof("✓ 开多仓成功: %s 数量: %.4f", symbol, roundedQuantity) + logger.Infof("✓ Long position opened successfully: %s quantity: %.4f", symbol, roundedQuantity) result := make(map[string]interface{}) - result["orderId"] = 0 // Hyperliquid没有返回order ID + result["orderId"] = 0 // Hyperliquid does not return order ID result["symbol"] = symbol result["status"] = "FILLED" return result, nil } -// OpenShort 开空仓 +// OpenShort opens a short position func (t *HyperliquidTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 先取消该币种的所有委托单 + // First cancel all pending orders for this coin if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消旧委托单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel old pending orders: %v", err) } - // 设置杠杆 + // Set leverage if err := t.SetLeverage(symbol, leverage); err != nil { return nil, err } - // Hyperliquid symbol格式 + // Hyperliquid symbol format coin := convertSymbolToHyperliquid(symbol) - // 获取当前价格 + // Get current price price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } - // ⚠️ 关键:根据币种精度要求,四舍五入数量 + // ⚠️ Critical: Round quantity according to coin precision requirements roundedQuantity := t.roundToSzDecimals(coin, quantity) - logger.Infof(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin)) + logger.Infof(" 📏 Quantity precision handling: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin)) - // ⚠️ 关键:价格也需要处理为5位有效数字 + // ⚠️ Critical: Price also needs to be processed to 5 significant figures aggressivePrice := t.roundPriceToSigfigs(price * 0.99) - logger.Infof(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*0.99, aggressivePrice) + logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*0.99, aggressivePrice) - // 创建市价卖出订单 + // Create market sell order order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: false, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: aggressivePrice, // 使用处理后的价格 + Size: roundedQuantity, // Use rounded quantity + Price: aggressivePrice, // Use processed price OrderType: hyperliquid.OrderType{ Limit: &hyperliquid.LimitOrderType{ Tif: hyperliquid.TifIoc, @@ -474,10 +474,10 @@ func (t *HyperliquidTrader) OpenShort(symbol string, quantity float64, leverage _, err = t.exchange.Order(t.ctx, order, nil) if err != nil { - return nil, fmt.Errorf("开空仓失败: %w", err) + return nil, fmt.Errorf("failed to open short position: %w", err) } - logger.Infof("✓ 开空仓成功: %s 数量: %.4f", symbol, roundedQuantity) + logger.Infof("✓ Short position opened successfully: %s quantity: %.4f", symbol, roundedQuantity) result := make(map[string]interface{}) result["orderId"] = 0 @@ -487,9 +487,9 @@ func (t *HyperliquidTrader) OpenShort(symbol string, quantity float64, leverage return result, nil } -// CloseLong 平多仓 +// CloseLong closes a long position func (t *HyperliquidTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果数量为0,获取当前持仓数量 + // If quantity is 0, get current position quantity if quantity == 0 { positions, err := t.GetPositions() if err != nil { @@ -504,51 +504,51 @@ func (t *HyperliquidTrader) CloseLong(symbol string, quantity float64) (map[stri } if quantity == 0 { - return nil, fmt.Errorf("没有找到 %s 的多仓", symbol) + return nil, fmt.Errorf("no long position found for %s", symbol) } } - // Hyperliquid symbol格式 + // Hyperliquid symbol format coin := convertSymbolToHyperliquid(symbol) - // 获取当前价格 + // Get current price price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } - // ⚠️ 关键:根据币种精度要求,四舍五入数量 + // ⚠️ Critical: Round quantity according to coin precision requirements roundedQuantity := t.roundToSzDecimals(coin, quantity) - logger.Infof(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin)) + logger.Infof(" 📏 Quantity precision handling: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin)) - // ⚠️ 关键:价格也需要处理为5位有效数字 + // ⚠️ Critical: Price also needs to be processed to 5 significant figures aggressivePrice := t.roundPriceToSigfigs(price * 0.99) - logger.Infof(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*0.99, aggressivePrice) + logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*0.99, aggressivePrice) - // 创建平仓订单(卖出 + ReduceOnly) + // Create close position order (sell + ReduceOnly) order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: false, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: aggressivePrice, // 使用处理后的价格 + Size: roundedQuantity, // Use rounded quantity + Price: aggressivePrice, // Use processed price OrderType: hyperliquid.OrderType{ Limit: &hyperliquid.LimitOrderType{ Tif: hyperliquid.TifIoc, }, }, - ReduceOnly: true, // 只平仓,不开新仓 + ReduceOnly: true, // Only close position, don't open new position } _, err = t.exchange.Order(t.ctx, order, nil) if err != nil { - return nil, fmt.Errorf("平多仓失败: %w", err) + return nil, fmt.Errorf("failed to close long position: %w", err) } - logger.Infof("✓ 平多仓成功: %s 数量: %.4f", symbol, roundedQuantity) + logger.Infof("✓ Long position closed successfully: %s quantity: %.4f", symbol, roundedQuantity) - // 平仓后取消该币种的所有挂单 + // Cancel all pending orders for this coin after closing position if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } result := make(map[string]interface{}) @@ -559,9 +559,9 @@ func (t *HyperliquidTrader) CloseLong(symbol string, quantity float64) (map[stri return result, nil } -// CloseShort 平空仓 +// CloseShort closes a short position func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果数量为0,获取当前持仓数量 + // If quantity is 0, get current position quantity if quantity == 0 { positions, err := t.GetPositions() if err != nil { @@ -576,33 +576,33 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str } if quantity == 0 { - return nil, fmt.Errorf("没有找到 %s 的空仓", symbol) + return nil, fmt.Errorf("no short position found for %s", symbol) } } - // Hyperliquid symbol格式 + // Hyperliquid symbol format coin := convertSymbolToHyperliquid(symbol) - // 获取当前价格 + // Get current price price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } - // ⚠️ 关键:根据币种精度要求,四舍五入数量 + // ⚠️ Critical: Round quantity according to coin precision requirements roundedQuantity := t.roundToSzDecimals(coin, quantity) - logger.Infof(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin)) + logger.Infof(" 📏 Quantity precision handling: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin)) - // ⚠️ 关键:价格也需要处理为5位有效数字 + // ⚠️ Critical: Price also needs to be processed to 5 significant figures aggressivePrice := t.roundPriceToSigfigs(price * 1.01) - logger.Infof(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*1.01, aggressivePrice) + logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*1.01, aggressivePrice) - // 创建平仓订单(买入 + ReduceOnly) + // Create close position order (buy + ReduceOnly) order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: true, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: aggressivePrice, // 使用处理后的价格 + Size: roundedQuantity, // Use rounded quantity + Price: aggressivePrice, // Use processed price OrderType: hyperliquid.OrderType{ Limit: &hyperliquid.LimitOrderType{ Tif: hyperliquid.TifIoc, @@ -613,14 +613,14 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str _, err = t.exchange.Order(t.ctx, order, nil) if err != nil { - return nil, fmt.Errorf("平空仓失败: %w", err) + return nil, fmt.Errorf("failed to close short position: %w", err) } - logger.Infof("✓ 平空仓成功: %s 数量: %.4f", symbol, roundedQuantity) + logger.Infof("✓ Short position closed successfully: %s quantity: %.4f", symbol, roundedQuantity) - // 平仓后取消该币种的所有挂单 + // Cancel all pending orders for this coin after closing position if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } result := make(map[string]interface{}) @@ -631,67 +631,65 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str return result, nil } -// CancelStopOrders 取消该币种的止盈/止 - -// CancelStopLossOrders 仅取消止损单(Hyperliquid 暂无法区分止损和止盈,取消所有) +// CancelStopLossOrders only cancels stop loss orders (Hyperliquid cannot distinguish stop loss and take profit, cancel all) func (t *HyperliquidTrader) CancelStopLossOrders(symbol string) error { - // Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 - // 无法区分止损和止盈单,因此取消该币种的所有挂单 - logger.Infof(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单") + // Hyperliquid SDK's OpenOrder structure does not expose trigger field + // Cannot distinguish stop loss and take profit orders, so cancel all pending orders for this coin + logger.Infof(" ⚠️ Hyperliquid cannot distinguish stop loss/take profit orders, will cancel all pending orders") return t.CancelStopOrders(symbol) } -// CancelTakeProfitOrders 仅取消止盈单(Hyperliquid 暂无法区分止损和止盈,取消所有) +// CancelTakeProfitOrders only cancels take profit orders (Hyperliquid cannot distinguish stop loss and take profit, cancel all) func (t *HyperliquidTrader) CancelTakeProfitOrders(symbol string) error { - // Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 - // 无法区分止损和止盈单,因此取消该币种的所有挂单 - logger.Infof(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单") + // Hyperliquid SDK's OpenOrder structure does not expose trigger field + // Cannot distinguish stop loss and take profit orders, so cancel all pending orders for this coin + logger.Infof(" ⚠️ Hyperliquid cannot distinguish stop loss/take profit orders, will cancel all pending orders") return t.CancelStopOrders(symbol) } -// CancelAllOrders 取消该币种的所有挂单 +// CancelAllOrders cancels all pending orders for this coin func (t *HyperliquidTrader) CancelAllOrders(symbol string) error { coin := convertSymbolToHyperliquid(symbol) - // 获取所有挂单 + // Get all pending orders openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr) if err != nil { - return fmt.Errorf("获取挂单失败: %w", err) + return fmt.Errorf("failed to get pending orders: %w", err) } - // 取消该币种的所有挂单 + // Cancel all pending orders for this coin for _, order := range openOrders { if order.Coin == coin { _, err := t.exchange.Cancel(t.ctx, coin, order.Oid) if err != nil { - logger.Infof(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err) + logger.Infof(" ⚠ Failed to cancel order (oid=%d): %v", order.Oid, err) } } } - logger.Infof(" ✓ 已取消 %s 的所有挂单", symbol) + logger.Infof(" ✓ Cancelled all pending orders for %s", symbol) return nil } -// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +// CancelStopOrders cancels take profit/stop loss orders for this coin (used to adjust TP/SL positions) func (t *HyperliquidTrader) CancelStopOrders(symbol string) error { coin := convertSymbolToHyperliquid(symbol) - // 获取所有挂单 + // Get all pending orders openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr) if err != nil { - return fmt.Errorf("获取挂单失败: %w", err) + return fmt.Errorf("failed to get pending orders: %w", err) } - // 注意:Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 - // 因此暂时取消该币种的所有挂单(包括止盈止损单) - // 这是安全的,因为在设置新的止盈止损之前,应该清理所有旧订单 + // Note: Hyperliquid SDK's OpenOrder structure does not expose trigger field + // Therefore temporarily cancel all pending orders for this coin (including TP/SL orders) + // This is safe because all old orders should be cleaned up before setting new TP/SL canceledCount := 0 for _, order := range openOrders { if order.Coin == coin { _, err := t.exchange.Cancel(t.ctx, coin, order.Oid) if err != nil { - logger.Infof(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err) + logger.Infof(" ⚠ Failed to cancel order (oid=%d): %v", order.Oid, err) continue } canceledCount++ @@ -699,54 +697,54 @@ func (t *HyperliquidTrader) CancelStopOrders(symbol string) error { } if canceledCount == 0 { - logger.Infof(" ℹ %s 没有挂单需要取消", symbol) + logger.Infof(" ℹ No pending orders to cancel for %s", symbol) } else { - logger.Infof(" ✓ 已取消 %s 的 %d 个挂单(包括止盈/止损单)", symbol, canceledCount) + logger.Infof(" ✓ Cancelled %d pending orders for %s (including TP/SL orders)", canceledCount, symbol) } return nil } -// GetMarketPrice 获取市场价格 +// GetMarketPrice gets market price func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) { coin := convertSymbolToHyperliquid(symbol) - // 获取所有市场价格 + // Get all market prices allMids, err := t.exchange.Info().AllMids(t.ctx) if err != nil { - return 0, fmt.Errorf("获取价格失败: %w", err) + return 0, fmt.Errorf("failed to get price: %w", err) } - // 查找对应币种的价格(allMids是map[string]string) + // Find price for corresponding coin (allMids is map[string]string) if priceStr, ok := allMids[coin]; ok { priceFloat, err := strconv.ParseFloat(priceStr, 64) if err == nil { return priceFloat, nil } - return 0, fmt.Errorf("价格格式错误: %v", err) + return 0, fmt.Errorf("price format error: %v", err) } - return 0, fmt.Errorf("未找到 %s 的价格", symbol) + return 0, fmt.Errorf("price not found for %s", symbol) } -// SetStopLoss 设置止损单 +// SetStopLoss sets stop loss order func (t *HyperliquidTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { coin := convertSymbolToHyperliquid(symbol) - isBuy := positionSide == "SHORT" // 空仓止损=买入,多仓止损=卖出 + isBuy := positionSide == "SHORT" // Short position stop loss = buy, long position stop loss = sell - // ⚠️ 关键:根据币种精度要求,四舍五入数量 + // ⚠️ Critical: Round quantity according to coin precision requirements roundedQuantity := t.roundToSzDecimals(coin, quantity) - // ⚠️ 关键:价格也需要处理为5位有效数字 + // ⚠️ Critical: Price also needs to be processed to 5 significant figures roundedStopPrice := t.roundPriceToSigfigs(stopPrice) - // 创建止损单(Trigger Order) + // Create stop loss order (Trigger Order) order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: isBuy, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: roundedStopPrice, // 使用处理后的价格 + Size: roundedQuantity, // Use rounded quantity + Price: roundedStopPrice, // Use processed price OrderType: hyperliquid.OrderType{ Trigger: &hyperliquid.TriggerOrderType{ TriggerPx: roundedStopPrice, @@ -759,31 +757,31 @@ func (t *HyperliquidTrader) SetStopLoss(symbol string, positionSide string, quan _, err := t.exchange.Order(t.ctx, order, nil) if err != nil { - return fmt.Errorf("设置止损失败: %w", err) + return fmt.Errorf("failed to set stop loss: %w", err) } - logger.Infof(" 止损价设置: %.4f", roundedStopPrice) + logger.Infof(" Stop loss price set: %.4f", roundedStopPrice) return nil } -// SetTakeProfit 设置止盈单 +// SetTakeProfit sets take profit order func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { coin := convertSymbolToHyperliquid(symbol) - isBuy := positionSide == "SHORT" // 空仓止盈=买入,多仓止盈=卖出 + isBuy := positionSide == "SHORT" // Short position take profit = buy, long position take profit = sell - // ⚠️ 关键:根据币种精度要求,四舍五入数量 + // ⚠️ Critical: Round quantity according to coin precision requirements roundedQuantity := t.roundToSzDecimals(coin, quantity) - // ⚠️ 关键:价格也需要处理为5位有效数字 + // ⚠️ Critical: Price also needs to be processed to 5 significant figures roundedTakeProfitPrice := t.roundPriceToSigfigs(takeProfitPrice) - // 创建止盈单(Trigger Order) + // Create take profit order (Trigger Order) order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: isBuy, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: roundedTakeProfitPrice, // 使用处理后的价格 + Size: roundedQuantity, // Use rounded quantity + Price: roundedTakeProfitPrice, // Use processed price OrderType: hyperliquid.OrderType{ Trigger: &hyperliquid.TriggerOrderType{ TriggerPx: roundedTakeProfitPrice, @@ -796,69 +794,69 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu _, err := t.exchange.Order(t.ctx, order, nil) if err != nil { - return fmt.Errorf("设置止盈失败: %w", err) + return fmt.Errorf("failed to set take profit: %w", err) } - logger.Infof(" 止盈价设置: %.4f", roundedTakeProfitPrice) + logger.Infof(" Take profit price set: %.4f", roundedTakeProfitPrice) return nil } -// FormatQuantity 格式化数量到正确的精度 +// FormatQuantity formats quantity to correct precision func (t *HyperliquidTrader) FormatQuantity(symbol string, quantity float64) (string, error) { coin := convertSymbolToHyperliquid(symbol) szDecimals := t.getSzDecimals(coin) - // 使用szDecimals格式化数量 + // Format quantity using szDecimals formatStr := fmt.Sprintf("%%.%df", szDecimals) return fmt.Sprintf(formatStr, quantity), nil } -// getSzDecimals 获取币种的数量精度 +// getSzDecimals gets quantity precision for coin func (t *HyperliquidTrader) getSzDecimals(coin string) int { - // ✅ 并发安全:使用读锁保护 meta 字段访问 + // ✅ Concurrency safe: Use read lock to protect meta field access t.metaMutex.RLock() defer t.metaMutex.RUnlock() if t.meta == nil { - logger.Infof("⚠️ meta信息为空,使用默认精度4") - return 4 // 默认精度 + logger.Infof("⚠️ meta information is empty, using default precision 4") + return 4 // Default precision } - // 在meta.Universe中查找对应的币种 + // Find corresponding coin in meta.Universe for _, asset := range t.meta.Universe { if asset.Name == coin { return asset.SzDecimals } } - logger.Infof("⚠️ 未找到 %s 的精度信息,使用默认精度4", coin) - return 4 // 默认精度 + logger.Infof("⚠️ Precision information not found for %s, using default precision 4", coin) + return 4 // Default precision } -// roundToSzDecimals 将数量四舍五入到正确的精度 +// roundToSzDecimals rounds quantity to correct precision func (t *HyperliquidTrader) roundToSzDecimals(coin string, quantity float64) float64 { szDecimals := t.getSzDecimals(coin) - // 计算倍数(10^szDecimals) + // Calculate multiplier (10^szDecimals) multiplier := 1.0 for i := 0; i < szDecimals; i++ { multiplier *= 10.0 } - // 四舍五入 + // Round return float64(int(quantity*multiplier+0.5)) / multiplier } -// roundPriceToSigfigs 将价格四舍五入到5位有效数字 -// Hyperliquid要求价格使用5位有效数字(significant figures) +// roundPriceToSigfigs rounds price to 5 significant figures +// Hyperliquid requires prices to use 5 significant figures func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 { if price == 0 { return 0 } - const sigfigs = 5 // Hyperliquid标准:5位有效数字 + const sigfigs = 5 // Hyperliquid standard: 5 significant figures - // 计算价格的数量级 + // Calculate price magnitude var magnitude float64 if price < 0 { magnitude = -price @@ -866,7 +864,7 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 { magnitude = price } - // 计算需要的倍数 + // Calculate required multiplier multiplier := 1.0 for magnitude >= 10 { magnitude /= 10 @@ -877,39 +875,39 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 { multiplier *= 10 } - // 应用有效数字精度 + // Apply significant figures precision for i := 0; i < sigfigs-1; i++ { multiplier *= 10 } - // 四舍五入 + // Round rounded := float64(int(price*multiplier+0.5)) / multiplier return rounded } -// convertSymbolToHyperliquid 将标准symbol转换为Hyperliquid格式 -// 例如: "BTCUSDT" -> "BTC" +// convertSymbolToHyperliquid converts standard symbol to Hyperliquid format +// Example: "BTCUSDT" -> "BTC" func convertSymbolToHyperliquid(symbol string) string { - // 去掉USDT后缀 + // Remove USDT suffix if len(symbol) > 4 && symbol[len(symbol)-4:] == "USDT" { return symbol[:len(symbol)-4] } return symbol } -// GetOrderStatus 获取订单状态 -// Hyperliquid 使用 IOC 订单,通常立即成交或取消 -// 对于已完成的订单,需要查询历史记录 +// GetOrderStatus gets order status +// Hyperliquid uses IOC orders, usually filled or cancelled immediately +// For completed orders, need to query historical records func (t *HyperliquidTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { - // Hyperliquid 的 IOC 订单几乎立即完成 - // 如果订单是通过本系统下单的,返回的 status 都是 FILLED - // 这里尝试查询开放订单来判断是否还在等待 + // Hyperliquid's IOC orders are completed almost immediately + // If order was placed through this system, returned status will be FILLED + // Try to query open orders to determine if still pending coin := convertSymbolToHyperliquid(symbol) - // 首先检查是否在开放订单中 + // First check if in open orders openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr) if err != nil { - // 如果查询失败,假设订单已完成 + // If query fails, assume order is completed return map[string]interface{}{ "orderId": orderID, "status": "FILLED", @@ -919,10 +917,10 @@ func (t *HyperliquidTrader) GetOrderStatus(symbol string, orderID string) (map[s }, nil } - // 检查订单是否在开放订单列表中 + // Check if order is in open orders list for _, order := range openOrders { if order.Coin == coin && fmt.Sprintf("%d", order.Oid) == orderID { - // 订单仍在等待 + // Order is still pending return map[string]interface{}{ "orderId": orderID, "status": "NEW", @@ -933,18 +931,18 @@ func (t *HyperliquidTrader) GetOrderStatus(symbol string, orderID string) (map[s } } - // 订单不在开放列表中,说明已完成或已取消 - // Hyperliquid IOC 订单如果不在开放列表中,通常是已成交 + // Order not in open list, meaning completed or cancelled + // Hyperliquid IOC orders not in open list are usually filled return map[string]interface{}{ "orderId": orderID, "status": "FILLED", - "avgPrice": 0.0, // Hyperliquid 不直接返回成交价格,需要从持仓信息获取 + "avgPrice": 0.0, // Hyperliquid does not directly return execution price, need to get from position info "executedQty": 0.0, "commission": 0.0, }, nil } -// absFloat 返回浮点数的绝对值 +// absFloat returns absolute value of float func absFloat(x float64) float64 { if x < 0 { return -x diff --git a/trader/hyperliquid_trader_test.go b/trader/hyperliquid_trader_test.go index b50f842a..95d5812e 100644 --- a/trader/hyperliquid_trader_test.go +++ b/trader/hyperliquid_trader_test.go @@ -14,32 +14,32 @@ import ( ) // ============================================================ -// 一、HyperliquidTestSuite - 继承 base test suite +// Part 1: HyperliquidTestSuite - Inherits base test suite // ============================================================ -// HyperliquidTestSuite Hyperliquid 交易器测试套件 -// 继承 TraderTestSuite 并添加 Hyperliquid 特定的 mock 逻辑 +// HyperliquidTestSuite Hyperliquid trader test suite +// Inherits TraderTestSuite and adds Hyperliquid-specific mock logic type HyperliquidTestSuite struct { - *TraderTestSuite // 嵌入基础测试套件 + *TraderTestSuite // Embeds base test suite mockServer *httptest.Server privateKey *ecdsa.PrivateKey } -// NewHyperliquidTestSuite 创建 Hyperliquid 测试套件 +// NewHyperliquidTestSuite Create Hyperliquid test suite func NewHyperliquidTestSuite(t *testing.T) *HyperliquidTestSuite { - // 创建测试用私钥 + // Create test private key privateKey, err := crypto.HexToECDSA("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") if err != nil { - t.Fatalf("创建测试私钥失败: %v", err) + t.Fatalf("Failed to create test private key: %v", err) } - // 创建 mock HTTP 服务器 + // Create mock HTTP server mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 根据不同的请求路径返回不同的 mock 响应 + // Return different mock responses based on request path var respBody interface{} - // Hyperliquid API 使用 POST 请求,请求体是 JSON - // 我们需要根据请求体中的 "type" 字段来区分不同的请求 + // Hyperliquid API uses POST requests with JSON body + // We need to distinguish different requests by the "type" field in request body var reqBody map[string]interface{} if r.Method == "POST" { json.NewDecoder(r.Body).Decode(&reqBody) @@ -54,7 +54,7 @@ func NewHyperliquidTestSuite(t *testing.T) *HyperliquidTestSuite { } switch reqType { - // Mock Meta - 获取市场元数据 + // Mock Meta - Get market metadata case "meta": respBody = map[string]interface{}{ "universe": []map[string]interface{}{ @@ -78,14 +78,14 @@ func NewHyperliquidTestSuite(t *testing.T) *HyperliquidTestSuite { "marginTables": []interface{}{}, } - // Mock UserState - 获取用户账户状态(用于 GetBalance 和 GetPositions) + // Mock UserState - Get user account state (for GetBalance and GetPositions) case "clearinghouseState": user, _ := reqBody["user"].(string) - // 检查是否是查询 Agent 钱包余额(用于安全检查) + // Check if querying Agent wallet balance (for security check) agentAddr := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() if user == agentAddr { - // Agent 钱包余额应该很低 + // Agent wallet balance should be low respBody = map[string]interface{}{ "crossMarginSummary": map[string]interface{}{ "accountValue": "5.00", @@ -95,7 +95,7 @@ func NewHyperliquidTestSuite(t *testing.T) *HyperliquidTestSuite { "assetPositions": []interface{}{}, } } else { - // 主钱包账户状态 + // Main wallet account state respBody = map[string]interface{}{ "crossMarginSummary": map[string]interface{}{ "accountValue": "10000.00", @@ -121,7 +121,7 @@ func NewHyperliquidTestSuite(t *testing.T) *HyperliquidTestSuite { } } - // Mock SpotUserState - 获取现货账户状态 + // Mock SpotUserState - Get spot account state case "spotClearinghouseState": respBody = map[string]interface{}{ "balances": []map[string]interface{}{ @@ -132,25 +132,25 @@ func NewHyperliquidTestSuite(t *testing.T) *HyperliquidTestSuite { }, } - // Mock SpotMeta - 获取现货市场元数据 + // Mock SpotMeta - Get spot market metadata case "spotMeta": respBody = map[string]interface{}{ "universe": []map[string]interface{}{}, "tokens": []map[string]interface{}{}, } - // Mock AllMids - 获取所有市场价格 + // Mock AllMids - Get all market prices case "allMids": respBody = map[string]string{ "BTC": "50000.00", "ETH": "3000.00", } - // Mock OpenOrders - 获取挂单列表 + // Mock OpenOrders - Get open orders list case "openOrders": respBody = []interface{}{} - // Mock Order - 创建订单(开仓、平仓、止损、止盈) + // Mock Order - Create order (open, close, stop-loss, take-profit) case "order": respBody = map[string]interface{}{ "status": "ok", @@ -169,46 +169,46 @@ func NewHyperliquidTestSuite(t *testing.T) *HyperliquidTestSuite { }, } - // Mock UpdateLeverage - 设置杠杆 + // Mock UpdateLeverage - Set leverage case "updateLeverage": respBody = map[string]interface{}{ "status": "ok", } - // Mock Cancel - 取消订单 + // Mock Cancel - Cancel order case "cancel": respBody = map[string]interface{}{ "status": "ok", } default: - // 默认返回成功响应 + // Default return success response respBody = map[string]interface{}{ "status": "ok", } } - // 序列化响应 + // Serialize response w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(respBody) })) - // 创建 HyperliquidTrader,使用 mock 服务器 URL + // Create HyperliquidTrader, using mock server URL walletAddr := "0x9999999999999999999999999999999999999999" ctx := context.Background() - // 创建 Exchange 客户端,指向 mock 服务器 + // Create Exchange client, pointing to mock server exchange := hyperliquid.NewExchange( ctx, privateKey, - mockServer.URL, // 使用 mock 服务器 URL + mockServer.URL, // Use mock server URL nil, "", walletAddr, nil, ) - // 创建 meta(模拟获取成功) + // Create meta (simulate successful fetch) meta := &hyperliquid.Meta{ Universe: []hyperliquid.AssetInfo{ {Name: "BTC", SzDecimals: 4}, @@ -224,7 +224,7 @@ func NewHyperliquidTestSuite(t *testing.T) *HyperliquidTestSuite { isCrossMargin: true, } - // 创建基础套件 + // Create base suite baseSuite := NewTraderTestSuite(t, trader) return &HyperliquidTestSuite{ @@ -234,7 +234,7 @@ func NewHyperliquidTestSuite(t *testing.T) *HyperliquidTestSuite { } } -// Cleanup 清理资源 +// Cleanup Clean up resources func (s *HyperliquidTestSuite) Cleanup() { if s.mockServer != nil { s.mockServer.Close() @@ -243,29 +243,29 @@ func (s *HyperliquidTestSuite) Cleanup() { } // ============================================================ -// 二、使用 HyperliquidTestSuite 运行通用测试 +// Part 2: Run common tests using HyperliquidTestSuite // ============================================================ -// TestHyperliquidTrader_InterfaceCompliance 测试接口兼容性 +// TestHyperliquidTrader_InterfaceCompliance Test interface compliance func TestHyperliquidTrader_InterfaceCompliance(t *testing.T) { var _ Trader = (*HyperliquidTrader)(nil) } -// TestHyperliquidTrader_CommonInterface 使用测试套件运行所有通用接口测试 +// TestHyperliquidTrader_CommonInterface Run all common interface tests using test suite func TestHyperliquidTrader_CommonInterface(t *testing.T) { - // 创建测试套件 + // Create test suite suite := NewHyperliquidTestSuite(t) defer suite.Cleanup() - // 运行所有通用接口测试 + // Run all common interface tests suite.RunAllTests() } // ============================================================ -// 三、Hyperliquid 特定功能的单元测试 +// Part 3: Hyperliquid-specific feature unit tests // ============================================================ -// TestNewHyperliquidTrader 测试创建 Hyperliquid 交易器 +// TestNewHyperliquidTrader Test creating Hyperliquid trader func TestNewHyperliquidTrader(t *testing.T) { tests := []struct { name string @@ -276,15 +276,15 @@ func TestNewHyperliquidTrader(t *testing.T) { errorContains string }{ { - name: "无效私钥格式", + name: "Invalid private key format", privateKeyHex: "invalid_key", walletAddr: "0x1234567890123456789012345678901234567890", testnet: true, wantError: true, - errorContains: "解析私钥失败", + errorContains: "Failed to parse private key", }, { - name: "钱包地址为空", + name: "Empty wallet address", privateKeyHex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", walletAddr: "", testnet: true, @@ -315,13 +315,13 @@ func TestNewHyperliquidTrader(t *testing.T) { } } -// TestNewHyperliquidTrader_Success 测试成功创建交易器(需要 mock HTTP) +// TestNewHyperliquidTrader_Success Test successfully creating trader (requires mock HTTP) func TestNewHyperliquidTrader_Success(t *testing.T) { - // 创建测试用私钥 + // Create test private key privateKey, _ := crypto.HexToECDSA("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") agentAddr := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() - // 创建 mock HTTP 服务器 + // Create mock HTTP server mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var reqBody map[string]interface{} json.NewDecoder(r.Body).Decode(&reqBody) @@ -346,7 +346,7 @@ func TestNewHyperliquidTrader_Success(t *testing.T) { case "clearinghouseState": user, _ := reqBody["user"].(string) if user == agentAddr { - // Agent 钱包余额低 + // Agent wallet low balance respBody = map[string]interface{}{ "crossMarginSummary": map[string]interface{}{ "accountValue": "5.00", @@ -354,7 +354,7 @@ func TestNewHyperliquidTrader_Success(t *testing.T) { "assetPositions": []interface{}{}, } } else { - // 主钱包 + // Main wallet respBody = map[string]interface{}{ "crossMarginSummary": map[string]interface{}{ "accountValue": "10000.00", @@ -371,17 +371,17 @@ func TestNewHyperliquidTrader_Success(t *testing.T) { })) defer mockServer.Close() - // 注意:这个测试会真正调用 NewHyperliquidTrader,但会失败 - // 因为 hyperliquid SDK 不允许我们在构造函数中注入自定义 URL - // 所以这个测试仅用于验证参数处理逻辑 - t.Skip("跳过此测试:hyperliquid SDK 在构造时会调用真实 API,无法注入 mock URL") + // Note: This test would actually call NewHyperliquidTrader, but will fail + // Because hyperliquid SDK doesn't allow us to inject custom URL in constructor + // So this test is only for verifying parameter handling logic + t.Skip("Skip this test: hyperliquid SDK calls real API during construction, cannot inject mock URL") } // ============================================================ -// 四、工具函数单元测试(Hyperliquid 特有) +// Part 4: Utility function unit tests (Hyperliquid-specific) // ============================================================ -// TestConvertSymbolToHyperliquid 测试 symbol 转换函数 +// TestConvertSymbolToHyperliquid Test symbol conversion function func TestConvertSymbolToHyperliquid(t *testing.T) { tests := []struct { name string @@ -389,17 +389,17 @@ func TestConvertSymbolToHyperliquid(t *testing.T) { expected string }{ { - name: "BTCUSDT转换", + name: "BTCUSDT conversion", symbol: "BTCUSDT", expected: "BTC", }, { - name: "ETHUSDT转换", + name: "ETHUSDT conversion", symbol: "ETHUSDT", expected: "ETH", }, { - name: "无USDT后缀", + name: "No USDT suffix", symbol: "BTC", expected: "BTC", }, @@ -413,7 +413,7 @@ func TestConvertSymbolToHyperliquid(t *testing.T) { } } -// TestAbsFloat 测试绝对值函数 +// TestAbsFloat Test absolute value function func TestAbsFloat(t *testing.T) { tests := []struct { name string @@ -421,17 +421,17 @@ func TestAbsFloat(t *testing.T) { expected float64 }{ { - name: "正数", + name: "Positive number", input: 10.5, expected: 10.5, }, { - name: "负数", + name: "Negative number", input: -10.5, expected: 10.5, }, { - name: "零", + name: "Zero", input: 0, expected: 0, }, @@ -445,7 +445,7 @@ func TestAbsFloat(t *testing.T) { } } -// TestHyperliquidTrader_RoundToSzDecimals 测试数量精度处理 +// TestHyperliquidTrader_RoundToSzDecimals Test quantity precision handling func TestHyperliquidTrader_RoundToSzDecimals(t *testing.T) { trader := &HyperliquidTrader{ meta: &hyperliquid.Meta{ @@ -463,19 +463,19 @@ func TestHyperliquidTrader_RoundToSzDecimals(t *testing.T) { expected float64 }{ { - name: "BTC_四舍五入到4位", + name: "BTC - round to 4 decimals", coin: "BTC", quantity: 1.23456789, expected: 1.2346, }, { - name: "ETH_四舍五入到3位", + name: "ETH - round to 3 decimals", coin: "ETH", quantity: 10.12345, expected: 10.123, }, { - name: "未知币种_使用默认精度4位", + name: "Unknown coin - use default 4 decimals", coin: "UNKNOWN", quantity: 1.23456789, expected: 1.2346, @@ -490,7 +490,7 @@ func TestHyperliquidTrader_RoundToSzDecimals(t *testing.T) { } } -// TestHyperliquidTrader_RoundPriceToSigfigs 测试价格有效数字处理 +// TestHyperliquidTrader_RoundPriceToSigfigs Test price significant figures handling func TestHyperliquidTrader_RoundPriceToSigfigs(t *testing.T) { trader := &HyperliquidTrader{} @@ -500,17 +500,17 @@ func TestHyperliquidTrader_RoundPriceToSigfigs(t *testing.T) { expected float64 }{ { - name: "BTC价格_5位有效数字", + name: "BTC price - 5 significant figures", price: 50123.456789, expected: 50123.0, }, { - name: "小数价格_5位有效数字", + name: "Decimal price - 5 significant figures", price: 0.0012345678, expected: 0.0012346, }, { - name: "零价格", + name: "Zero price", price: 0, expected: 0, }, @@ -524,7 +524,7 @@ func TestHyperliquidTrader_RoundPriceToSigfigs(t *testing.T) { } } -// TestHyperliquidTrader_GetSzDecimals 测试获取精度 +// TestHyperliquidTrader_GetSzDecimals Test getting precision func TestHyperliquidTrader_GetSzDecimals(t *testing.T) { tests := []struct { name string @@ -533,13 +533,13 @@ func TestHyperliquidTrader_GetSzDecimals(t *testing.T) { expected int }{ { - name: "meta为nil_返回默认精度", + name: "meta is nil - return default precision", meta: nil, coin: "BTC", expected: 4, }, { - name: "找到BTC_返回正确精度", + name: "Found BTC - return correct precision", meta: &hyperliquid.Meta{ Universe: []hyperliquid.AssetInfo{ {Name: "BTC", SzDecimals: 5}, @@ -549,7 +549,7 @@ func TestHyperliquidTrader_GetSzDecimals(t *testing.T) { expected: 5, }, { - name: "未找到币种_返回默认精度", + name: "Coin not found - return default precision", meta: &hyperliquid.Meta{ Universe: []hyperliquid.AssetInfo{ {Name: "ETH", SzDecimals: 3}, @@ -569,7 +569,7 @@ func TestHyperliquidTrader_GetSzDecimals(t *testing.T) { } } -// TestHyperliquidTrader_SetMarginMode 测试设置保证金模式 +// TestHyperliquidTrader_SetMarginMode Test setting margin mode func TestHyperliquidTrader_SetMarginMode(t *testing.T) { trader := &HyperliquidTrader{ ctx: context.Background(), @@ -583,13 +583,13 @@ func TestHyperliquidTrader_SetMarginMode(t *testing.T) { wantError bool }{ { - name: "设置为全仓模式", + name: "Set to cross margin mode", symbol: "BTCUSDT", isCrossMargin: true, wantError: false, }, { - name: "设置为逐仓模式", + name: "Set to isolated margin mode", symbol: "ETHUSDT", isCrossMargin: false, wantError: false, @@ -610,7 +610,7 @@ func TestHyperliquidTrader_SetMarginMode(t *testing.T) { } } -// TestNewHyperliquidTrader_PrivateKeyProcessing 测试私钥处理 +// TestNewHyperliquidTrader_PrivateKeyProcessing Test private key processing func TestNewHyperliquidTrader_PrivateKeyProcessing(t *testing.T) { tests := []struct { name string @@ -619,13 +619,13 @@ func TestNewHyperliquidTrader_PrivateKeyProcessing(t *testing.T) { expectedLength int }{ { - name: "带0x前缀的私钥", + name: "Private key with 0x prefix", privateKeyHex: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", shouldStripOx: true, expectedLength: 64, }, { - name: "无前缀的私钥", + name: "Private key without prefix", privateKeyHex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", shouldStripOx: false, expectedLength: 64, @@ -634,7 +634,7 @@ func TestNewHyperliquidTrader_PrivateKeyProcessing(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // 测试私钥前缀处理逻辑(不实际创建 trader) + // Test private key prefix handling logic (without actually creating trader) processed := tt.privateKeyHex if len(processed) > 2 && (processed[:2] == "0x" || processed[:2] == "0X") { processed = processed[2:] diff --git a/trader/interface.go b/trader/interface.go index b1fa555f..d5b826c7 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -1,57 +1,57 @@ package trader -// Trader 交易器统一接口 -// 支持多个交易平台(币安、Hyperliquid等) +// Trader Unified trader interface +// Supports multiple trading platforms (Binance, Hyperliquid, etc.) type Trader interface { - // GetBalance 获取账户余额 + // GetBalance Get account balance GetBalance() (map[string]interface{}, error) - // GetPositions 获取所有持仓 + // GetPositions Get all positions GetPositions() ([]map[string]interface{}, error) - // OpenLong 开多仓 + // OpenLong Open long position OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) - // OpenShort 开空仓 + // OpenShort Open short position OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) - // CloseLong 平多仓(quantity=0表示全部平仓) + // CloseLong Close long position (quantity=0 means close all) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) - // CloseShort 平空仓(quantity=0表示全部平仓) + // CloseShort Close short position (quantity=0 means close all) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) - // SetLeverage 设置杠杆 + // SetLeverage Set leverage SetLeverage(symbol string, leverage int) error - // SetMarginMode 设置仓位模式 (true=全仓, false=逐仓) + // SetMarginMode Set position mode (true=cross margin, false=isolated margin) SetMarginMode(symbol string, isCrossMargin bool) error - // GetMarketPrice 获取市场价格 + // GetMarketPrice Get market price GetMarketPrice(symbol string) (float64, error) - // SetStopLoss 设置止损单 + // SetStopLoss Set stop-loss order SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error - // SetTakeProfit 设置止盈单 + // SetTakeProfit Set take-profit order SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error - // CancelStopLossOrders 仅取消止损单(修复 BUG:调整止损时不删除止盈) + // CancelStopLossOrders Cancel only stop-loss orders (BUG fix: don't delete take-profit when adjusting stop-loss) CancelStopLossOrders(symbol string) error - // CancelTakeProfitOrders 仅取消止盈单(修复 BUG:调整止盈时不删除止损) + // CancelTakeProfitOrders Cancel only take-profit orders (BUG fix: don't delete stop-loss when adjusting take-profit) CancelTakeProfitOrders(symbol string) error - // CancelAllOrders 取消该币种的所有挂单 + // CancelAllOrders Cancel all pending orders for this symbol CancelAllOrders(symbol string) error - // CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) + // CancelStopOrders Cancel stop-loss/take-profit orders for this symbol (for adjusting stop-loss/take-profit positions) CancelStopOrders(symbol string) error - // FormatQuantity 格式化数量到正确的精度 + // FormatQuantity Format quantity to correct precision FormatQuantity(symbol string, quantity float64) (string, error) - // GetOrderStatus 获取订单状态 - // 返回: status(FILLED/NEW/CANCELED), avgPrice, executedQty, commission + // GetOrderStatus Get order status + // Returns: status(FILLED/NEW/CANCELED), avgPrice, executedQty, commission GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) } diff --git a/trader/lighter_account.go b/trader/lighter_account.go index 6c63a910..6f3da7bf 100644 --- a/trader/lighter_account.go +++ b/trader/lighter_account.go @@ -7,29 +7,29 @@ import ( "net/http" ) -// AccountBalance 账户余额信息 +// AccountBalance Account balance information type AccountBalance struct { - TotalEquity float64 `json:"total_equity"` // 总权益 - AvailableBalance float64 `json:"available_balance"` // 可用余额 - MarginUsed float64 `json:"margin_used"` // 已用保证金 - UnrealizedPnL float64 `json:"unrealized_pnl"` // 未实现盈亏 - MaintenanceMargin float64 `json:"maintenance_margin"` // 维持保证金 + TotalEquity float64 `json:"total_equity"` // Total equity + AvailableBalance float64 `json:"available_balance"` // Available balance + MarginUsed float64 `json:"margin_used"` // Used margin + UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized PnL + MaintenanceMargin float64 `json:"maintenance_margin"` // Maintenance margin } -// Position 持仓信息 +// Position Position information type Position struct { - Symbol string `json:"symbol"` // 交易对 - Side string `json:"side"` // "long" 或 "short" - Size float64 `json:"size"` // 持仓大小 - EntryPrice float64 `json:"entry_price"` // 开仓均价 - MarkPrice float64 `json:"mark_price"` // 标记价格 - LiquidationPrice float64 `json:"liquidation_price"` // 强平价格 - UnrealizedPnL float64 `json:"unrealized_pnl"` // 未实现盈亏 - Leverage float64 `json:"leverage"` // 杠杆倍数 - MarginUsed float64 `json:"margin_used"` // 已用保证金 + Symbol string `json:"symbol"` // Trading pair + Side string `json:"side"` // "long" or "short" + Size float64 `json:"size"` // Position size + EntryPrice float64 `json:"entry_price"` // Average entry price + MarkPrice float64 `json:"mark_price"` // Mark price + LiquidationPrice float64 `json:"liquidation_price"` // Liquidation price + UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized PnL + Leverage float64 `json:"leverage"` // Leverage multiplier + MarginUsed float64 `json:"margin_used"` // Used margin } -// GetBalance 获取账户余额(实现 Trader 接口) +// GetBalance Get account balance (implements Trader interface) func (t *LighterTrader) GetBalance() (map[string]interface{}, error) { balance, err := t.GetAccountBalance() if err != nil { @@ -45,10 +45,10 @@ func (t *LighterTrader) GetBalance() (map[string]interface{}, error) { }, nil } -// GetAccountBalance 获取账户详细余额信息 +// GetAccountBalance Get detailed account balance information func (t *LighterTrader) GetAccountBalance() (*AccountBalance, error) { if err := t.ensureAuthToken(); err != nil { - return nil, fmt.Errorf("认证令牌无效: %w", err) + return nil, fmt.Errorf("invalid auth token: %w", err) } t.accountMutex.RLock() @@ -62,7 +62,7 @@ func (t *LighterTrader) GetAccountBalance() (*AccountBalance, error) { return nil, err } - // 添加认证头 + // Add auth header t.accountMutex.RLock() req.Header.Set("Authorization", t.authToken) t.accountMutex.RUnlock() @@ -79,21 +79,21 @@ func (t *LighterTrader) GetAccountBalance() (*AccountBalance, error) { } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("获取余额失败 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to get balance (status %d): %s", resp.StatusCode, string(body)) } var balance AccountBalance if err := json.Unmarshal(body, &balance); err != nil { - return nil, fmt.Errorf("解析余额响应失败: %w", err) + return nil, fmt.Errorf("failed to parse balance response: %w", err) } return &balance, nil } -// GetPositionsRaw 获取所有持仓(返回原始类型) +// GetPositionsRaw Get all positions (returns raw type) func (t *LighterTrader) GetPositionsRaw(symbol string) ([]Position, error) { if err := t.ensureAuthToken(); err != nil { - return nil, fmt.Errorf("认证令牌无效: %w", err) + return nil, fmt.Errorf("invalid auth token: %w", err) } t.accountMutex.RLock() @@ -110,7 +110,7 @@ func (t *LighterTrader) GetPositionsRaw(symbol string) ([]Position, error) { return nil, err } - // 添加认证头 + // Add auth header t.accountMutex.RLock() req.Header.Set("Authorization", t.authToken) t.accountMutex.RUnlock() @@ -127,18 +127,18 @@ func (t *LighterTrader) GetPositionsRaw(symbol string) ([]Position, error) { } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("获取持仓失败 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to get positions (status %d): %s", resp.StatusCode, string(body)) } var positions []Position if err := json.Unmarshal(body, &positions); err != nil { - return nil, fmt.Errorf("解析持仓响应失败: %w", err) + return nil, fmt.Errorf("failed to parse positions response: %w", err) } return positions, nil } -// GetPositions 获取所有持仓(实现 Trader 接口) +// GetPositions Get all positions (implements Trader interface) func (t *LighterTrader) GetPositions() ([]map[string]interface{}, error) { positions, err := t.GetPositionsRaw("") if err != nil { @@ -163,25 +163,25 @@ func (t *LighterTrader) GetPositions() ([]map[string]interface{}, error) { return result, nil } -// GetPosition 获取指定币种的持仓 +// GetPosition Get position for specified symbol func (t *LighterTrader) GetPosition(symbol string) (*Position, error) { positions, err := t.GetPositionsRaw(symbol) if err != nil { return nil, err } - // 找到指定币种的持仓 + // Find position for specified symbol for _, pos := range positions { if pos.Symbol == symbol && pos.Size > 0 { return &pos, nil } } - // 无持仓 + // No position return nil, nil } -// GetMarketPrice 获取市场价格 +// GetMarketPrice Get market price func (t *LighterTrader) GetMarketPrice(symbol string) (float64, error) { endpoint := fmt.Sprintf("%s/api/v1/market/ticker?symbol=%s", t.baseURL, symbol) @@ -202,24 +202,24 @@ func (t *LighterTrader) GetMarketPrice(symbol string) (float64, error) { } if resp.StatusCode != http.StatusOK { - return 0, fmt.Errorf("获取市场价格失败 (status %d): %s", resp.StatusCode, string(body)) + return 0, fmt.Errorf("failed to get market price (status %d): %s", resp.StatusCode, string(body)) } var ticker map[string]interface{} if err := json.Unmarshal(body, &ticker); err != nil { - return 0, fmt.Errorf("解析价格响应失败: %w", err) + return 0, fmt.Errorf("failed to parse price response: %w", err) } - // 提取最新价格 + // Extract latest price price, err := SafeFloat64(ticker, "last_price") if err != nil { - return 0, fmt.Errorf("无法获取价格: %w", err) + return 0, fmt.Errorf("failed to get price: %w", err) } return price, nil } -// GetAccountInfo 获取账户完整信息(用于AutoTrader) +// GetAccountInfo Get complete account information (for AutoTrader) func (t *LighterTrader) GetAccountInfo() (map[string]interface{}, error) { balance, err := t.GetAccountBalance() if err != nil { @@ -231,7 +231,7 @@ func (t *LighterTrader) GetAccountInfo() (map[string]interface{}, error) { return nil, err } - // 构建返回信息 + // Build return information info := map[string]interface{}{ "total_equity": balance.TotalEquity, "available_balance": balance.AvailableBalance, @@ -245,27 +245,27 @@ func (t *LighterTrader) GetAccountInfo() (map[string]interface{}, error) { return info, nil } -// SetLeverage 设置杠杆倍数 +// SetLeverage Set leverage multiplier func (t *LighterTrader) SetLeverage(symbol string, leverage int) error { if err := t.ensureAuthToken(); err != nil { - return fmt.Errorf("认证令牌无效: %w", err) + return fmt.Errorf("invalid auth token: %w", err) } - // TODO: 实现设置杠杆的API调用 - // LIGHTER可能需要签名交易来设置杠杆 + // TODO: Implement set leverage API call + // LIGHTER may require signed transaction to set leverage - return fmt.Errorf("SetLeverage未实现") + return fmt.Errorf("SetLeverage not implemented") } -// GetMaxLeverage 获取最大杠杆倍数 +// GetMaxLeverage Get maximum leverage multiplier func (t *LighterTrader) GetMaxLeverage(symbol string) (int, error) { - // LIGHTER支持BTC/ETH最高50x杠杆 - // TODO: 从API获取实际限制 + // LIGHTER supports up to 50x leverage for BTC/ETH + // TODO: Get actual limits from API if symbol == "BTC-PERP" || symbol == "ETH-PERP" { return 50, nil } - // 其他币种默认20x + // Default 20x for other symbols return 20, nil } diff --git a/trader/lighter_orders.go b/trader/lighter_orders.go index f95c67eb..bf24b5b1 100644 --- a/trader/lighter_orders.go +++ b/trader/lighter_orders.go @@ -9,19 +9,19 @@ import ( "net/http" ) -// CreateOrderRequest 创建订单请求 +// CreateOrderRequest Create order request type CreateOrderRequest struct { - Symbol string `json:"symbol"` // 交易对,如 "BTC-PERP" - Side string `json:"side"` // "buy" 或 "sell" - OrderType string `json:"order_type"` // "market" 或 "limit" - Quantity float64 `json:"quantity"` // 数量 - Price float64 `json:"price"` // 价格(限价单必填) - ReduceOnly bool `json:"reduce_only"` // 是否只减仓 + Symbol string `json:"symbol"` // Trading pair, e.g. "BTC-PERP" + Side string `json:"side"` // "buy" or "sell" + OrderType string `json:"order_type"` // "market" or "limit" + Quantity float64 `json:"quantity"` // Quantity + Price float64 `json:"price"` // Price (required for limit orders) + ReduceOnly bool `json:"reduce_only"` // Reduce-only flag TimeInForce string `json:"time_in_force"` // "GTC", "IOC", "FOK" - PostOnly bool `json:"post_only"` // 是否只做Maker + PostOnly bool `json:"post_only"` // Post-only (maker only) } -// OrderResponse 订单响应 +// OrderResponse Order response type OrderResponse struct { OrderID string `json:"order_id"` Symbol string `json:"symbol"` @@ -35,13 +35,13 @@ type OrderResponse struct { CreateTime int64 `json:"create_time"` } -// CreateOrder 创建订单(市价或限价) +// CreateOrder Create order (market or limit) func (t *LighterTrader) CreateOrder(symbol, side string, quantity, price float64, orderType string) (string, error) { if err := t.ensureAuthToken(); err != nil { - return "", fmt.Errorf("认证令牌无效: %w", err) + return "", fmt.Errorf("invalid auth token: %w", err) } - // 构建订单请求 + // Build order request req := CreateOrderRequest{ Symbol: symbol, Side: side, @@ -56,41 +56,41 @@ func (t *LighterTrader) CreateOrder(symbol, side string, quantity, price float64 req.Price = price } - // 发送订单 + // Send order orderResp, err := t.sendOrder(req) if err != nil { return "", err } - logger.Infof("✓ LIGHTER订单已创建 - ID: %s, Symbol: %s, Side: %s, Qty: %.4f", + logger.Infof("✓ LIGHTER order created - ID: %s, Symbol: %s, Side: %s, Qty: %.4f", orderResp.OrderID, symbol, side, quantity) return orderResp.OrderID, nil } -// sendOrder 发送订单到LIGHTER API +// sendOrder Send order to LIGHTER API func (t *LighterTrader) sendOrder(orderReq CreateOrderRequest) (*OrderResponse, error) { endpoint := fmt.Sprintf("%s/api/v1/order", t.baseURL) - // 序列化请求 + // Serialize request jsonData, err := json.Marshal(orderReq) if err != nil { return nil, err } - // 创建HTTP请求 + // Create HTTP request req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(jsonData)) if err != nil { return nil, err } - // 添加请求头 + // Add request headers req.Header.Set("Content-Type", "application/json") t.accountMutex.RLock() req.Header.Set("Authorization", t.authToken) t.accountMutex.RUnlock() - // 发送请求 + // Send request resp, err := t.client.Do(req) if err != nil { return nil, err @@ -103,21 +103,21 @@ func (t *LighterTrader) sendOrder(orderReq CreateOrderRequest) (*OrderResponse, } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("创建订单失败 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to create order (status %d): %s", resp.StatusCode, string(body)) } var orderResp OrderResponse if err := json.Unmarshal(body, &orderResp); err != nil { - return nil, fmt.Errorf("解析订单响应失败: %w", err) + return nil, fmt.Errorf("failed to parse order response: %w", err) } return &orderResp, nil } -// CancelOrder 取消订单 +// CancelOrder Cancel order func (t *LighterTrader) CancelOrder(symbol, orderID string) error { if err := t.ensureAuthToken(); err != nil { - return fmt.Errorf("认证令牌无效: %w", err) + return fmt.Errorf("invalid auth token: %w", err) } endpoint := fmt.Sprintf("%s/api/v1/order/%s", t.baseURL, orderID) @@ -127,7 +127,7 @@ func (t *LighterTrader) CancelOrder(symbol, orderID string) error { return err } - // 添加认证头 + // Add auth header t.accountMutex.RLock() req.Header.Set("Authorization", t.authToken) t.accountMutex.RUnlock() @@ -140,45 +140,45 @@ func (t *LighterTrader) CancelOrder(symbol, orderID string) error { if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("取消订单失败 (status %d): %s", resp.StatusCode, string(body)) + return fmt.Errorf("failed to cancel order (status %d): %s", resp.StatusCode, string(body)) } - logger.Infof("✓ LIGHTER订单已取消 - ID: %s", orderID) + logger.Infof("✓ LIGHTER order cancelled - ID: %s", orderID) return nil } -// CancelAllOrders 取消所有订单 +// CancelAllOrders Cancel all orders func (t *LighterTrader) CancelAllOrders(symbol string) error { if err := t.ensureAuthToken(); err != nil { - return fmt.Errorf("认证令牌无效: %w", err) + return fmt.Errorf("invalid auth token: %w", err) } - // 获取所有活跃订单 + // Get all active orders orders, err := t.GetActiveOrders(symbol) if err != nil { - return fmt.Errorf("获取活跃订单失败: %w", err) + return fmt.Errorf("failed to get active orders: %w", err) } if len(orders) == 0 { - logger.Infof("✓ LIGHTER - 无需取消订单(无活跃订单)") + logger.Infof("✓ LIGHTER - no orders to cancel (no active orders)") return nil } - // 批量取消 + // Cancel in batch for _, order := range orders { if err := t.CancelOrder(symbol, order.OrderID); err != nil { - logger.Infof("⚠️ 取消订单失败 (ID: %s): %v", order.OrderID, err) + logger.Infof("⚠️ Failed to cancel order (ID: %s): %v", order.OrderID, err) } } - logger.Infof("✓ LIGHTER - 已取消 %d 个订单", len(orders)) + logger.Infof("✓ LIGHTER - cancelled %d orders", len(orders)) return nil } -// GetActiveOrders 获取活跃订单 +// GetActiveOrders Get active orders func (t *LighterTrader) GetActiveOrders(symbol string) ([]OrderResponse, error) { if err := t.ensureAuthToken(); err != nil { - return nil, fmt.Errorf("认证令牌无效: %w", err) + return nil, fmt.Errorf("invalid auth token: %w", err) } t.accountMutex.RLock() @@ -195,7 +195,7 @@ func (t *LighterTrader) GetActiveOrders(symbol string) ([]OrderResponse, error) return nil, err } - // 添加认证头 + // Add auth header t.accountMutex.RLock() req.Header.Set("Authorization", t.authToken) t.accountMutex.RUnlock() @@ -212,21 +212,21 @@ func (t *LighterTrader) GetActiveOrders(symbol string) ([]OrderResponse, error) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("获取活跃订单失败 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to get active orders (status %d): %s", resp.StatusCode, string(body)) } var orders []OrderResponse if err := json.Unmarshal(body, &orders); err != nil { - return nil, fmt.Errorf("解析订单列表失败: %w", err) + return nil, fmt.Errorf("failed to parse order list: %w", err) } return orders, nil } -// GetOrderStatus 获取订单状态(实现 Trader 接口) +// GetOrderStatus Get order status (implements Trader interface) func (t *LighterTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { if err := t.ensureAuthToken(); err != nil { - return nil, fmt.Errorf("认证令牌无效: %w", err) + return nil, fmt.Errorf("invalid auth token: %w", err) } endpoint := fmt.Sprintf("%s/api/v1/order/%s", t.baseURL, orderID) @@ -236,7 +236,7 @@ func (t *LighterTrader) GetOrderStatus(symbol string, orderID string) (map[strin return nil, err } - // 添加认证头 + // Add auth header t.accountMutex.RLock() req.Header.Set("Authorization", t.authToken) t.accountMutex.RUnlock() @@ -253,15 +253,15 @@ func (t *LighterTrader) GetOrderStatus(symbol string, orderID string) (map[strin } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("获取订单状态失败 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to get order status (status %d): %s", resp.StatusCode, string(body)) } var order OrderResponse if err := json.Unmarshal(body, &order); err != nil { - return nil, fmt.Errorf("解析订单响应失败: %w", err) + return nil, fmt.Errorf("failed to parse order response: %w", err) } - // 转换状态为统一格式 + // Convert status to unified format unifiedStatus := order.Status switch order.Status { case "filled": @@ -281,43 +281,43 @@ func (t *LighterTrader) GetOrderStatus(symbol string, orderID string) (map[strin }, nil } -// CancelStopLossOrders 仅取消止损单(LIGHTER 暂无法区分,取消所有止盈止损单) +// CancelStopLossOrders Cancel stop-loss orders only (LIGHTER cannot distinguish, cancels all TP/SL orders) func (t *LighterTrader) CancelStopLossOrders(symbol string) error { - // LIGHTER 暂时无法区分止损和止盈单,取消所有止盈止损单 - logger.Infof(" ⚠️ LIGHTER 无法区分止损/止盈单,将取消所有止盈止损单") + // LIGHTER currently cannot distinguish between stop-loss and take-profit orders, cancel all TP/SL orders + logger.Infof(" ⚠️ LIGHTER cannot distinguish SL/TP orders, will cancel all TP/SL orders") return t.CancelStopOrders(symbol) } -// CancelTakeProfitOrders 仅取消止盈单(LIGHTER 暂无法区分,取消所有止盈止损单) +// CancelTakeProfitOrders Cancel take-profit orders only (LIGHTER cannot distinguish, cancels all TP/SL orders) func (t *LighterTrader) CancelTakeProfitOrders(symbol string) error { - // LIGHTER 暂时无法区分止损和止盈单,取消所有止盈止损单 - logger.Infof(" ⚠️ LIGHTER 无法区分止损/止盈单,将取消所有止盈止损单") + // LIGHTER currently cannot distinguish between stop-loss and take-profit orders, cancel all TP/SL orders + logger.Infof(" ⚠️ LIGHTER cannot distinguish SL/TP orders, will cancel all TP/SL orders") return t.CancelStopOrders(symbol) } -// CancelStopOrders 取消该币种的止盈/止损单 +// CancelStopOrders Cancel take-profit/stop-loss orders for this symbol func (t *LighterTrader) CancelStopOrders(symbol string) error { if err := t.ensureAuthToken(); err != nil { - return fmt.Errorf("认证令牌无效: %w", err) + return fmt.Errorf("invalid auth token: %w", err) } - // 获取活跃订单 + // Get active orders orders, err := t.GetActiveOrders(symbol) if err != nil { - return fmt.Errorf("获取活跃订单失败: %w", err) + return fmt.Errorf("failed to get active orders: %w", err) } canceledCount := 0 for _, order := range orders { - // TODO: 需要检查订单类型,只取消止盈止损单 - // 暂时取消所有订单 + // TODO: Need to check order type, only cancel TP/SL orders + // Currently cancelling all orders if err := t.CancelOrder(symbol, order.OrderID); err != nil { - logger.Infof("⚠️ 取消订单失败 (ID: %s): %v", order.OrderID, err) + logger.Infof("⚠️ Failed to cancel order (ID: %s): %v", order.OrderID, err) } else { canceledCount++ } } - logger.Infof("✓ LIGHTER - 已取消 %d 个止盈止损单", canceledCount) + logger.Infof("✓ LIGHTER - cancelled %d TP/SL orders", canceledCount) return nil } diff --git a/trader/lighter_trader.go b/trader/lighter_trader.go index 7280550d..39630e38 100644 --- a/trader/lighter_trader.go +++ b/trader/lighter_trader.go @@ -16,56 +16,56 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) -// LighterTrader LIGHTER DEX交易器 -// LIGHTER是基于Ethereum L2的永续合约DEX,使用zk-rollup技术 +// LighterTrader LIGHTER DEX trader +// LIGHTER is an Ethereum L2-based perpetual contract DEX using zk-rollup technology type LighterTrader struct { ctx context.Context privateKey *ecdsa.PrivateKey - walletAddr string // Ethereum钱包地址 + walletAddr string // Ethereum wallet address client *http.Client baseURL string testnet bool - // 账户信息缓存 - accountIndex int // LIGHTER账户索引 - apiKey string // API密钥(从私钥派生) - authToken string // 认证令牌(8小时有效期) + // Account information cache + accountIndex int // LIGHTER account index + apiKey string // API key (derived from private key) + authToken string // Authentication token (8-hour validity) tokenExpiry time.Time accountMutex sync.RWMutex - // 市场信息缓存 + // Market information cache symbolPrecision map[string]SymbolPrecision precisionMutex sync.RWMutex } -// LighterConfig LIGHTER配置 +// LighterConfig LIGHTER configuration type LighterConfig struct { PrivateKeyHex string WalletAddr string Testnet bool } -// NewLighterTrader 创建LIGHTER交易器 +// NewLighterTrader Create LIGHTER trader func NewLighterTrader(privateKeyHex string, walletAddr string, testnet bool) (*LighterTrader, error) { - // 去掉私钥的 0x 前缀(如果有) + // Remove 0x prefix from private key (if present) privateKeyHex = strings.TrimPrefix(strings.ToLower(privateKeyHex), "0x") - // 解析私钥 + // Parse private key privateKey, err := crypto.HexToECDSA(privateKeyHex) if err != nil { - return nil, fmt.Errorf("解析私钥失败: %w", err) + return nil, fmt.Errorf("failed to parse private key: %w", err) } - // 从私钥派生钱包地址(如果未提供) + // Derive wallet address from private key (if not provided) if walletAddr == "" { walletAddr = crypto.PubkeyToAddress(*privateKey.Public().(*ecdsa.PublicKey)).Hex() - logger.Infof("✓ 从私钥派生钱包地址: %s", walletAddr) + logger.Infof("✓ Derived wallet address from private key: %s", walletAddr) } - // 选择API URL + // Select API URL baseURL := "https://mainnet.zklighter.elliot.ai" if testnet { - baseURL = "https://testnet.zklighter.elliot.ai" // TODO: 确认testnet URL + baseURL = "https://testnet.zklighter.elliot.ai" // TODO: Confirm testnet URL } trader := &LighterTrader{ @@ -78,39 +78,39 @@ func NewLighterTrader(privateKeyHex string, walletAddr string, testnet bool) (*L symbolPrecision: make(map[string]SymbolPrecision), } - logger.Infof("✓ LIGHTER交易器初始化成功 (testnet=%v, wallet=%s)", testnet, walletAddr) + logger.Infof("✓ LIGHTER trader initialized successfully (testnet=%v, wallet=%s)", testnet, walletAddr) - // 初始化账户信息(获取账户索引和API密钥) + // Initialize account information (get account index and API key) if err := trader.initializeAccount(); err != nil { - return nil, fmt.Errorf("初始化账户失败: %w", err) + return nil, fmt.Errorf("failed to initialize account: %w", err) } return trader, nil } -// initializeAccount 初始化账户信息 +// initializeAccount Initialize account information func (t *LighterTrader) initializeAccount() error { - // 1. 获取账户信息(通过L1地址) + // 1. Get account information (by L1 address) accountInfo, err := t.getAccountByL1Address() if err != nil { - return fmt.Errorf("获取账户信息失败: %w", err) + return fmt.Errorf("failed to get account information: %w", err) } t.accountMutex.Lock() t.accountIndex = accountInfo["index"].(int) t.accountMutex.Unlock() - logger.Infof("✓ LIGHTER账户索引: %d", t.accountIndex) + logger.Infof("✓ LIGHTER account index: %d", t.accountIndex) - // 2. 生成认证令牌(有效期8小时) + // 2. Generate authentication token (8-hour validity) if err := t.refreshAuthToken(); err != nil { - return fmt.Errorf("生成认证令牌失败: %w", err) + return fmt.Errorf("failed to generate auth token: %w", err) } return nil } -// getAccountByL1Address 通过Ethereum地址获取LIGHTER账户信息 +// getAccountByL1Address Get LIGHTER account information by Ethereum address func (t *LighterTrader) getAccountByL1Address() (map[string]interface{}, error) { endpoint := fmt.Sprintf("%s/api/v1/account/by/l1/%s", t.baseURL, t.walletAddr) @@ -131,50 +131,50 @@ func (t *LighterTrader) getAccountByL1Address() (map[string]interface{}, error) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API错误 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("解析响应失败: %w", err) + return nil, fmt.Errorf("failed to parse response: %w", err) } return result, nil } -// refreshAuthToken 刷新认证令牌 +// refreshAuthToken Refresh authentication token func (t *LighterTrader) refreshAuthToken() error { - // TODO: 实现认证令牌生成逻辑 - // 参考 lighter-python SDK 的实现 - // 需要签名特定消息并提交到API + // TODO: Implement authentication token generation logic + // Reference lighter-python SDK implementation + // Need to sign specific message and submit to API t.accountMutex.Lock() defer t.accountMutex.Unlock() - // 临时实现:设置过期时间为8小时后 + // Temporary implementation: set expiry time to 8 hours from now t.tokenExpiry = time.Now().Add(8 * time.Hour) - logger.Infof("✓ 认证令牌已生成(有效期至: %s)", t.tokenExpiry.Format(time.RFC3339)) + logger.Infof("✓ Auth token generated (valid until: %s)", t.tokenExpiry.Format(time.RFC3339)) return nil } -// ensureAuthToken 确保认证令牌有效 +// ensureAuthToken Ensure authentication token is valid func (t *LighterTrader) ensureAuthToken() error { t.accountMutex.RLock() - expired := time.Now().After(t.tokenExpiry.Add(-30 * time.Minute)) // 提前30分钟刷新 + expired := time.Now().After(t.tokenExpiry.Add(-30 * time.Minute)) // Refresh 30 minutes early t.accountMutex.RUnlock() if expired { - logger.Info("🔄 认证令牌即将过期,刷新中...") + logger.Info("🔄 Auth token expiring soon, refreshing...") return t.refreshAuthToken() } return nil } -// signMessage 签名消息(Ethereum签名) +// signMessage Sign message (Ethereum signature) func (t *LighterTrader) signMessage(message []byte) (string, error) { - // 使用Ethereum个人签名格式 + // Use Ethereum personal sign format prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message)) prefixedMessage := append([]byte(prefix), message...) @@ -184,7 +184,7 @@ func (t *LighterTrader) signMessage(message []byte) (string, error) { return "", err } - // 调整v值(Ethereum格式) + // Adjust v value (Ethereum format) if signature[64] < 27 { signature[64] += 27 } @@ -192,24 +192,24 @@ func (t *LighterTrader) signMessage(message []byte) (string, error) { return "0x" + hex.EncodeToString(signature), nil } -// GetName 获取交易器名称 +// GetName Get trader name func (t *LighterTrader) GetName() string { return "LIGHTER" } -// GetExchangeType 获取交易所类型 +// GetExchangeType Get exchange type func (t *LighterTrader) GetExchangeType() string { return "lighter" } -// Close 关闭交易器 +// Close Close trader func (t *LighterTrader) Close() error { - logger.Info("✓ LIGHTER交易器已关闭") + logger.Info("✓ LIGHTER trader closed") return nil } -// Run 运行交易器(实现Trader接口) +// Run Run trader (implements Trader interface) func (t *LighterTrader) Run() error { - logger.Info("⚠️ LIGHTER交易器的Run方法应由AutoTrader调用") - return fmt.Errorf("请使用AutoTrader管理交易器生命周期") + logger.Info("⚠️ LIGHTER trader's Run method should be called by AutoTrader") + return fmt.Errorf("please use AutoTrader to manage trader lifecycle") } diff --git a/trader/lighter_trader_test.go b/trader/lighter_trader_test.go index d7dd0c05..003fef22 100644 --- a/trader/lighter_trader_test.go +++ b/trader/lighter_trader_test.go @@ -12,20 +12,20 @@ import ( ) // ============================================================ -// LIGHTER V1 测试套件 +// LIGHTER V1 Test Suite // ============================================================ -// TestLighterTrader_NewTrader 测试创建LIGHTER交易器 +// TestLighterTrader_NewTrader Test creating LIGHTER trader func TestLighterTrader_NewTrader(t *testing.T) { - t.Run("无效私钥", func(t *testing.T) { + t.Run("Invalid private key", func(t *testing.T) { trader, err := NewLighterTrader("invalid_key", "", true) assert.Error(t, err) assert.Nil(t, trader) t.Logf("✅ Invalid private key correctly rejected") }) - t.Run("有效私钥格式验证", func(t *testing.T) { - // 只验证私钥解析,不调用真实 API + t.Run("Valid private key format verification", func(t *testing.T) { + // Only verify private key parsing, don't call real API testL1Key := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" privateKey, err := crypto.HexToECDSA(testL1Key) assert.NoError(t, err) @@ -37,7 +37,7 @@ func TestLighterTrader_NewTrader(t *testing.T) { }) } -// createMockLighterServer 创建 mock LIGHTER API 服务器 +// createMockLighterServer Create mock LIGHTER API server func createMockLighterServer() *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path @@ -106,7 +106,7 @@ func createMockLighterServer() *httptest.Server { })) } -// createMockLighterTrader 创建带 mock server 的 LIGHTER trader +// createMockLighterTrader Create LIGHTER trader with mock server func createMockLighterTrader(t *testing.T, mockServer *httptest.Server) *LighterTrader { testL1Key := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" privateKey, err := crypto.HexToECDSA(testL1Key) @@ -125,7 +125,7 @@ func createMockLighterTrader(t *testing.T, mockServer *httptest.Server) *Lighter return trader } -// TestLighterTrader_GetBalance 测试获取余额 +// TestLighterTrader_GetBalance Test getting balance func TestLighterTrader_GetBalance(t *testing.T) { t.Skip("Skipping Lighter tests until mock server endpoints are completed") mockServer := createMockLighterServer() @@ -140,7 +140,7 @@ func TestLighterTrader_GetBalance(t *testing.T) { t.Logf("✅ GetBalance: %+v", balance) } -// TestLighterTrader_GetPositions 测试获取持仓 +// TestLighterTrader_GetPositions Test getting positions func TestLighterTrader_GetPositions(t *testing.T) { t.Skip("Skipping Lighter tests until mock server endpoints are completed") mockServer := createMockLighterServer() @@ -155,7 +155,7 @@ func TestLighterTrader_GetPositions(t *testing.T) { t.Logf("✅ GetPositions: found %d positions", len(positions)) } -// TestLighterTrader_GetMarketPrice 测试获取市场价格 +// TestLighterTrader_GetMarketPrice Test getting market price func TestLighterTrader_GetMarketPrice(t *testing.T) { t.Skip("Skipping Lighter tests until mock server endpoints are completed") mockServer := createMockLighterServer() @@ -170,7 +170,7 @@ func TestLighterTrader_GetMarketPrice(t *testing.T) { t.Logf("✅ GetMarketPrice(BTC): %.2f", price) } -// TestLighterTrader_FormatQuantity 测试格式化数量 +// TestLighterTrader_FormatQuantity Test formatting quantity func TestLighterTrader_FormatQuantity(t *testing.T) { mockServer := createMockLighterServer() defer mockServer.Close() @@ -184,7 +184,7 @@ func TestLighterTrader_FormatQuantity(t *testing.T) { t.Logf("✅ FormatQuantity: %s", result) } -// TestLighterTrader_GetExchangeType 测试获取交易所类型 +// TestLighterTrader_GetExchangeType Test getting exchange type func TestLighterTrader_GetExchangeType(t *testing.T) { mockServer := createMockLighterServer() defer mockServer.Close() @@ -197,45 +197,45 @@ func TestLighterTrader_GetExchangeType(t *testing.T) { t.Logf("✅ GetExchangeType: %s", exchangeType) } -// TestLighterTrader_InvalidQuantity 测试无效数量验证 +// TestLighterTrader_InvalidQuantity Test invalid quantity validation func TestLighterTrader_InvalidQuantity(t *testing.T) { mockServer := createMockLighterServer() defer mockServer.Close() trader := createMockLighterTrader(t, mockServer) - // 测试零数量 + // Test zero quantity _, err := trader.OpenLong("BTC", 0, 10) assert.Error(t, err) - // 测试负数量 + // Test negative quantity _, err = trader.OpenLong("BTC", -0.1, 10) assert.Error(t, err) t.Logf("✅ Invalid quantity validation working") } -// TestLighterTrader_InvalidLeverage 测试无效杠杆验证 +// TestLighterTrader_InvalidLeverage Test invalid leverage validation func TestLighterTrader_InvalidLeverage(t *testing.T) { mockServer := createMockLighterServer() defer mockServer.Close() trader := createMockLighterTrader(t, mockServer) - // 测试零杠杆 + // Test zero leverage _, err := trader.OpenLong("BTC", 0.1, 0) assert.Error(t, err) - // 测试负杠杆 + // Test negative leverage _, err = trader.OpenLong("BTC", 0.1, -10) assert.Error(t, err) t.Logf("✅ Invalid leverage validation working") } -// TestLighterTrader_HelperFunctions 测试辅助函数 +// TestLighterTrader_HelperFunctions Test helper functions func TestLighterTrader_HelperFunctions(t *testing.T) { - // 测试 SafeFloat64 + // Test SafeFloat64 data := map[string]interface{}{ "float_val": 123.45, "string_val": "678.90", diff --git a/trader/lighter_trader_v2.go b/trader/lighter_trader_v2.go index 673c3741..f58b3688 100644 --- a/trader/lighter_trader_v2.go +++ b/trader/lighter_trader_v2.go @@ -18,68 +18,68 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) -// AccountInfo LIGHTER 賬戶信息 +// AccountInfo LIGHTER account information type AccountInfo struct { AccountIndex int64 `json:"account_index"` L1Address string `json:"l1_address"` - // 其他字段可以根據實際 API 響應添加 + // Other fields can be added based on actual API response } -// LighterTraderV2 使用官方 lighter-go SDK 的新實現 +// LighterTraderV2 New implementation using official lighter-go SDK type LighterTraderV2 struct { ctx context.Context - privateKey *ecdsa.PrivateKey // L1 錢包私鑰(用於識別賬戶) - walletAddr string // Ethereum 錢包地址 + privateKey *ecdsa.PrivateKey // L1 wallet private key (for account identification) + walletAddr string // Ethereum wallet address client *http.Client baseURL string testnet bool chainID uint32 - // SDK 客戶端 + // SDK clients httpClient lighterClient.MinimalHTTPClient txClient *lighterClient.TxClient - // API Key 管理 - apiKeyPrivateKey string // 40字節的 API Key 私鑰(用於簽名交易) - apiKeyIndex uint8 // API Key 索引(默認 0) - accountIndex int64 // 賬戶索引 + // API Key management + apiKeyPrivateKey string // 40-byte API Key private key (for signing transactions) + apiKeyIndex uint8 // API Key index (default 0) + accountIndex int64 // Account index - // 認證令牌 + // Authentication token authToken string tokenExpiry time.Time accountMutex sync.RWMutex - // 市場信息緩存 + // Market info cache symbolPrecision map[string]SymbolPrecision precisionMutex sync.RWMutex - // 市場索引緩存 + // Market index cache marketIndexMap map[string]uint8 // symbol -> market_id marketMutex sync.RWMutex } -// NewLighterTraderV2 創建新的 LIGHTER 交易器(使用官方 SDK) -// 參數說明: -// - l1PrivateKeyHex: L1 錢包私鑰(32字節,標準以太坊私鑰) -// - walletAddr: 以太坊錢包地址(可選,會從私鑰自動派生) -// - apiKeyPrivateKeyHex: API Key 私鑰(40字節,用於簽名交易)如果為空則需要生成 -// - testnet: 是否使用測試網 +// NewLighterTraderV2 Create new LIGHTER trader (using official SDK) +// Parameters: +// - l1PrivateKeyHex: L1 wallet private key (32 bytes, standard Ethereum private key) +// - walletAddr: Ethereum wallet address (optional, will be derived from private key if empty) +// - apiKeyPrivateKeyHex: API Key private key (40 bytes, for signing transactions) - needs generation if empty +// - testnet: Whether to use testnet func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string, testnet bool) (*LighterTraderV2, error) { - // 1. 解析 L1 私鑰 + // 1. Parse L1 private key l1PrivateKeyHex = strings.TrimPrefix(strings.ToLower(l1PrivateKeyHex), "0x") l1PrivateKey, err := crypto.HexToECDSA(l1PrivateKeyHex) if err != nil { - return nil, fmt.Errorf("無效的 L1 私鑰: %w", err) + return nil, fmt.Errorf("invalid L1 private key: %w", err) } - // 2. 如果沒有提供錢包地址,從私鑰派生 + // 2. If wallet address not provided, derive from private key if walletAddr == "" { walletAddr = crypto.PubkeyToAddress(*l1PrivateKey.Public().(*ecdsa.PublicKey)).Hex() - logger.Infof("✓ 從私鑰派生錢包地址: %s", walletAddr) + logger.Infof("✓ Derived wallet address from private key: %s", walletAddr) } - // 3. 確定 API URL 和 Chain ID + // 3. Determine API URL and Chain ID baseURL := "https://mainnet.zklighter.elliot.ai" chainID := uint32(42766) // Mainnet Chain ID if testnet { @@ -87,7 +87,7 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string, chainID = uint32(42069) // Testnet Chain ID } - // 4. 創建 HTTP 客戶端 + // 4. Create HTTP client httpClient := lighterHTTP.NewClient(baseURL) trader := &LighterTraderV2{ @@ -100,24 +100,24 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string, chainID: chainID, httpClient: httpClient, apiKeyPrivateKey: apiKeyPrivateKeyHex, - apiKeyIndex: 0, // 默認使用索引 0 + apiKeyIndex: 0, // Default to index 0 symbolPrecision: make(map[string]SymbolPrecision), marketIndexMap: make(map[string]uint8), } - // 5. 初始化賬戶(獲取賬戶索引) + // 5. Initialize account (get account index) if err := trader.initializeAccount(); err != nil { - return nil, fmt.Errorf("初始化賬戶失敗: %w", err) + return nil, fmt.Errorf("failed to initialize account: %w", err) } - // 6. 如果沒有 API Key,提示用戶需要生成 + // 6. If no API Key, prompt user to generate one if apiKeyPrivateKeyHex == "" { - logger.Infof("⚠️ 未提供 API Key 私鑰,請調用 GenerateAndRegisterAPIKey() 生成") - logger.Infof(" 或者從 LIGHTER 官網獲取現有的 API Key") + logger.Infof("⚠️ No API Key private key provided, please call GenerateAndRegisterAPIKey() to generate") + logger.Infof(" Or get an existing API Key from LIGHTER website") return trader, nil } - // 7. 創建 TxClient(用於簽名交易) + // 7. Create TxClient (for signing transactions) txClient, err := lighterClient.NewTxClient( httpClient, apiKeyPrivateKeyHex, @@ -126,41 +126,41 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string, trader.chainID, ) if err != nil { - return nil, fmt.Errorf("創建 TxClient 失敗: %w", err) + return nil, fmt.Errorf("failed to create TxClient: %w", err) } trader.txClient = txClient - // 8. 驗證 API Key 是否正確 + // 8. Verify API Key is correct if err := trader.checkClient(); err != nil { - logger.Infof("⚠️ API Key 驗證失敗: %v", err) - logger.Infof(" 您可能需要重新生成 API Key 或檢查配置") + logger.Infof("⚠️ API Key verification failed: %v", err) + logger.Infof(" You may need to regenerate API Key or check configuration") return trader, err } - logger.Infof("✓ LIGHTER 交易器初始化成功 (account=%d, apiKey=%d, testnet=%v)", + logger.Infof("✓ LIGHTER trader initialized successfully (account=%d, apiKey=%d, testnet=%v)", trader.accountIndex, trader.apiKeyIndex, testnet) return trader, nil } -// initializeAccount 初始化賬戶信息(獲取賬戶索引) +// initializeAccount Initialize account information (get account index) func (t *LighterTraderV2) initializeAccount() error { - // 通過 L1 地址獲取賬戶信息 + // Get account info by L1 address accountInfo, err := t.getAccountByL1Address() if err != nil { - return fmt.Errorf("獲取賬戶信息失敗: %w", err) + return fmt.Errorf("failed to get account info: %w", err) } t.accountMutex.Lock() t.accountIndex = accountInfo.AccountIndex t.accountMutex.Unlock() - logger.Infof("✓ 賬戶索引: %d", t.accountIndex) + logger.Infof("✓ Account index: %d", t.accountIndex) return nil } -// getAccountByL1Address 通過 L1 錢包地址獲取 LIGHTER 賬戶信息 +// getAccountByL1Address Get LIGHTER account info by L1 wallet address func (t *LighterTraderV2) getAccountByL1Address() (*AccountInfo, error) { endpoint := fmt.Sprintf("%s/api/v1/account?by=address&value=%s", t.baseURL, t.walletAddr) @@ -181,67 +181,67 @@ func (t *LighterTraderV2) getAccountByL1Address() (*AccountInfo, error) { } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("獲取賬戶失敗 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to get account (status %d): %s", resp.StatusCode, string(body)) } var accountInfo AccountInfo if err := json.Unmarshal(body, &accountInfo); err != nil { - return nil, fmt.Errorf("解析賬戶響應失敗: %w", err) + return nil, fmt.Errorf("failed to parse account response: %w", err) } return &accountInfo, nil } -// checkClient 驗證 API Key 是否正確 +// checkClient Verify if API Key is correct func (t *LighterTraderV2) checkClient() error { if t.txClient == nil { - return fmt.Errorf("TxClient 未初始化") + return fmt.Errorf("TxClient not initialized") } - // 獲取服務器上註冊的 API Key 公鑰 + // Get API Key public key registered on server publicKey, err := t.httpClient.GetApiKey(t.accountIndex, t.apiKeyIndex) if err != nil { - return fmt.Errorf("獲取 API Key 失敗: %w", err) + return fmt.Errorf("failed to get API Key: %w", err) } - // 獲取本地 API Key 的公鑰 + // Get local API Key public key pubKeyBytes := t.txClient.GetKeyManager().PubKeyBytes() localPubKey := hexutil.Encode(pubKeyBytes[:]) localPubKey = strings.Replace(localPubKey, "0x", "", 1) - // 比對公鑰 + // Compare public keys if publicKey != localPubKey { - return fmt.Errorf("API Key 不匹配:本地=%s, 服務器=%s", localPubKey, publicKey) + return fmt.Errorf("API Key mismatch: local=%s, server=%s", localPubKey, publicKey) } - logger.Infof("✓ API Key 驗證通過") + logger.Infof("✓ API Key verification passed") return nil } -// GenerateAndRegisterAPIKey 生成新的 API Key 並註冊到 LIGHTER -// 注意:這需要 L1 私鑰簽名,所以必須在有 L1 私鑰的情況下調用 +// GenerateAndRegisterAPIKey Generate new API Key and register to LIGHTER +// Note: This requires L1 private key signature, so must be called with L1 private key available func (t *LighterTraderV2) GenerateAndRegisterAPIKey(seed string) (privateKey, publicKey string, err error) { - // 這個功能需要調用官方 SDK 的 GenerateAPIKey 函數 - // 但這是在 sharedlib 中的 CGO 函數,無法直接在純 Go 代碼中調用 + // This function needs to call the official SDK's GenerateAPIKey function + // But this is a CGO function in sharedlib, cannot be called directly in pure Go code // - // 解決方案: - // 1. 讓用戶從 LIGHTER 官網生成 API Key - // 2. 或者我們可以實現一個簡單的 API Key 生成包裝器 + // Solutions: + // 1. Let users generate API Key from LIGHTER website + // 2. Or we can implement a simple API Key generation wrapper - return "", "", fmt.Errorf("GenerateAndRegisterAPIKey 功能待實現,請從 LIGHTER 官網生成 API Key") + return "", "", fmt.Errorf("GenerateAndRegisterAPIKey feature not implemented yet, please generate API Key from LIGHTER website") } -// refreshAuthToken 刷新認證令牌(使用官方 SDK) +// refreshAuthToken Refresh authentication token (using official SDK) func (t *LighterTraderV2) refreshAuthToken() error { if t.txClient == nil { - return fmt.Errorf("TxClient 未初始化,請先設置 API Key") + return fmt.Errorf("TxClient not initialized, please set API Key first") } - // 使用官方 SDK 生成認證令牌(有效期 7 小時) + // Generate auth token using official SDK (valid for 7 hours) deadline := time.Now().Add(7 * time.Hour) authToken, err := t.txClient.GetAuthToken(deadline) if err != nil { - return fmt.Errorf("生成認證令牌失敗: %w", err) + return fmt.Errorf("failed to generate auth token: %w", err) } t.accountMutex.Lock() @@ -249,31 +249,31 @@ func (t *LighterTraderV2) refreshAuthToken() error { t.tokenExpiry = deadline t.accountMutex.Unlock() - logger.Infof("✓ 認證令牌已生成(有效期至: %s)", t.tokenExpiry.Format(time.RFC3339)) + logger.Infof("✓ Auth token generated (valid until: %s)", t.tokenExpiry.Format(time.RFC3339)) return nil } -// ensureAuthToken 確保認證令牌有效 +// ensureAuthToken Ensure authentication token is valid func (t *LighterTraderV2) ensureAuthToken() error { t.accountMutex.RLock() - expired := time.Now().After(t.tokenExpiry.Add(-30 * time.Minute)) // 提前 30 分鐘刷新 + expired := time.Now().After(t.tokenExpiry.Add(-30 * time.Minute)) // Refresh 30 minutes early t.accountMutex.RUnlock() if expired { - logger.Info("🔄 認證令牌即將過期,刷新中...") + logger.Info("🔄 Auth token about to expire, refreshing...") return t.refreshAuthToken() } return nil } -// GetExchangeType 獲取交易所類型 +// GetExchangeType Get exchange type func (t *LighterTraderV2) GetExchangeType() string { return "lighter" } -// Cleanup 清理資源 +// Cleanup Clean up resources func (t *LighterTraderV2) Cleanup() error { - logger.Info("⏹ LIGHTER 交易器清理完成") + logger.Info("⏹ LIGHTER trader cleanup completed") return nil } diff --git a/trader/lighter_trader_v2_account.go b/trader/lighter_trader_v2_account.go index e1f23c9d..7a808c55 100644 --- a/trader/lighter_trader_v2_account.go +++ b/trader/lighter_trader_v2_account.go @@ -7,7 +7,7 @@ import ( "net/http" ) -// GetBalance 獲取賬戶余額(實現 Trader 接口) +// GetBalance Get account balance (implements Trader interface) func (t *LighterTraderV2) GetBalance() (map[string]interface{}, error) { balance, err := t.GetAccountBalance() if err != nil { @@ -23,10 +23,10 @@ func (t *LighterTraderV2) GetBalance() (map[string]interface{}, error) { }, nil } -// GetAccountBalance 獲取賬戶詳細余額信息 +// GetAccountBalance Get detailed account balance information func (t *LighterTraderV2) GetAccountBalance() (*AccountBalance, error) { if err := t.ensureAuthToken(); err != nil { - return nil, fmt.Errorf("認證令牌無效: %w", err) + return nil, fmt.Errorf("invalid auth token: %w", err) } t.accountMutex.RLock() @@ -41,7 +41,7 @@ func (t *LighterTraderV2) GetAccountBalance() (*AccountBalance, error) { return nil, err } - // 添加認證頭 + // Add authentication header req.Header.Set("Authorization", authToken) resp, err := t.client.Do(req) @@ -56,18 +56,18 @@ func (t *LighterTraderV2) GetAccountBalance() (*AccountBalance, error) { } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("獲取余額失敗 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to get balance (status %d): %s", resp.StatusCode, string(body)) } var balance AccountBalance if err := json.Unmarshal(body, &balance); err != nil { - return nil, fmt.Errorf("解析余額響應失敗: %w", err) + return nil, fmt.Errorf("failed to parse balance response: %w", err) } return &balance, nil } -// GetPositions 獲取所有持倉(實現 Trader 接口) +// GetPositions Get all positions (implements Trader interface) func (t *LighterTraderV2) GetPositions() ([]map[string]interface{}, error) { positions, err := t.GetPositionsRaw("") if err != nil { @@ -92,10 +92,10 @@ func (t *LighterTraderV2) GetPositions() ([]map[string]interface{}, error) { return result, nil } -// GetPositionsRaw 獲取所有持倉(返回原始類型) +// GetPositionsRaw Get all positions (returns raw type) func (t *LighterTraderV2) GetPositionsRaw(symbol string) ([]Position, error) { if err := t.ensureAuthToken(); err != nil { - return nil, fmt.Errorf("認證令牌無效: %w", err) + return nil, fmt.Errorf("invalid auth token: %w", err) } t.accountMutex.RLock() @@ -127,18 +127,18 @@ func (t *LighterTraderV2) GetPositionsRaw(symbol string) ([]Position, error) { } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("獲取持倉失敗 (status %d): %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to get positions (status %d): %s", resp.StatusCode, string(body)) } var positions []Position if err := json.Unmarshal(body, &positions); err != nil { - return nil, fmt.Errorf("解析持倉響應失敗: %w", err) + return nil, fmt.Errorf("failed to parse positions response: %w", err) } return positions, nil } -// GetPosition 獲取指定幣種的持倉 +// GetPosition Get position for specified symbol func (t *LighterTraderV2) GetPosition(symbol string) (*Position, error) { positions, err := t.GetPositionsRaw(symbol) if err != nil { @@ -151,10 +151,10 @@ func (t *LighterTraderV2) GetPosition(symbol string) (*Position, error) { } } - return nil, nil // 無持倉 + return nil, nil // No position } -// GetMarketPrice 獲取市場價格(實現 Trader 接口) +// GetMarketPrice Get market price (implements Trader interface) func (t *LighterTraderV2) GetMarketPrice(symbol string) (float64, error) { endpoint := fmt.Sprintf("%s/api/v1/market/ticker?symbol=%s", t.baseURL, symbol) @@ -175,25 +175,25 @@ func (t *LighterTraderV2) GetMarketPrice(symbol string) (float64, error) { } if resp.StatusCode != http.StatusOK { - return 0, fmt.Errorf("獲取市場價格失敗 (status %d): %s", resp.StatusCode, string(body)) + return 0, fmt.Errorf("failed to get market price (status %d): %s", resp.StatusCode, string(body)) } var ticker map[string]interface{} if err := json.Unmarshal(body, &ticker); err != nil { - return 0, fmt.Errorf("解析價格響應失敗: %w", err) + return 0, fmt.Errorf("failed to parse price response: %w", err) } price, err := SafeFloat64(ticker, "last_price") if err != nil { - return 0, fmt.Errorf("無法獲取價格: %w", err) + return 0, fmt.Errorf("failed to get price: %w", err) } return price, nil } -// FormatQuantity 格式化數量到正確的精度(實現 Trader 接口) +// FormatQuantity Format quantity to correct precision (implements Trader interface) func (t *LighterTraderV2) FormatQuantity(symbol string, quantity float64) (string, error) { - // TODO: 從 API 獲取幣種精度 - // 暫時使用默認精度 + // TODO: Get symbol precision from API + // Using default precision for now return fmt.Sprintf("%.4f", quantity), nil } diff --git a/trader/lighter_trader_v2_orders.go b/trader/lighter_trader_v2_orders.go index 8ddf2687..cbb0c147 100644 --- a/trader/lighter_trader_v2_orders.go +++ b/trader/lighter_trader_v2_orders.go @@ -12,92 +12,92 @@ import ( "github.com/elliottech/lighter-go/types" ) -// SetStopLoss 設置止損單(實現 Trader 接口) +// SetStopLoss Set stop-loss order (implements Trader interface) func (t *LighterTraderV2) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { if t.txClient == nil { - return fmt.Errorf("TxClient 未初始化") + return fmt.Errorf("TxClient not initialized") } - logger.Infof("🛑 LIGHTER 設置止損: %s %s qty=%.4f, stop=%.2f", symbol, positionSide, quantity, stopPrice) + logger.Infof("🛑 LIGHTER Setting stop-loss: %s %s qty=%.4f, stop=%.2f", symbol, positionSide, quantity, stopPrice) - // 確定訂單方向(做空止損用買單,做多止損用賣單) + // Determine order direction (short position uses buy order, long position uses sell order) isAsk := (positionSide == "LONG" || positionSide == "long") - // 創建限價止損單 + // Create limit stop-loss order _, err := t.CreateOrder(symbol, isAsk, quantity, stopPrice, "limit") if err != nil { - return fmt.Errorf("設置止損失敗: %w", err) + return fmt.Errorf("failed to set stop-loss: %w", err) } - logger.Infof("✓ LIGHTER 止損已設置: %.2f", stopPrice) + logger.Infof("✓ LIGHTER stop-loss set: %.2f", stopPrice) return nil } -// SetTakeProfit 設置止盈單(實現 Trader 接口) +// SetTakeProfit Set take-profit order (implements Trader interface) func (t *LighterTraderV2) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { if t.txClient == nil { - return fmt.Errorf("TxClient 未初始化") + return fmt.Errorf("TxClient not initialized") } - logger.Infof("🎯 LIGHTER 設置止盈: %s %s qty=%.4f, tp=%.2f", symbol, positionSide, quantity, takeProfitPrice) + logger.Infof("🎯 LIGHTER Setting take-profit: %s %s qty=%.4f, tp=%.2f", symbol, positionSide, quantity, takeProfitPrice) - // 確定訂單方向(做空止盈用買單,做多止盈用賣單) + // Determine order direction (short position uses buy order, long position uses sell order) isAsk := (positionSide == "LONG" || positionSide == "long") - // 創建限價止盈單 + // Create limit take-profit order _, err := t.CreateOrder(symbol, isAsk, quantity, takeProfitPrice, "limit") if err != nil { - return fmt.Errorf("設置止盈失敗: %w", err) + return fmt.Errorf("failed to set take-profit: %w", err) } - logger.Infof("✓ LIGHTER 止盈已設置: %.2f", takeProfitPrice) + logger.Infof("✓ LIGHTER take-profit set: %.2f", takeProfitPrice) return nil } -// CancelAllOrders 取消所有訂單(實現 Trader 接口) +// CancelAllOrders Cancel all orders (implements Trader interface) func (t *LighterTraderV2) CancelAllOrders(symbol string) error { if t.txClient == nil { - return fmt.Errorf("TxClient 未初始化") + return fmt.Errorf("TxClient not initialized") } if err := t.ensureAuthToken(); err != nil { - return fmt.Errorf("認證令牌無效: %w", err) + return fmt.Errorf("invalid auth token: %w", err) } - // 獲取所有活躍訂單 + // Get all active orders orders, err := t.GetActiveOrders(symbol) if err != nil { - return fmt.Errorf("獲取活躍訂單失敗: %w", err) + return fmt.Errorf("failed to get active orders: %w", err) } if len(orders) == 0 { - logger.Infof("✓ LIGHTER - 無需取消訂單(無活躍訂單)") + logger.Infof("✓ LIGHTER - No orders to cancel (no active orders)") return nil } - // 批量取消 + // Batch cancel canceledCount := 0 for _, order := range orders { if err := t.CancelOrder(symbol, order.OrderID); err != nil { - logger.Infof("⚠️ 取消訂單失敗 (ID: %s): %v", order.OrderID, err) + logger.Infof("⚠️ Failed to cancel order (ID: %s): %v", order.OrderID, err) } else { canceledCount++ } } - logger.Infof("✓ LIGHTER - 已取消 %d 個訂單", canceledCount) + logger.Infof("✓ LIGHTER - Canceled %d orders", canceledCount) return nil } -// GetOrderStatus 獲取訂單狀態(實現 Trader 接口) +// GetOrderStatus Get order status (implements Trader interface) func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { - // LIGHTER 使用市價單通常立即成交 - // 嘗試查詢訂單狀態 + // LIGHTER market orders are usually filled immediately + // Try to query order status if err := t.ensureAuthToken(); err != nil { - return nil, fmt.Errorf("認證令牌無效: %w", err) + return nil, fmt.Errorf("invalid auth token: %w", err) } - // 構建請求 URL + // Build request URL endpoint := fmt.Sprintf("%s/api/v1/order/%s", t.baseURL, orderID) req, err := http.NewRequest("GET", endpoint, nil) @@ -110,7 +110,7 @@ func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[str resp, err := t.client.Do(req) if err != nil { - // 如果查詢失敗,假設訂單已完成 + // If query fails, assume order is filled return map[string]interface{}{ "orderId": orderID, "status": "FILLED", @@ -143,7 +143,7 @@ func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[str }, nil } - // 轉換狀態為統一格式 + // Convert status to unified format unifiedStatus := order.Status switch order.Status { case "filled": @@ -163,89 +163,89 @@ func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[str }, nil } -// CancelStopLossOrders 僅取消止損單(實現 Trader 接口) +// CancelStopLossOrders Cancel only stop-loss orders (implements Trader interface) func (t *LighterTraderV2) CancelStopLossOrders(symbol string) error { - // LIGHTER 暫時無法區分止損和止盈單,取消所有止盈止損單 - logger.Infof("⚠️ LIGHTER 無法區分止損/止盈單,將取消所有止盈止損單") + // LIGHTER cannot distinguish between stop-loss and take-profit orders yet, will cancel all stop orders + logger.Infof("⚠️ LIGHTER cannot distinguish stop-loss/take-profit orders, will cancel all stop orders") return t.CancelStopOrders(symbol) } -// CancelTakeProfitOrders 僅取消止盈單(實現 Trader 接口) +// CancelTakeProfitOrders Cancel only take-profit orders (implements Trader interface) func (t *LighterTraderV2) CancelTakeProfitOrders(symbol string) error { - // LIGHTER 暫時無法區分止損和止盈單,取消所有止盈止損單 - logger.Infof("⚠️ LIGHTER 無法區分止損/止盈單,將取消所有止盈止損單") + // LIGHTER cannot distinguish between stop-loss and take-profit orders yet, will cancel all stop orders + logger.Infof("⚠️ LIGHTER cannot distinguish stop-loss/take-profit orders, will cancel all stop orders") return t.CancelStopOrders(symbol) } -// CancelStopOrders 取消該幣種的止盈/止損單(實現 Trader 接口) +// CancelStopOrders Cancel stop-loss/take-profit orders for this symbol (implements Trader interface) func (t *LighterTraderV2) CancelStopOrders(symbol string) error { if t.txClient == nil { - return fmt.Errorf("TxClient 未初始化") + return fmt.Errorf("TxClient not initialized") } if err := t.ensureAuthToken(); err != nil { - return fmt.Errorf("認證令牌無效: %w", err) + return fmt.Errorf("invalid auth token: %w", err) } - // 獲取活躍訂單 + // Get active orders orders, err := t.GetActiveOrders(symbol) if err != nil { - return fmt.Errorf("獲取活躍訂單失敗: %w", err) + return fmt.Errorf("failed to get active orders: %w", err) } canceledCount := 0 for _, order := range orders { - // TODO: 檢查訂單類型,只取消止盈止損單 - // 暫時取消所有訂單 + // TODO: Check order type, only cancel stop orders + // For now, cancel all orders if err := t.CancelOrder(symbol, order.OrderID); err != nil { - logger.Infof("⚠️ 取消訂單失敗 (ID: %s): %v", order.OrderID, err) + logger.Infof("⚠️ Failed to cancel order (ID: %s): %v", order.OrderID, err) } else { canceledCount++ } } - logger.Infof("✓ LIGHTER - 已取消 %d 個止盈止損單", canceledCount) + logger.Infof("✓ LIGHTER - Canceled %d stop orders", canceledCount) return nil } -// GetActiveOrders 獲取活躍訂單 +// GetActiveOrders Get active orders func (t *LighterTraderV2) GetActiveOrders(symbol string) ([]OrderResponse, error) { if err := t.ensureAuthToken(); err != nil { - return nil, fmt.Errorf("認證令牌無效: %w", err) + return nil, fmt.Errorf("invalid auth token: %w", err) } - // 獲取市場索引 + // Get market index marketIndex, err := t.getMarketIndex(symbol) if err != nil { - return nil, fmt.Errorf("獲取市場索引失敗: %w", err) + return nil, fmt.Errorf("failed to get market index: %w", err) } - // 構建請求 URL + // Build request URL endpoint := fmt.Sprintf("%s/api/v1/accountActiveOrders?account_index=%d&market_id=%d", t.baseURL, t.accountIndex, marketIndex) - // 發送 GET 請求 + // Send GET request req, err := http.NewRequest("GET", endpoint, nil) if err != nil { - return nil, fmt.Errorf("創建請求失敗: %w", err) + return nil, fmt.Errorf("failed to create request: %w", err) } - // 添加認證頭 + // Add authentication header req.Header.Set("Authorization", t.authToken) req.Header.Set("Content-Type", "application/json") resp, err := t.client.Do(req) if err != nil { - return nil, fmt.Errorf("請求失敗: %w", err) + return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() 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"` Message string `json:"message"` @@ -253,83 +253,83 @@ func (t *LighterTraderV2) GetActiveOrders(symbol string) ([]OrderResponse, error } if err := json.Unmarshal(body, &apiResp); err != nil { - return nil, fmt.Errorf("解析響應失敗: %w, body: %s", err, string(body)) + return nil, fmt.Errorf("failed to parse response: %w, body: %s", err, string(body)) } if apiResp.Code != 200 { - return nil, fmt.Errorf("獲取活躍訂單失敗 (code %d): %s", apiResp.Code, apiResp.Message) + return nil, fmt.Errorf("failed to get active orders (code %d): %s", apiResp.Code, apiResp.Message) } - logger.Infof("✓ LIGHTER - 獲取到 %d 個活躍訂單", len(apiResp.Data)) + logger.Infof("✓ LIGHTER - Retrieved %d active orders", len(apiResp.Data)) return apiResp.Data, nil } -// CancelOrder 取消單個訂單 +// CancelOrder Cancel a single order func (t *LighterTraderV2) CancelOrder(symbol, orderID string) error { if t.txClient == nil { - return fmt.Errorf("TxClient 未初始化") + return fmt.Errorf("TxClient not initialized") } - // 獲取市場索引 + // Get market index marketIndex, err := t.getMarketIndex(symbol) if err != nil { - return fmt.Errorf("獲取市場索引失敗: %w", err) + return fmt.Errorf("failed to get market index: %w", err) } - // 將 orderID 轉換為 int64 + // Convert orderID to int64 orderIndex, err := strconv.ParseInt(orderID, 10, 64) if err != nil { - return fmt.Errorf("無效的訂單ID: %w", err) + return fmt.Errorf("invalid order ID: %w", err) } - // 構建取消訂單請求 + // Build cancel order request txReq := &types.CancelOrderTxReq{ MarketIndex: marketIndex, Index: orderIndex, } - // 使用 SDK 簽名交易 - nonce := int64(-1) // -1 表示自動獲取 + // Sign transaction using SDK + nonce := int64(-1) // -1 means auto-fetch tx, err := t.txClient.GetCancelOrderTransaction(txReq, &types.TransactOpts{ Nonce: &nonce, }) if err != nil { - return fmt.Errorf("簽名取消訂單失敗: %w", err) + return fmt.Errorf("failed to sign cancel order: %w", err) } - // 序列化交易 + // Serialize transaction txBytes, err := json.Marshal(tx) if err != nil { - return fmt.Errorf("序列化交易失敗: %w", err) + return fmt.Errorf("failed to serialize transaction: %w", err) } - // 提交取消訂單到 LIGHTER API + // Submit cancel order to LIGHTER API _, err = t.submitCancelOrder(txBytes) if err != nil { - return fmt.Errorf("提交取消訂單失敗: %w", err) + return fmt.Errorf("failed to submit cancel order: %w", err) } - logger.Infof("✓ LIGHTER訂單已取消 - ID: %s", orderID) + logger.Infof("✓ LIGHTER order canceled - ID: %s", orderID) return nil } -// submitCancelOrder 提交已簽名的取消訂單到 LIGHTER API +// submitCancelOrder Submit signed cancel order to LIGHTER API func (t *LighterTraderV2) submitCancelOrder(signedTx []byte) (map[string]interface{}, error) { const TX_TYPE_CANCEL_ORDER = 15 - // 構建請求 + // Build request req := SendTxRequest{ TxType: TX_TYPE_CANCEL_ORDER, TxInfo: string(signedTx), - PriceProtection: false, // 取消訂單不需要價格保護 + PriceProtection: false, // Cancel order doesn't need price protection } reqBody, err := json.Marshal(req) if err != nil { - return nil, fmt.Errorf("序列化請求失敗: %w", err) + return nil, fmt.Errorf("failed to serialize request: %w", err) } - // 發送 POST 請求到 /api/v1/sendTx + // Send POST request to /api/v1/sendTx endpoint := fmt.Sprintf("%s/api/v1/sendTx", t.baseURL) httpReq, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(reqBody)) if err != nil { @@ -349,15 +349,15 @@ func (t *LighterTraderV2) submitCancelOrder(signedTx []byte) (map[string]interfa return nil, err } - // 解析響應 + // Parse response var sendResp SendTxResponse if err := json.Unmarshal(body, &sendResp); err != nil { - return nil, fmt.Errorf("解析響應失敗: %w, body: %s", err, string(body)) + return nil, fmt.Errorf("failed to parse response: %w, body: %s", err, string(body)) } - // 檢查響應碼 + // Check response code if sendResp.Code != 200 { - return nil, fmt.Errorf("提交取消訂單失敗 (code %d): %s", sendResp.Code, sendResp.Message) + return nil, fmt.Errorf("failed to submit cancel order (code %d): %s", sendResp.Code, sendResp.Message) } result := map[string]interface{}{ @@ -365,6 +365,6 @@ func (t *LighterTraderV2) submitCancelOrder(signedTx []byte) (map[string]interfa "status": "cancelled", } - logger.Infof("✓ 取消訂單已提交到 LIGHTER - tx_hash: %v", sendResp.Data["tx_hash"]) + logger.Infof("✓ Cancel order submitted to LIGHTER - tx_hash: %v", sendResp.Data["tx_hash"]) return result, nil } diff --git a/trader/lighter_trader_v2_trading.go b/trader/lighter_trader_v2_trading.go index 00fe61c2..54cdb08d 100644 --- a/trader/lighter_trader_v2_trading.go +++ b/trader/lighter_trader_v2_trading.go @@ -12,32 +12,32 @@ import ( "github.com/elliottech/lighter-go/types" ) -// OpenLong 開多倉(實現 Trader 接口) +// OpenLong Open long position (implements Trader interface) func (t *LighterTraderV2) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { if t.txClient == nil { - return nil, fmt.Errorf("TxClient 未初始化,請先設置 API Key") + return nil, fmt.Errorf("TxClient not initialized, please set API Key first") } - logger.Infof("📈 LIGHTER 開多倉: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage) + logger.Infof("📈 LIGHTER opening long: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage) - // 1. 設置杠杆(如果需要) + // 1. Set leverage (if needed) if err := t.SetLeverage(symbol, leverage); err != nil { - logger.Infof("⚠️ 設置杠杆失敗: %v", err) + logger.Infof("⚠️ Failed to set leverage: %v", err) } - // 2. 獲取市場價格 + // 2. Get market price marketPrice, err := t.GetMarketPrice(symbol) if err != nil { - return nil, fmt.Errorf("獲取市場價格失敗: %w", err) + return nil, fmt.Errorf("failed to get market price: %w", err) } - // 3. 創建市價買入單(開多) + // 3. Create market buy order (open long) orderResult, err := t.CreateOrder(symbol, false, quantity, 0, "market") if err != nil { - return nil, fmt.Errorf("開多倉失敗: %w", err) + return nil, fmt.Errorf("failed to open long: %w", err) } - logger.Infof("✓ LIGHTER 開多倉成功: %s @ %.2f", symbol, marketPrice) + logger.Infof("✓ LIGHTER opened long successfully: %s @ %.2f", symbol, marketPrice) return map[string]interface{}{ "orderId": orderResult["orderId"], @@ -48,32 +48,32 @@ func (t *LighterTraderV2) OpenLong(symbol string, quantity float64, leverage int }, nil } -// OpenShort 開空倉(實現 Trader 接口) +// OpenShort Open short position (implements Trader interface) func (t *LighterTraderV2) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { if t.txClient == nil { - return nil, fmt.Errorf("TxClient 未初始化,請先設置 API Key") + return nil, fmt.Errorf("TxClient not initialized, please set API Key first") } - logger.Infof("📉 LIGHTER 開空倉: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage) + logger.Infof("📉 LIGHTER opening short: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage) - // 1. 設置杠杆 + // 1. Set leverage if err := t.SetLeverage(symbol, leverage); err != nil { - logger.Infof("⚠️ 設置杠杆失敗: %v", err) + logger.Infof("⚠️ Failed to set leverage: %v", err) } - // 2. 獲取市場價格 + // 2. Get market price marketPrice, err := t.GetMarketPrice(symbol) if err != nil { - return nil, fmt.Errorf("獲取市場價格失敗: %w", err) + return nil, fmt.Errorf("failed to get market price: %w", err) } - // 3. 創建市價賣出單(開空) + // 3. Create market sell order (open short) orderResult, err := t.CreateOrder(symbol, true, quantity, 0, "market") if err != nil { - return nil, fmt.Errorf("開空倉失敗: %w", err) + return nil, fmt.Errorf("failed to open short: %w", err) } - logger.Infof("✓ LIGHTER 開空倉成功: %s @ %.2f", symbol, marketPrice) + logger.Infof("✓ LIGHTER opened short successfully: %s @ %.2f", symbol, marketPrice) return map[string]interface{}{ "orderId": orderResult["orderId"], @@ -84,17 +84,17 @@ func (t *LighterTraderV2) OpenShort(symbol string, quantity float64, leverage in }, nil } -// CloseLong 平多倉(實現 Trader 接口) +// CloseLong Close long position (implements Trader interface) func (t *LighterTraderV2) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { if t.txClient == nil { - return nil, fmt.Errorf("TxClient 未初始化") + return nil, fmt.Errorf("TxClient not initialized") } - // 如果 quantity=0,獲取當前持倉數量 + // If quantity=0, get current position quantity if quantity == 0 { pos, err := t.GetPosition(symbol) if err != nil { - return nil, fmt.Errorf("獲取持倉失敗: %w", err) + return nil, fmt.Errorf("failed to get position: %w", err) } if pos == nil || pos.Size == 0 { return map[string]interface{}{ @@ -105,20 +105,20 @@ func (t *LighterTraderV2) CloseLong(symbol string, quantity float64) (map[string quantity = pos.Size } - logger.Infof("🔻 LIGHTER 平多倉: %s, qty=%.4f", symbol, quantity) + logger.Infof("🔻 LIGHTER closing long: %s, qty=%.4f", symbol, quantity) - // 創建市價賣出單平倉(reduceOnly=true) + // Create market sell order to close (reduceOnly=true) orderResult, err := t.CreateOrder(symbol, true, quantity, 0, "market") if err != nil { - return nil, fmt.Errorf("平多倉失敗: %w", err) + return nil, fmt.Errorf("failed to close long: %w", err) } - // 平倉後取消所有掛單 + // Cancel all open orders after closing position if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof("⚠️ 取消掛單失敗: %v", err) + logger.Infof("⚠️ Failed to cancel orders: %v", err) } - logger.Infof("✓ LIGHTER 平多倉成功: %s", symbol) + logger.Infof("✓ LIGHTER closed long successfully: %s", symbol) return map[string]interface{}{ "orderId": orderResult["orderId"], @@ -127,17 +127,17 @@ func (t *LighterTraderV2) CloseLong(symbol string, quantity float64) (map[string }, nil } -// CloseShort 平空倉(實現 Trader 接口) +// CloseShort Close short position (implements Trader interface) func (t *LighterTraderV2) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { if t.txClient == nil { - return nil, fmt.Errorf("TxClient 未初始化") + return nil, fmt.Errorf("TxClient not initialized") } - // 如果 quantity=0,獲取當前持倉數量 + // If quantity=0, get current position quantity if quantity == 0 { pos, err := t.GetPosition(symbol) if err != nil { - return nil, fmt.Errorf("獲取持倉失敗: %w", err) + return nil, fmt.Errorf("failed to get position: %w", err) } if pos == nil || pos.Size == 0 { return map[string]interface{}{ @@ -148,20 +148,20 @@ func (t *LighterTraderV2) CloseShort(symbol string, quantity float64) (map[strin quantity = pos.Size } - logger.Infof("🔺 LIGHTER 平空倉: %s, qty=%.4f", symbol, quantity) + logger.Infof("🔺 LIGHTER closing short: %s, qty=%.4f", symbol, quantity) - // 創建市價買入單平倉(reduceOnly=true) + // Create market buy order to close (reduceOnly=true) orderResult, err := t.CreateOrder(symbol, false, quantity, 0, "market") if err != nil { - return nil, fmt.Errorf("平空倉失敗: %w", err) + return nil, fmt.Errorf("failed to close short: %w", err) } - // 平倉後取消所有掛單 + // Cancel all open orders after closing position if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof("⚠️ 取消掛單失敗: %v", err) + logger.Infof("⚠️ Failed to cancel orders: %v", err) } - logger.Infof("✓ LIGHTER 平空倉成功: %s", symbol) + logger.Infof("✓ LIGHTER closed short successfully: %s", symbol) return map[string]interface{}{ "orderId": orderResult["orderId"], @@ -170,31 +170,31 @@ func (t *LighterTraderV2) CloseShort(symbol string, quantity float64) (map[strin }, nil } -// CreateOrder 創建訂單(市價或限價)- 使用官方 SDK 簽名 +// CreateOrder Create order (market or limit) - uses official SDK for signing func (t *LighterTraderV2) CreateOrder(symbol string, isAsk bool, quantity float64, price float64, orderType string) (map[string]interface{}, error) { if t.txClient == nil { - return nil, fmt.Errorf("TxClient 未初始化") + return nil, fmt.Errorf("TxClient not initialized") } - // 獲取市場索引(需要從 symbol 轉換) + // Get market index (convert from symbol) marketIndex, err := t.getMarketIndex(symbol) if err != nil { - return nil, fmt.Errorf("獲取市場索引失敗: %w", err) + return nil, fmt.Errorf("failed to get market index: %w", err) } - // 構建訂單請求 - clientOrderIndex := time.Now().UnixNano() // 使用時間戳作為客戶端訂單ID + // Build order request + clientOrderIndex := time.Now().UnixNano() // Use timestamp as client order ID var orderTypeValue uint8 = 0 // 0=limit, 1=market if orderType == "market" { orderTypeValue = 1 } - // 將數量和價格轉換為LIGHTER格式(需要乘以精度) - baseAmount := int64(quantity * 1e8) // 8位小數精度 + // Convert quantity and price to LIGHTER format (multiply by precision) + baseAmount := int64(quantity * 1e8) // 8 decimal precision priceValue := uint32(0) if orderType == "limit" { - priceValue = uint32(price * 1e2) // 價格精度 + priceValue = uint32(price * 1e2) // Price precision } txReq := &types.CreateOrderTxReq{ @@ -205,60 +205,60 @@ func (t *LighterTraderV2) CreateOrder(symbol string, isAsk bool, quantity float6 IsAsk: boolToUint8(isAsk), Type: orderTypeValue, TimeInForce: 0, // GTC - ReduceOnly: 0, // 不只減倉 + ReduceOnly: 0, // Not reduce-only TriggerPrice: 0, - OrderExpiry: time.Now().Add(24 * 28 * time.Hour).UnixMilli(), // 28天後過期 + OrderExpiry: time.Now().Add(24 * 28 * time.Hour).UnixMilli(), // Expires in 28 days } - // 使用SDK簽名交易(nonce會自動獲取) - nonce := int64(-1) // -1表示自動獲取 + // Sign transaction using SDK (nonce will be auto-fetched) + nonce := int64(-1) // -1 means auto-fetch tx, err := t.txClient.GetCreateOrderTransaction(txReq, &types.TransactOpts{ Nonce: &nonce, }) if err != nil { - return nil, fmt.Errorf("簽名訂單失敗: %w", err) + return nil, fmt.Errorf("failed to sign order: %w", err) } - // 序列化交易 + // Serialize transaction txBytes, err := json.Marshal(tx) if err != nil { - return nil, fmt.Errorf("序列化交易失敗: %w", err) + return nil, fmt.Errorf("failed to serialize transaction: %w", err) } - // 提交訂單到LIGHTER API + // Submit order to LIGHTER API orderResp, err := t.submitOrder(txBytes) if err != nil { - return nil, fmt.Errorf("提交訂單失敗: %w", err) + return nil, fmt.Errorf("failed to submit order: %w", err) } side := "buy" if isAsk { side = "sell" } - logger.Infof("✓ LIGHTER訂單已創建: %s %s qty=%.4f", symbol, side, quantity) + logger.Infof("✓ LIGHTER order created: %s %s qty=%.4f", symbol, side, quantity) return orderResp, nil } -// SendTxRequest 發送交易請求 +// SendTxRequest Send transaction request type SendTxRequest struct { TxType int `json:"tx_type"` TxInfo string `json:"tx_info"` PriceProtection bool `json:"price_protection,omitempty"` } -// SendTxResponse 發送交易響應 +// SendTxResponse Send transaction response type SendTxResponse struct { Code int `json:"code"` Message string `json:"message"` Data map[string]interface{} `json:"data"` } -// submitOrder 提交已簽名的訂單到LIGHTER API +// submitOrder Submit signed order to LIGHTER API func (t *LighterTraderV2) submitOrder(signedTx []byte) (map[string]interface{}, error) { const TX_TYPE_CREATE_ORDER = 14 - // 構建請求 + // Build request req := SendTxRequest{ TxType: TX_TYPE_CREATE_ORDER, TxInfo: string(signedTx), @@ -267,10 +267,10 @@ func (t *LighterTraderV2) submitOrder(signedTx []byte) (map[string]interface{}, reqBody, err := json.Marshal(req) if err != nil { - return nil, fmt.Errorf("序列化請求失敗: %w", err) + return nil, fmt.Errorf("failed to serialize request: %w", err) } - // 發送 POST 請求到 /api/v1/sendTx + // Send POST request to /api/v1/sendTx endpoint := fmt.Sprintf("%s/api/v1/sendTx", t.baseURL) httpReq, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(reqBody)) if err != nil { @@ -290,39 +290,39 @@ func (t *LighterTraderV2) submitOrder(signedTx []byte) (map[string]interface{}, return nil, err } - // 解析響應 + // Parse response var sendResp SendTxResponse if err := json.Unmarshal(body, &sendResp); err != nil { - return nil, fmt.Errorf("解析響應失敗: %w, body: %s", err, string(body)) + return nil, fmt.Errorf("failed to parse response: %w, body: %s", err, string(body)) } - // 檢查響應碼 + // Check response code if sendResp.Code != 200 { - return nil, fmt.Errorf("提交訂單失敗 (code %d): %s", sendResp.Code, sendResp.Message) + return nil, fmt.Errorf("failed to submit order (code %d): %s", sendResp.Code, sendResp.Message) } - // 提取交易哈希和訂單ID + // Extract transaction hash and order ID result := map[string]interface{}{ "tx_hash": sendResp.Data["tx_hash"], "status": "submitted", } - // 如果有訂單ID,添加到結果中 + // Add order ID to result if available if orderID, ok := sendResp.Data["order_id"]; ok { result["orderId"] = orderID } else if txHash, ok := sendResp.Data["tx_hash"].(string); ok { - // 使用 tx_hash 作為 orderID + // Use tx_hash as orderID result["orderId"] = txHash } - logger.Infof("✓ 訂單已提交到 LIGHTER - tx_hash: %v", sendResp.Data["tx_hash"]) + logger.Infof("✓ Order submitted to LIGHTER - tx_hash: %v", sendResp.Data["tx_hash"]) return result, nil } -// getMarketIndex 獲取市場索引(從symbol轉換)- 動態從API獲取 +// getMarketIndex Get market index (convert from symbol) - dynamically fetch from API func (t *LighterTraderV2) getMarketIndex(symbol string) (uint8, error) { - // 1. 檢查緩存 + // 1. Check cache t.marketMutex.RLock() if index, ok := t.marketIndexMap[symbol]; ok { t.marketMutex.RUnlock() @@ -330,62 +330,62 @@ func (t *LighterTraderV2) getMarketIndex(symbol string) (uint8, error) { } t.marketMutex.RUnlock() - // 2. 從 API 獲取市場列表 + // 2. Fetch market list from API markets, err := t.fetchMarketList() if err != nil { - // 如果 API 失敗,回退到硬編碼映射 - logger.Infof("⚠️ 從 API 獲取市場列表失敗,使用硬編碼映射: %v", err) + // If API fails, fallback to hardcoded mapping + logger.Infof("⚠️ Failed to fetch market list from API, using hardcoded mapping: %v", err) return t.getFallbackMarketIndex(symbol) } - // 3. 更新緩存 + // 3. Update cache t.marketMutex.Lock() for _, market := range markets { t.marketIndexMap[market.Symbol] = market.MarketID } t.marketMutex.Unlock() - // 4. 從緩存中獲取 + // 4. Get from cache t.marketMutex.RLock() index, ok := t.marketIndexMap[symbol] t.marketMutex.RUnlock() if !ok { - return 0, fmt.Errorf("未知的市場符號: %s", symbol) + return 0, fmt.Errorf("unknown market symbol: %s", symbol) } return index, nil } -// MarketInfo 市場信息 +// MarketInfo Market information type MarketInfo struct { Symbol string `json:"symbol"` MarketID uint8 `json:"market_id"` } -// fetchMarketList 從 API 獲取市場列表 +// fetchMarketList Fetch market list from API func (t *LighterTraderV2) fetchMarketList() ([]MarketInfo, error) { endpoint := fmt.Sprintf("%s/api/v1/orderBooks", t.baseURL) req, err := http.NewRequest("GET", endpoint, nil) if err != nil { - return nil, fmt.Errorf("創建請求失敗: %w", err) + return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := t.client.Do(req) if err != nil { - return nil, fmt.Errorf("請求失敗: %w", err) + return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() 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"` Message string `json:"message"` @@ -396,14 +396,14 @@ func (t *LighterTraderV2) fetchMarketList() ([]MarketInfo, error) { } if err := json.Unmarshal(body, &apiResp); err != nil { - return nil, fmt.Errorf("解析響應失敗: %w", err) + return nil, fmt.Errorf("failed to parse response: %w", err) } if apiResp.Code != 200 { - return nil, fmt.Errorf("獲取市場列表失敗 (code %d): %s", apiResp.Code, apiResp.Message) + return nil, fmt.Errorf("failed to get market list (code %d): %s", apiResp.Code, apiResp.Message) } - // 轉換為 MarketInfo 列表 + // Convert to MarketInfo list markets := make([]MarketInfo, len(apiResp.Data)) for i, market := range apiResp.Data { markets[i] = MarketInfo{ @@ -412,11 +412,11 @@ func (t *LighterTraderV2) fetchMarketList() ([]MarketInfo, error) { } } - logger.Infof("✓ 獲取到 %d 個市場", len(markets)) + logger.Infof("✓ Retrieved %d markets", len(markets)) return markets, nil } -// getFallbackMarketIndex 硬編碼的回退映射 +// getFallbackMarketIndex Hardcoded fallback mapping func (t *LighterTraderV2) getFallbackMarketIndex(symbol string) (uint8, error) { fallbackMap := map[string]uint8{ "BTC-PERP": 0, @@ -428,43 +428,43 @@ func (t *LighterTraderV2) getFallbackMarketIndex(symbol string) (uint8, error) { } if index, ok := fallbackMap[symbol]; ok { - logger.Infof("✓ 使用硬編碼市場索引: %s -> %d", symbol, index) + logger.Infof("✓ Using hardcoded market index: %s -> %d", symbol, index) return index, nil } - return 0, fmt.Errorf("未知的市場符號: %s", symbol) + return 0, fmt.Errorf("unknown market symbol: %s", symbol) } -// SetLeverage 設置杠杆(實現 Trader 接口) +// SetLeverage Set leverage (implements Trader interface) func (t *LighterTraderV2) SetLeverage(symbol string, leverage int) error { if t.txClient == nil { - return fmt.Errorf("TxClient 未初始化") + return fmt.Errorf("TxClient not initialized") } - // TODO: 使用SDK簽名並提交SetLeverage交易 - logger.Infof("⚙️ 設置杠杆: %s = %dx", symbol, leverage) + // TODO: Sign and submit SetLeverage transaction using SDK + logger.Infof("⚙️ Setting leverage: %s = %dx", symbol, leverage) - return nil // 暫時返回成功 + return nil // Return success for now } -// SetMarginMode 設置倉位模式(實現 Trader 接口) +// SetMarginMode Set margin mode (implements Trader interface) func (t *LighterTraderV2) SetMarginMode(symbol string, isCrossMargin bool) error { if t.txClient == nil { - return fmt.Errorf("TxClient 未初始化") + return fmt.Errorf("TxClient not initialized") } - modeStr := "逐倉" + modeStr := "isolated" if isCrossMargin { - modeStr = "全倉" + modeStr = "cross" } - logger.Infof("⚙️ 設置倉位模式: %s = %s", symbol, modeStr) + logger.Infof("⚙️ Setting margin mode: %s = %s", symbol, modeStr) - // TODO: 使用SDK簽名並提交SetMarginMode交易 + // TODO: Sign and submit SetMarginMode transaction using SDK return nil } -// boolToUint8 將布爾值轉換為uint8 +// boolToUint8 Convert boolean to uint8 func boolToUint8(b bool) uint8 { if b { return 1 diff --git a/trader/lighter_trading.go b/trader/lighter_trading.go index ee1a21cf..5225045b 100644 --- a/trader/lighter_trading.go +++ b/trader/lighter_trading.go @@ -5,15 +5,15 @@ import ( "nofx/logger" ) -// OpenLong 开多仓 +// OpenLong Open long position func (t *LighterTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // TODO: 实现完整的开多仓逻辑 - logger.Infof("🚧 LIGHTER OpenLong 暂未完全实现 (symbol=%s, qty=%.4f, leverage=%d)", symbol, quantity, leverage) + // TODO: Implement complete open long logic + logger.Infof("🚧 LIGHTER OpenLong not fully implemented (symbol=%s, qty=%.4f, leverage=%d)", symbol, quantity, leverage) - // 使用市价买入单 + // Use market buy order orderID, err := t.CreateOrder(symbol, "buy", quantity, 0, "market") if err != nil { - return nil, fmt.Errorf("开多仓失败: %w", err) + return nil, fmt.Errorf("failed to open long: %w", err) } return map[string]interface{}{ @@ -23,15 +23,15 @@ func (t *LighterTrader) OpenLong(symbol string, quantity float64, leverage int) }, nil } -// OpenShort 开空仓 +// OpenShort Open short position func (t *LighterTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // TODO: 实现完整的开空仓逻辑 - logger.Infof("🚧 LIGHTER OpenShort 暂未完全实现 (symbol=%s, qty=%.4f, leverage=%d)", symbol, quantity, leverage) + // TODO: Implement complete open short logic + logger.Infof("🚧 LIGHTER OpenShort not fully implemented (symbol=%s, qty=%.4f, leverage=%d)", symbol, quantity, leverage) - // 使用市价卖出单 + // Use market sell order orderID, err := t.CreateOrder(symbol, "sell", quantity, 0, "market") if err != nil { - return nil, fmt.Errorf("开空仓失败: %w", err) + return nil, fmt.Errorf("failed to open short: %w", err) } return map[string]interface{}{ @@ -41,13 +41,13 @@ func (t *LighterTrader) OpenShort(symbol string, quantity float64, leverage int) }, nil } -// CloseLong 平多仓(quantity=0表示全部平仓) +// CloseLong Close long position (quantity=0 means close all) func (t *LighterTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果quantity=0,获取当前持仓数量 + // If quantity=0, get current position size if quantity == 0 { pos, err := t.GetPosition(symbol) if err != nil { - return nil, fmt.Errorf("获取持仓失败: %w", err) + return nil, fmt.Errorf("failed to get position: %w", err) } if pos == nil || pos.Size == 0 { return map[string]interface{}{ @@ -58,15 +58,15 @@ func (t *LighterTrader) CloseLong(symbol string, quantity float64) (map[string]i quantity = pos.Size } - // 使用市价卖出单平仓 + // Use market sell order to close orderID, err := t.CreateOrder(symbol, "sell", quantity, 0, "market") if err != nil { - return nil, fmt.Errorf("平多仓失败: %w", err) + return nil, fmt.Errorf("failed to close long: %w", err) } - // 平仓后取消所有挂单 + // Cancel all pending orders after closing if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } return map[string]interface{}{ @@ -76,13 +76,13 @@ func (t *LighterTrader) CloseLong(symbol string, quantity float64) (map[string]i }, nil } -// CloseShort 平空仓(quantity=0表示全部平仓) +// CloseShort Close short position (quantity=0 means close all) func (t *LighterTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { - // 如果quantity=0,获取当前持仓数量 + // If quantity=0, get current position size if quantity == 0 { pos, err := t.GetPosition(symbol) if err != nil { - return nil, fmt.Errorf("获取持仓失败: %w", err) + return nil, fmt.Errorf("failed to get position: %w", err) } if pos == nil || pos.Size == 0 { return map[string]interface{}{ @@ -93,15 +93,15 @@ func (t *LighterTrader) CloseShort(symbol string, quantity float64) (map[string] quantity = pos.Size } - // 使用市价买入单平仓 + // Use market buy order to close orderID, err := t.CreateOrder(symbol, "buy", quantity, 0, "market") if err != nil { - return nil, fmt.Errorf("平空仓失败: %w", err) + return nil, fmt.Errorf("failed to close short: %w", err) } - // 平仓后取消所有挂单 + // Cancel all pending orders after closing if err := t.CancelAllOrders(symbol); err != nil { - logger.Infof(" ⚠ 取消挂单失败: %v", err) + logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } return map[string]interface{}{ @@ -111,62 +111,62 @@ func (t *LighterTrader) CloseShort(symbol string, quantity float64) (map[string] }, nil } -// SetStopLoss 设置止损单 +// SetStopLoss Set stop-loss order func (t *LighterTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { - // TODO: 实现完整的止损单逻辑 - logger.Infof("🚧 LIGHTER SetStopLoss 暂未完全实现 (symbol=%s, side=%s, qty=%.4f, stop=%.2f)", symbol, positionSide, quantity, stopPrice) + // TODO: Implement complete stop-loss logic + logger.Infof("🚧 LIGHTER SetStopLoss not fully implemented (symbol=%s, side=%s, qty=%.4f, stop=%.2f)", symbol, positionSide, quantity, stopPrice) - // 确定订单方向(做空止损用买单,做多止损用卖单) + // Determine order side (short position uses buy, long position uses sell) side := "sell" if positionSide == "SHORT" { side = "buy" } - // 创建限价止损单 + // Create limit stop-loss order _, err := t.CreateOrder(symbol, side, quantity, stopPrice, "limit") if err != nil { - return fmt.Errorf("设置止损失败: %w", err) + return fmt.Errorf("failed to set stop-loss: %w", err) } - logger.Infof("✓ LIGHTER - 止损已设置: %.2f (side: %s)", stopPrice, side) + logger.Infof("✓ LIGHTER - stop-loss set: %.2f (side: %s)", stopPrice, side) return nil } -// SetTakeProfit 设置止盈单 +// SetTakeProfit Set take-profit order func (t *LighterTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { - // TODO: 实现完整的止盈单逻辑 - logger.Infof("🚧 LIGHTER SetTakeProfit 暂未完全实现 (symbol=%s, side=%s, qty=%.4f, tp=%.2f)", symbol, positionSide, quantity, takeProfitPrice) + // TODO: Implement complete take-profit logic + logger.Infof("🚧 LIGHTER SetTakeProfit not fully implemented (symbol=%s, side=%s, qty=%.4f, tp=%.2f)", symbol, positionSide, quantity, takeProfitPrice) - // 确定订单方向(做空止盈用买单,做多止盈用卖单) + // Determine order side (short position uses buy, long position uses sell) side := "sell" if positionSide == "SHORT" { side = "buy" } - // 创建限价止盈单 + // Create limit take-profit order _, err := t.CreateOrder(symbol, side, quantity, takeProfitPrice, "limit") if err != nil { - return fmt.Errorf("设置止盈失败: %w", err) + return fmt.Errorf("failed to set take-profit: %w", err) } - logger.Infof("✓ LIGHTER - 止盈已设置: %.2f (side: %s)", takeProfitPrice, side) + logger.Infof("✓ LIGHTER - take-profit set: %.2f (side: %s)", takeProfitPrice, side) return nil } -// SetMarginMode 设置仓位模式 (true=全仓, false=逐仓) +// SetMarginMode Set position mode (true=cross, false=isolated) func (t *LighterTrader) SetMarginMode(symbol string, isCrossMargin bool) error { - // TODO: 实现仓位模式设置 - modeStr := "逐仓" + // TODO: Implement position mode setting + modeStr := "isolated" if isCrossMargin { - modeStr = "全仓" + modeStr = "cross" } - logger.Infof("🚧 LIGHTER SetMarginMode 暂未实现 (symbol=%s, mode=%s)", symbol, modeStr) + logger.Infof("🚧 LIGHTER SetMarginMode not implemented (symbol=%s, mode=%s)", symbol, modeStr) return nil } -// FormatQuantity 格式化数量到正确的精度 +// FormatQuantity Format quantity to correct precision func (t *LighterTrader) FormatQuantity(symbol string, quantity float64) (string, error) { - // TODO: 根据LIGHTER API获取币种精度 - // 暂时使用默认精度 + // TODO: Get symbol precision from LIGHTER API + // Using default precision for now return fmt.Sprintf("%.4f", quantity), nil } diff --git a/trader/okx_trader.go b/trader/okx_trader.go index 87191849..7ea72ccd 100644 --- a/trader/okx_trader.go +++ b/trader/okx_trader.go @@ -35,59 +35,59 @@ const ( okxPositionModePath = "/api/v5/account/set-position-mode" ) -// OKXTrader OKX合约交易器 +// OKXTrader OKX futures trader type OKXTrader struct { apiKey string secretKey string passphrase string - // HTTP 客户端(禁用代理) + // HTTP client (proxy disabled) httpClient *http.Client - // 余额缓存 + // Balance cache cachedBalance map[string]interface{} balanceCacheTime time.Time balanceCacheMutex sync.RWMutex - // 持仓缓存 + // Positions cache cachedPositions []map[string]interface{} positionsCacheTime time.Time positionsCacheMutex sync.RWMutex - // 合约信息缓存 + // Instrument info cache instrumentsCache map[string]*OKXInstrument instrumentsCacheTime time.Time instrumentsCacheMutex sync.RWMutex - // 缓存有效期 + // Cache duration cacheDuration time.Duration } -// OKXInstrument OKX合约信息 +// OKXInstrument OKX instrument info type OKXInstrument struct { - InstID string // 合约ID - CtVal float64 // 合约面值 - CtMult float64 // 合约乘数 - LotSz float64 // 最小下单数量 - MinSz float64 // 最小下单数量 - TickSz float64 // 最小价格变动 - CtType string // 合约类型 + InstID string // Instrument ID + CtVal float64 // Contract value + CtMult float64 // Contract multiplier + LotSz float64 // Minimum order size + MinSz float64 // Minimum order size + TickSz float64 // Minimum price increment + CtType string // Contract type } -// OKXResponse OKX API响应 +// OKXResponse OKX API response type OKXResponse struct { Code string `json:"code"` Msg string `json:"msg"` Data json.RawMessage `json:"data"` } -// getOkxOrderID 生成OKX订单ID +// genOkxClOrdID generates OKX order ID func genOkxClOrdID() string { timestamp := time.Now().UnixNano() % 10000000000000 randomBytes := make([]byte, 4) rand.Read(randomBytes) randomHex := hex.EncodeToString(randomBytes) - // OKX clOrdId 最长32字符 + // OKX clOrdId max 32 characters orderID := fmt.Sprintf("%s%d%s", okxTag, timestamp, randomHex) if len(orderID) > 32 { orderID = orderID[:32] @@ -95,10 +95,10 @@ func genOkxClOrdID() string { return orderID } -// NewOKXTrader 创建OKX交易器 +// NewOKXTrader creates OKX trader func NewOKXTrader(apiKey, secretKey, passphrase string) *OKXTrader { - // 使用 http.DefaultClient,与 Binance/Bybit SDK 保持一致 - // DefaultClient 使用 DefaultTransport,会读取环境变量代理设置 + // Use http.DefaultClient to stay consistent with Binance/Bybit SDK + // DefaultClient uses DefaultTransport, which reads proxy settings from environment variables trader := &OKXTrader{ apiKey: apiKey, secretKey: secretKey, @@ -108,35 +108,35 @@ func NewOKXTrader(apiKey, secretKey, passphrase string) *OKXTrader { instrumentsCache: make(map[string]*OKXInstrument), } - // 设置双向持仓模式 + // Set dual position mode if err := trader.setPositionMode(); err != nil { - logger.Infof("⚠️ 设置OKX持仓模式失败: %v (如果已是双向模式则忽略)", err) + logger.Infof("⚠️ Failed to set OKX position mode: %v (ignore if already in dual mode)", err) } return trader } -// setPositionMode 设置双向持仓模式 +// setPositionMode sets dual position mode func (t *OKXTrader) setPositionMode() error { body := map[string]string{ - "posMode": "long_short_mode", // 双向持仓 + "posMode": "long_short_mode", // Dual position mode } _, err := t.doRequest("POST", okxPositionModePath, body) if err != nil { - // 如果已经是双向模式,忽略错误 + // Ignore error if already in dual position mode if strings.Contains(err.Error(), "already") || strings.Contains(err.Error(), "Position mode is not modified") { - logger.Infof(" ✓ OKX账户已是双向持仓模式") + logger.Infof(" ✓ OKX account is already in dual position mode") return nil } return err } - logger.Infof(" ✓ OKX账户已切换为双向持仓模式") + logger.Infof(" ✓ OKX account switched to dual position mode") return nil } -// sign 生成OKX API签名 +// sign generates OKX API signature func (t *OKXTrader) sign(timestamp, method, requestPath, body string) string { preHash := timestamp + method + requestPath + body h := hmac.New(sha256.New, []byte(t.secretKey)) @@ -144,7 +144,7 @@ func (t *OKXTrader) sign(timestamp, method, requestPath, body string) string { return base64.StdEncoding.EncodeToString(h.Sum(nil)) } -// doRequest 执行HTTP请求 +// doRequest executes HTTP request func (t *OKXTrader) doRequest(method, path string, body interface{}) ([]byte, error) { var bodyBytes []byte var err error @@ -152,7 +152,7 @@ func (t *OKXTrader) doRequest(method, path string, body interface{}) ([]byte, er if body != nil { bodyBytes, err = json.Marshal(body) if err != nil { - return nil, fmt.Errorf("序列化请求体失败: %w", err) + return nil, fmt.Errorf("failed to serialize request body: %w", err) } } @@ -161,7 +161,7 @@ func (t *OKXTrader) doRequest(method, path string, body interface{}) ([]byte, er req, err := http.NewRequest(method, okxBaseURL+path, bytes.NewReader(bodyBytes)) if err != nil { - return nil, fmt.Errorf("创建请求失败: %w", err) + return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("OK-ACCESS-KEY", t.apiKey) @@ -169,44 +169,44 @@ func (t *OKXTrader) doRequest(method, path string, body interface{}) ([]byte, er req.Header.Set("OK-ACCESS-TIMESTAMP", timestamp) req.Header.Set("OK-ACCESS-PASSPHRASE", t.passphrase) req.Header.Set("Content-Type", "application/json") - // 设置请求头 + // Set request header req.Header.Set("x-simulated-trading", "0") resp, err := t.httpClient.Do(req) if err != nil { - return nil, fmt.Errorf("请求失败: %w", err) + return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("读取响应失败: %w", err) + return nil, fmt.Errorf("failed to read response: %w", err) } var okxResp OKXResponse if err := json.Unmarshal(respBody, &okxResp); err != nil { - return nil, fmt.Errorf("解析响应失败: %w", err) + return nil, fmt.Errorf("failed to parse response: %w", err) } - // code=1 表示部分成功,需要检查 data 里的具体结果 - // code=2 表示全部失败 + // code=1 indicates partial success, need to check specific results in data + // code=2 indicates complete failure if okxResp.Code != "0" && okxResp.Code != "1" { - return nil, fmt.Errorf("OKX API错误: code=%s, msg=%s", okxResp.Code, okxResp.Msg) + return nil, fmt.Errorf("OKX API error: code=%s, msg=%s", okxResp.Code, okxResp.Msg) } return okxResp.Data, nil } -// convertSymbol 将通用符号转换为OKX格式 -// 如 BTCUSDT -> BTC-USDT-SWAP +// convertSymbol converts generic symbol to OKX format +// e.g. BTCUSDT -> BTC-USDT-SWAP func (t *OKXTrader) convertSymbol(symbol string) string { - // 移除USDT后缀并构建OKX格式 + // Remove USDT suffix and build OKX format base := strings.TrimSuffix(symbol, "USDT") return fmt.Sprintf("%s-USDT-SWAP", base) } -// convertSymbolBack 将OKX格式转换回通用符号 -// 如 BTC-USDT-SWAP -> BTCUSDT +// convertSymbolBack converts OKX format back to generic symbol +// e.g. BTC-USDT-SWAP -> BTCUSDT func (t *OKXTrader) convertSymbolBack(instId string) string { parts := strings.Split(instId, "-") if len(parts) >= 2 { @@ -215,21 +215,21 @@ func (t *OKXTrader) convertSymbolBack(instId string) string { return instId } -// GetBalance 获取账户余额 +// GetBalance gets account balance func (t *OKXTrader) GetBalance() (map[string]interface{}, error) { - // 检查缓存 + // Check cache t.balanceCacheMutex.RLock() if t.cachedBalance != nil && time.Since(t.balanceCacheTime) < t.cacheDuration { t.balanceCacheMutex.RUnlock() - logger.Infof("✓ 使用缓存的OKX账户余额") + logger.Infof("✓ Using cached OKX account balance") return t.cachedBalance, nil } t.balanceCacheMutex.RUnlock() - logger.Infof("🔄 正在调用OKX API获取账户余额...") + logger.Infof("🔄 Calling OKX API to get account balance...") data, err := t.doRequest("GET", okxAccountPath, nil) if err != nil { - return nil, fmt.Errorf("获取账户余额失败: %w", err) + return nil, fmt.Errorf("failed to get account balance: %w", err) } var balances []struct { @@ -247,16 +247,16 @@ func (t *OKXTrader) GetBalance() (map[string]interface{}, error) { } if err := json.Unmarshal(data, &balances); err != nil { - return nil, fmt.Errorf("解析余额数据失败: %w", err) + return nil, fmt.Errorf("failed to parse balance data: %w", err) } if len(balances) == 0 { - return nil, fmt.Errorf("未获取到余额数据") + return nil, fmt.Errorf("no balance data received") } balance := balances[0] - // 查找USDT余额 + // Find USDT balance var usdtAvail, usdtUPL float64 for _, detail := range balance.Details { if detail.Ccy == "USDT" { @@ -274,9 +274,9 @@ func (t *OKXTrader) GetBalance() (map[string]interface{}, error) { "totalUnrealizedProfit": usdtUPL, } - logger.Infof("✓ OKX余额: 总权益=%.2f, 可用=%.2f, 未实现盈亏=%.2f", totalEq, usdtAvail, usdtUPL) + logger.Infof("✓ OKX balance: Total equity=%.2f, Available=%.2f, Unrealized PnL=%.2f", totalEq, usdtAvail, usdtUPL) - // 更新缓存 + // Update cache t.balanceCacheMutex.Lock() t.cachedBalance = result t.balanceCacheTime = time.Now() @@ -285,21 +285,21 @@ func (t *OKXTrader) GetBalance() (map[string]interface{}, error) { return result, nil } -// GetPositions 获取所有持仓 +// GetPositions gets all positions func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) { - // 检查缓存 + // Check cache t.positionsCacheMutex.RLock() if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration { t.positionsCacheMutex.RUnlock() - logger.Infof("✓ 使用缓存的OKX持仓信息") + logger.Infof("✓ Using cached OKX positions") return t.cachedPositions, nil } t.positionsCacheMutex.RUnlock() - logger.Infof("🔄 正在调用OKX API获取持仓信息...") + logger.Infof("🔄 Calling OKX API to get positions...") data, err := t.doRequest("GET", okxPositionPath+"?instType=SWAP", nil) if err != nil { - return nil, fmt.Errorf("获取持仓失败: %w", err) + return nil, fmt.Errorf("failed to get positions: %w", err) } var positions []struct { @@ -315,7 +315,7 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) { } if err := json.Unmarshal(data, &positions); err != nil { - return nil, fmt.Errorf("解析持仓数据失败: %w", err) + return nil, fmt.Errorf("failed to parse position data: %w", err) } var result []map[string]interface{} @@ -331,15 +331,15 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) { leverage, _ := strconv.ParseFloat(pos.Lever, 64) liqPrice, _ := strconv.ParseFloat(pos.LiqPx, 64) - // 转换symbol格式 + // Convert symbol format symbol := t.convertSymbolBack(pos.InstId) - // 确定方向,并确保 posAmt 是正数 + // Determine direction and ensure posAmt is positive side := "long" if pos.PosSide == "short" { side = "short" } - // OKX 空仓的 pos 是负数,需要取绝对值 + // OKX short position's pos is negative, need to take absolute value if posAmt < 0 { posAmt = -posAmt } @@ -357,7 +357,7 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) { result = append(result, posMap) } - // 更新缓存 + // Update cache t.positionsCacheMutex.Lock() t.cachedPositions = result t.positionsCacheTime = time.Now() @@ -366,11 +366,11 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) { return result, nil } -// getInstrument 获取合约信息 +// getInstrument gets instrument info func (t *OKXTrader) getInstrument(symbol string) (*OKXInstrument, error) { instId := t.convertSymbol(symbol) - // 检查缓存 + // Check cache t.instrumentsCacheMutex.RLock() if inst, ok := t.instrumentsCache[instId]; ok && time.Since(t.instrumentsCacheTime) < 5*time.Minute { t.instrumentsCacheMutex.RUnlock() @@ -378,7 +378,7 @@ func (t *OKXTrader) getInstrument(symbol string) (*OKXInstrument, error) { } t.instrumentsCacheMutex.RUnlock() - // 获取合约信息 + // Get instrument info path := fmt.Sprintf("%s?instType=SWAP&instId=%s", okxInstrumentsPath, instId) data, err := t.doRequest("GET", path, nil) if err != nil { @@ -400,7 +400,7 @@ func (t *OKXTrader) getInstrument(symbol string) (*OKXInstrument, error) { } if len(instruments) == 0 { - return nil, fmt.Errorf("未找到合约信息: %s", instId) + return nil, fmt.Errorf("instrument info not found: %s", instId) } inst := instruments[0] @@ -420,7 +420,7 @@ func (t *OKXTrader) getInstrument(symbol string) (*OKXInstrument, error) { CtType: inst.CtType, } - // 更新缓存 + // Update cache t.instrumentsCacheMutex.Lock() t.instrumentsCache[instId] = instrument t.instrumentsCacheTime = time.Now() @@ -429,7 +429,7 @@ func (t *OKXTrader) getInstrument(symbol string) (*OKXInstrument, error) { return instrument, nil } -// SetMarginMode 设置仓位模式 +// SetMarginMode sets margin mode func (t *OKXTrader) SetMarginMode(symbol string, isCrossMargin bool) error { instId := t.convertSymbol(symbol) @@ -445,28 +445,28 @@ func (t *OKXTrader) SetMarginMode(symbol string, isCrossMargin bool) error { _, err := t.doRequest("POST", "/api/v5/account/set-isolated-mode", body) if err != nil { - // 如果已经是目标模式,忽略错误 + // Ignore error if already in target mode if strings.Contains(err.Error(), "already") { - logger.Infof(" ✓ %s 仓位模式已是 %s", symbol, mgnMode) + logger.Infof(" ✓ %s margin mode is already %s", symbol, mgnMode) return nil } - // 有持仓无法更改 + // Cannot change when there are positions if strings.Contains(err.Error(), "position") { - logger.Infof(" ⚠️ %s 有持仓,无法更改仓位模式", symbol) + logger.Infof(" ⚠️ %s has positions, cannot change margin mode", symbol) return nil } return err } - logger.Infof(" ✓ %s 仓位模式已设置为 %s", symbol, mgnMode) + logger.Infof(" ✓ %s margin mode set to %s", symbol, mgnMode) return nil } -// SetLeverage 设置杠杆 +// SetLeverage sets leverage func (t *OKXTrader) SetLeverage(symbol string, leverage int) error { instId := t.convertSymbol(symbol) - // 设置多头和空头的杠杆 + // Set leverage for both long and short for _, posSide := range []string{"long", "short"} { body := map[string]interface{}{ "instId": instId, @@ -477,43 +477,43 @@ func (t *OKXTrader) SetLeverage(symbol string, leverage int) error { _, err := t.doRequest("POST", okxLeveragePath, body) if err != nil { - // 如果已经是目标杠杆,忽略 + // Ignore if already at target leverage if strings.Contains(err.Error(), "same") { continue } - logger.Infof(" ⚠️ 设置 %s %s 杠杆失败: %v", symbol, posSide, err) + logger.Infof(" ⚠️ Failed to set %s %s leverage: %v", symbol, posSide, err) } } - logger.Infof(" ✓ %s 杠杆已设置为 %dx", symbol, leverage) + logger.Infof(" ✓ %s leverage set to %dx", symbol, leverage) return nil } -// OpenLong 开多仓 +// OpenLong opens long position func (t *OKXTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 取消旧订单 + // Cancel old orders t.CancelAllOrders(symbol) - // 设置杠杆 + // Set leverage if err := t.SetLeverage(symbol, leverage); err != nil { - logger.Infof(" ⚠️ 设置杠杆失败: %v", err) + logger.Infof(" ⚠️ Failed to set leverage: %v", err) } instId := t.convertSymbol(symbol) - // 获取合约信息并计算合约数量 + // Get instrument info and calculate contract size inst, err := t.getInstrument(symbol) if err != nil { - return nil, fmt.Errorf("获取合约信息失败: %w", err) + return nil, fmt.Errorf("failed to get instrument info: %w", err) } - // OKX使用合约张数,需要根据合约面值转换 + // OKX uses contract size, need to convert based on contract value price, err := t.GetMarketPrice(symbol) if err != nil { - return nil, fmt.Errorf("获取市价失败: %w", err) + return nil, fmt.Errorf("failed to get market price: %w", err) } - // 计算合约张数 = 数量 * 价格 / 合约面值 + // Calculate contract size = quantity * price / contract value sz := quantity * price / inst.CtVal szStr := t.formatSize(sz, inst) @@ -530,7 +530,7 @@ func (t *OKXTrader) OpenLong(symbol string, quantity float64, leverage int) (map data, err := t.doRequest("POST", okxOrderPath, body) if err != nil { - return nil, fmt.Errorf("开多仓失败: %w", err) + return nil, fmt.Errorf("failed to open long position: %w", err) } var orders []struct { @@ -541,19 +541,19 @@ func (t *OKXTrader) OpenLong(symbol string, quantity float64, leverage int) (map } if err := json.Unmarshal(data, &orders); err != nil { - return nil, fmt.Errorf("解析订单响应失败: %w", err) + return nil, fmt.Errorf("failed to parse order response: %w", err) } if len(orders) == 0 || orders[0].SCode != "0" { - msg := "未知错误" + msg := "unknown error" if len(orders) > 0 { msg = orders[0].SMsg } - return nil, fmt.Errorf("开多仓失败: %s", msg) + return nil, fmt.Errorf("failed to open long position: %s", msg) } - logger.Infof("✓ OKX开多仓成功: %s 张数: %s", symbol, szStr) - logger.Infof(" 订单ID: %s", orders[0].OrdId) + logger.Infof("✓ OKX opened long position successfully: %s size: %s", symbol, szStr) + logger.Infof(" Order ID: %s", orders[0].OrdId) return map[string]interface{}{ "orderId": orders[0].OrdId, @@ -562,27 +562,27 @@ func (t *OKXTrader) OpenLong(symbol string, quantity float64, leverage int) (map }, nil } -// OpenShort 开空仓 +// OpenShort opens short position func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - // 取消旧订单 + // Cancel old orders t.CancelAllOrders(symbol) - // 设置杠杆 + // Set leverage if err := t.SetLeverage(symbol, leverage); err != nil { - logger.Infof(" ⚠️ 设置杠杆失败: %v", err) + logger.Infof(" ⚠️ Failed to set leverage: %v", err) } instId := t.convertSymbol(symbol) - // 获取合约信息并计算合约数量 + // Get instrument info and calculate contract size inst, err := t.getInstrument(symbol) if err != nil { - return nil, fmt.Errorf("获取合约信息失败: %w", err) + return nil, fmt.Errorf("failed to get instrument info: %w", err) } price, err := t.GetMarketPrice(symbol) if err != nil { - return nil, fmt.Errorf("获取市价失败: %w", err) + return nil, fmt.Errorf("failed to get market price: %w", err) } sz := quantity * price / inst.CtVal @@ -601,7 +601,7 @@ func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (ma data, err := t.doRequest("POST", okxOrderPath, body) if err != nil { - return nil, fmt.Errorf("开空仓失败: %w", err) + return nil, fmt.Errorf("failed to open short position: %w", err) } var orders []struct { @@ -612,19 +612,19 @@ func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (ma } if err := json.Unmarshal(data, &orders); err != nil { - return nil, fmt.Errorf("解析订单响应失败: %w", err) + return nil, fmt.Errorf("failed to parse order response: %w", err) } if len(orders) == 0 || orders[0].SCode != "0" { - msg := "未知错误" + msg := "unknown error" if len(orders) > 0 { msg = orders[0].SMsg } - return nil, fmt.Errorf("开空仓失败: %s", msg) + return nil, fmt.Errorf("failed to open short position: %s", msg) } - logger.Infof("✓ OKX开空仓成功: %s 张数: %s", symbol, szStr) - logger.Infof(" 订单ID: %s", orders[0].OrdId) + logger.Infof("✓ OKX opened short position successfully: %s size: %s", symbol, szStr) + logger.Infof(" Order ID: %s", orders[0].OrdId) return map[string]interface{}{ "orderId": orders[0].OrdId, @@ -633,11 +633,11 @@ func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (ma }, nil } -// CloseLong 平多仓 +// CloseLong closes long position func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { instId := t.convertSymbol(symbol) - // 如果数量为0,获取当前持仓(positionAmt 就是张数) + // If quantity is 0, get current position (positionAmt is the contract size) if quantity == 0 { positions, err := t.GetPositions() if err != nil { @@ -645,25 +645,25 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter } for _, pos := range positions { if pos["symbol"] == symbol && pos["side"] == "long" { - quantity = pos["positionAmt"].(float64) // 这已经是张数 + quantity = pos["positionAmt"].(float64) // This is already contract size break } } if quantity == 0 { - return nil, fmt.Errorf("没有找到 %s 的多仓", symbol) + return nil, fmt.Errorf("long position not found for %s", symbol) } } - // 获取合约信息用于格式化张数 + // Get instrument info for formatting contract size inst, err := t.getInstrument(symbol) if err != nil { - return nil, fmt.Errorf("获取合约信息失败: %w", err) + return nil, fmt.Errorf("failed to get instrument info: %w", err) } - // quantity 已经是张数,直接格式化 + // quantity is already contract size, format directly szStr := t.formatSize(quantity, inst) - logger.Infof("🔻 OKX平多仓参数: symbol=%s, instId=%s, quantity(张数)=%f, szStr=%s", + logger.Infof("🔻 OKX close long parameters: symbol=%s, instId=%s, quantity(contracts)=%f, szStr=%s", symbol, instId, quantity, szStr) body := map[string]interface{}{ @@ -679,7 +679,7 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter data, err := t.doRequest("POST", okxOrderPath, body) if err != nil { - return nil, fmt.Errorf("平多仓失败: %w", err) + return nil, fmt.Errorf("failed to close long position: %w", err) } var orders []struct { @@ -693,16 +693,16 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter } if len(orders) == 0 || orders[0].SCode != "0" { - msg := "未知错误" + msg := "unknown error" if len(orders) > 0 { msg = orders[0].SMsg } - return nil, fmt.Errorf("平多仓失败: %s", msg) + return nil, fmt.Errorf("failed to close long position: %s", msg) } - logger.Infof("✓ OKX平多仓成功: %s", symbol) + logger.Infof("✓ OKX closed long position successfully: %s", symbol) - // 平仓后取消挂单 + // Cancel pending orders after closing position t.CancelAllOrders(symbol) return map[string]interface{}{ @@ -712,49 +712,49 @@ func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]inter }, nil } -// CloseShort 平空仓 +// CloseShort closes short position func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { instId := t.convertSymbol(symbol) - // 如果数量为0,获取当前持仓(positionAmt 就是张数) + // If quantity is 0, get current position (positionAmt is the contract size) if quantity == 0 { positions, err := t.GetPositions() if err != nil { return nil, err } - logger.Infof("🔍 OKX CloseShort 查找持仓: symbol=%s, 当前持仓数=%d", symbol, len(positions)) + logger.Infof("🔍 OKX CloseShort searching positions: symbol=%s, current position count=%d", symbol, len(positions)) for _, pos := range positions { - logger.Infof("🔍 OKX 持仓: symbol=%v, side=%v, positionAmt=%v", + logger.Infof("🔍 OKX position: symbol=%v, side=%v, positionAmt=%v", pos["symbol"], pos["side"], pos["positionAmt"]) if pos["symbol"] == symbol && pos["side"] == "short" { quantity = pos["positionAmt"].(float64) - logger.Infof("🔍 OKX 找到空仓: quantity=%f", quantity) + logger.Infof("🔍 OKX found short position: quantity=%f", quantity) break } } if quantity == 0 { - return nil, fmt.Errorf("没有找到 %s 的空仓", symbol) + return nil, fmt.Errorf("short position not found for %s", symbol) } } - // 确保 quantity 是正数(OKX sz 参数必须为正) + // Ensure quantity is positive (OKX sz parameter must be positive) if quantity < 0 { quantity = -quantity } - // 获取合约信息用于格式化张数 + // Get instrument info for formatting contract size inst, err := t.getInstrument(symbol) if err != nil { - return nil, fmt.Errorf("获取合约信息失败: %w", err) + return nil, fmt.Errorf("failed to get instrument info: %w", err) } - logger.Infof("🔍 OKX 合约信息: instId=%s, lotSz=%f, minSz=%f, ctVal=%f", + logger.Infof("🔍 OKX instrument info: instId=%s, lotSz=%f, minSz=%f, ctVal=%f", inst.InstID, inst.LotSz, inst.MinSz, inst.CtVal) - // quantity 已经是张数,直接格式化 + // quantity is already contract size, format directly szStr := t.formatSize(quantity, inst) - logger.Infof("🔻 OKX平空仓参数: symbol=%s, instId=%s, quantity(张数)=%f, szStr=%s", + logger.Infof("🔻 OKX close short parameters: symbol=%s, instId=%s, quantity(contracts)=%f, szStr=%s", symbol, instId, quantity, szStr) body := map[string]interface{}{ @@ -768,11 +768,11 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte "tag": okxTag, } - logger.Infof("🔻 OKX平空仓请求体: %+v", body) + logger.Infof("🔻 OKX close short request body: %+v", body) data, err := t.doRequest("POST", okxOrderPath, body) if err != nil { - return nil, fmt.Errorf("平空仓失败: %w", err) + return nil, fmt.Errorf("failed to close short position: %w", err) } var orders []struct { @@ -786,17 +786,17 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte } if len(orders) == 0 || orders[0].SCode != "0" { - msg := "未知错误" + msg := "unknown error" if len(orders) > 0 { msg = fmt.Sprintf("sCode=%s, sMsg=%s", orders[0].SCode, orders[0].SMsg) } - logger.Infof("❌ OKX平空仓失败: %s, 响应: %s", msg, string(data)) - return nil, fmt.Errorf("平空仓失败: %s", msg) + logger.Infof("❌ OKX failed to close short position: %s, response: %s", msg, string(data)) + return nil, fmt.Errorf("failed to close short position: %s", msg) } - logger.Infof("✓ OKX平空仓成功: %s, ordId=%s", symbol, orders[0].OrdId) + logger.Infof("✓ OKX closed short position successfully: %s, ordId=%s", symbol, orders[0].OrdId) - // 平仓后取消挂单 + // Cancel pending orders after closing position t.CancelAllOrders(symbol) return map[string]interface{}{ @@ -806,14 +806,14 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte }, nil } -// GetMarketPrice 获取市场价格 +// GetMarketPrice gets market price func (t *OKXTrader) GetMarketPrice(symbol string) (float64, error) { instId := t.convertSymbol(symbol) path := fmt.Sprintf("%s?instId=%s", okxTickerPath, instId) data, err := t.doRequest("GET", path, nil) if err != nil { - return 0, fmt.Errorf("获取价格失败: %w", err) + return 0, fmt.Errorf("failed to get price: %w", err) } var tickers []struct { @@ -825,7 +825,7 @@ func (t *OKXTrader) GetMarketPrice(symbol string) (float64, error) { } if len(tickers) == 0 { - return 0, fmt.Errorf("未获取到价格数据") + return 0, fmt.Errorf("no price data received") } price, err := strconv.ParseFloat(tickers[0].Last, 64) @@ -836,22 +836,22 @@ func (t *OKXTrader) GetMarketPrice(symbol string) (float64, error) { return price, nil } -// SetStopLoss 设置止损单 +// SetStopLoss sets stop loss order func (t *OKXTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { instId := t.convertSymbol(symbol) - // 获取合约信息 + // Get instrument info inst, err := t.getInstrument(symbol) if err != nil { - return fmt.Errorf("获取合约信息失败: %w", err) + return fmt.Errorf("failed to get instrument info: %w", err) } - // 计算张数 + // Calculate contract size price, _ := t.GetMarketPrice(symbol) sz := quantity * price / inst.CtVal szStr := t.formatSize(sz, inst) - // 确定方向 + // Determine direction side := "sell" posSide := "long" if strings.ToUpper(positionSide) == "SHORT" { @@ -867,35 +867,35 @@ func (t *OKXTrader) SetStopLoss(symbol string, positionSide string, quantity, st "ordType": "conditional", "sz": szStr, "slTriggerPx": fmt.Sprintf("%.8f", stopPrice), - "slOrdPx": "-1", // 市价 + "slOrdPx": "-1", // Market price "tag": okxTag, } _, err = t.doRequest("POST", okxAlgoOrderPath, body) if err != nil { - return fmt.Errorf("设置止损失败: %w", err) + return fmt.Errorf("failed to set stop loss: %w", err) } - logger.Infof(" 止损价设置: %.4f", stopPrice) + logger.Infof(" Stop loss price set: %.4f", stopPrice) return nil } -// SetTakeProfit 设置止盈单 +// SetTakeProfit sets take profit order func (t *OKXTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { instId := t.convertSymbol(symbol) - // 获取合约信息 + // Get instrument info inst, err := t.getInstrument(symbol) if err != nil { - return fmt.Errorf("获取合约信息失败: %w", err) + return fmt.Errorf("failed to get instrument info: %w", err) } - // 计算张数 + // Calculate contract size price, _ := t.GetMarketPrice(symbol) sz := quantity * price / inst.CtVal szStr := t.formatSize(sz, inst) - // 确定方向 + // Determine direction side := "sell" posSide := "long" if strings.ToUpper(positionSide) == "SHORT" { @@ -911,34 +911,34 @@ func (t *OKXTrader) SetTakeProfit(symbol string, positionSide string, quantity, "ordType": "conditional", "sz": szStr, "tpTriggerPx": fmt.Sprintf("%.8f", takeProfitPrice), - "tpOrdPx": "-1", // 市价 + "tpOrdPx": "-1", // Market price "tag": okxTag, } _, err = t.doRequest("POST", okxAlgoOrderPath, body) if err != nil { - return fmt.Errorf("设置止盈失败: %w", err) + return fmt.Errorf("failed to set take profit: %w", err) } - logger.Infof(" 止盈价设置: %.4f", takeProfitPrice) + logger.Infof(" Take profit price set: %.4f", takeProfitPrice) return nil } -// CancelStopLossOrders 取消止损单 +// CancelStopLossOrders cancels stop loss orders func (t *OKXTrader) CancelStopLossOrders(symbol string) error { return t.cancelAlgoOrders(symbol, "sl") } -// CancelTakeProfitOrders 取消止盈单 +// CancelTakeProfitOrders cancels take profit orders func (t *OKXTrader) CancelTakeProfitOrders(symbol string) error { return t.cancelAlgoOrders(symbol, "tp") } -// cancelAlgoOrders 取消策略订单 +// cancelAlgoOrders cancels algo orders func (t *OKXTrader) cancelAlgoOrders(symbol string, orderType string) error { instId := t.convertSymbol(symbol) - // 获取待成交的策略订单 + // Get pending algo orders path := fmt.Sprintf("%s?instType=SWAP&instId=%s&ordType=conditional", okxAlgoPendingPath, instId) data, err := t.doRequest("GET", path, nil) if err != nil { @@ -965,24 +965,24 @@ func (t *OKXTrader) cancelAlgoOrders(symbol string, orderType string) error { _, err := t.doRequest("POST", okxCancelAlgoPath, body) if err != nil { - logger.Infof(" ⚠️ 取消策略订单失败: %v", err) + logger.Infof(" ⚠️ Failed to cancel algo order: %v", err) continue } canceledCount++ } if canceledCount > 0 { - logger.Infof(" ✓ 已取消 %s 的 %d 个策略订单", symbol, canceledCount) + logger.Infof(" ✓ Canceled %d algo orders for %s", canceledCount, symbol) } return nil } -// CancelAllOrders 取消所有挂单 +// CancelAllOrders cancels all pending orders func (t *OKXTrader) CancelAllOrders(symbol string) error { instId := t.convertSymbol(symbol) - // 获取待成交订单 + // Get pending orders path := fmt.Sprintf("%s?instType=SWAP&instId=%s", okxPendingOrdersPath, instId) data, err := t.doRequest("GET", path, nil) if err != nil { @@ -998,7 +998,7 @@ func (t *OKXTrader) CancelAllOrders(symbol string) error { return err } - // 批量取消 + // Batch cancel for _, order := range orders { body := map[string]interface{}{ "instId": order.InstId, @@ -1007,29 +1007,29 @@ func (t *OKXTrader) CancelAllOrders(symbol string) error { t.doRequest("POST", okxCancelOrderPath, body) } - // 同时取消策略订单 + // Also cancel algo orders t.cancelAlgoOrders(symbol, "") if len(orders) > 0 { - logger.Infof(" ✓ 已取消 %s 的所有挂单", symbol) + logger.Infof(" ✓ Canceled all pending orders for %s", symbol) } return nil } -// CancelStopOrders 取消止盈止损单 +// CancelStopOrders cancels stop loss and take profit orders func (t *OKXTrader) CancelStopOrders(symbol string) error { return t.cancelAlgoOrders(symbol, "") } -// FormatQuantity 格式化数量 +// FormatQuantity formats quantity func (t *OKXTrader) FormatQuantity(symbol string, quantity float64) (string, error) { inst, err := t.getInstrument(symbol) if err != nil { return fmt.Sprintf("%.3f", quantity), nil } - // OKX使用张数 + // OKX uses contract size price, _ := t.GetMarketPrice(symbol) if price == 0 { return fmt.Sprintf("%.0f", quantity), nil @@ -1039,21 +1039,21 @@ func (t *OKXTrader) FormatQuantity(symbol string, quantity float64) (string, err return t.formatSize(sz, inst), nil } -// formatSize 格式化张数 +// formatSize formats contract size func (t *OKXTrader) formatSize(sz float64, inst *OKXInstrument) string { - // 根据lotSz确定精度 + // Determine precision based on lotSz if inst.LotSz >= 1 { return fmt.Sprintf("%.0f", sz) } - // 计算小数位数 + // Calculate decimal places lotSzStr := fmt.Sprintf("%f", inst.LotSz) dotIndex := strings.Index(lotSzStr, ".") if dotIndex == -1 { return fmt.Sprintf("%.0f", sz) } - // 去除尾部0 + // Remove trailing zeros lotSzStr = strings.TrimRight(lotSzStr, "0") precision := len(lotSzStr) - dotIndex - 1 @@ -1061,14 +1061,14 @@ func (t *OKXTrader) formatSize(sz float64, inst *OKXInstrument) string { return fmt.Sprintf(format, sz) } -// GetOrderStatus 获取订单状态 +// GetOrderStatus gets order status func (t *OKXTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { instId := t.convertSymbol(symbol) path := fmt.Sprintf("/api/v5/trade/order?instId=%s&ordId=%s", instId, orderID) data, err := t.doRequest("GET", path, nil) if err != nil { - return nil, fmt.Errorf("获取订单状态失败: %w", err) + return nil, fmt.Errorf("failed to get order status: %w", err) } var orders []struct { @@ -1088,7 +1088,7 @@ func (t *OKXTrader) GetOrderStatus(symbol string, orderID string) (map[string]in } if len(orders) == 0 { - return nil, fmt.Errorf("未找到订单") + return nil, fmt.Errorf("order not found") } order := orders[0] @@ -1098,7 +1098,7 @@ func (t *OKXTrader) GetOrderStatus(symbol string, orderID string) (map[string]in cTime, _ := strconv.ParseInt(order.CTime, 10, 64) uTime, _ := strconv.ParseInt(order.UTime, 10, 64) - // 状态映射 + // Status mapping statusMap := map[string]string{ "filled": "FILLED", "live": "NEW", @@ -1121,11 +1121,11 @@ func (t *OKXTrader) GetOrderStatus(symbol string, orderID string) (map[string]in "type": order.OrdType, "time": cTime, "updateTime": uTime, - "commission": -fee, // OKX返回的是负数 + "commission": -fee, // OKX returns negative value }, nil } -// OKX 订单标签 +// OKX order tag var okxTag = func() string { b, _ := base64.StdEncoding.DecodeString("NGMzNjNjODFlZGM1QkNERQ==") return string(b) diff --git a/trader/order_sync.go b/trader/order_sync.go index 159983dd..b3b7be5c 100644 --- a/trader/order_sync.go +++ b/trader/order_sync.go @@ -8,19 +8,19 @@ import ( "time" ) -// OrderSyncManager 订单状态同步管理器 -// 负责定期扫描所有 NEW 状态的订单,并更新其状态 +// OrderSyncManager Order status synchronization manager +// Responsible for periodically scanning all NEW status orders and updating their status type OrderSyncManager struct { store *store.Store interval time.Duration stopCh chan struct{} wg sync.WaitGroup - traderCache map[string]Trader // trader_id -> Trader 实例缓存 - configCache map[string]*store.TraderFullConfig // trader_id -> 配置缓存 + traderCache map[string]Trader // trader_id -> Trader instance cache + configCache map[string]*store.TraderFullConfig // trader_id -> config cache cacheMutex sync.RWMutex } -// NewOrderSyncManager 创建订单同步管理器 +// NewOrderSyncManager Create order synchronization manager func NewOrderSyncManager(st *store.Store, interval time.Duration) *OrderSyncManager { if interval == 0 { interval = 10 * time.Second @@ -34,32 +34,32 @@ func NewOrderSyncManager(st *store.Store, interval time.Duration) *OrderSyncMana } } -// Start 启动订单同步服务 +// Start Start order synchronization service func (m *OrderSyncManager) Start() { m.wg.Add(1) go m.run() - logger.Info("📦 订单同步管理器已启动") + logger.Info("📦 Order sync manager started") } -// Stop 停止订单同步服务 +// Stop Stop order synchronization service func (m *OrderSyncManager) Stop() { close(m.stopCh) m.wg.Wait() - // 清理缓存 + // Clear cache m.cacheMutex.Lock() m.traderCache = make(map[string]Trader) m.configCache = make(map[string]*store.TraderFullConfig) m.cacheMutex.Unlock() - logger.Info("📦 订单同步管理器已停止") + logger.Info("📦 Order sync manager stopped") } -// run 主循环 +// run Main loop func (m *OrderSyncManager) run() { defer m.wg.Done() - // 启动时立即执行一次 + // Execute immediately on startup m.syncOrders() ticker := time.NewTicker(m.interval) @@ -75,12 +75,12 @@ func (m *OrderSyncManager) run() { } } -// syncOrders 同步所有待处理订单 +// syncOrders Synchronize all pending orders func (m *OrderSyncManager) syncOrders() { - // 获取所有 NEW 状态的订单 + // Get all NEW status orders orders, err := m.store.Order().GetAllPendingOrders() if err != nil { - logger.Infof("⚠️ 获取待处理订单失败: %v", err) + logger.Infof("⚠️ Failed to get pending orders: %v", err) return } @@ -88,26 +88,26 @@ func (m *OrderSyncManager) syncOrders() { return } - logger.Infof("📦 开始同步 %d 个待处理订单...", len(orders)) + logger.Infof("📦 Starting to sync %d pending orders...", len(orders)) - // 按 trader_id 分组 + // Group by trader_id ordersByTrader := make(map[string][]*store.TraderOrder) for _, order := range orders { ordersByTrader[order.TraderID] = append(ordersByTrader[order.TraderID], order) } - // 逐个 trader 处理 + // Process each trader for traderID, traderOrders := range ordersByTrader { m.syncTraderOrders(traderID, traderOrders) } } -// syncTraderOrders 同步单个 trader 的订单 +// syncTraderOrders Synchronize orders for a single trader func (m *OrderSyncManager) syncTraderOrders(traderID string, orders []*store.TraderOrder) { - // 获取或创建 trader 实例 + // Get or create trader instance trader, err := m.getOrCreateTrader(traderID) if err != nil { - logger.Infof("⚠️ 获取 trader 实例失败 (ID: %s): %v", traderID, err) + logger.Infof("⚠️ Failed to get trader instance (ID: %s): %v", traderID, err) return } @@ -116,13 +116,13 @@ func (m *OrderSyncManager) syncTraderOrders(traderID string, orders []*store.Tra } } -// syncSingleOrder 同步单个订单状态 +// syncSingleOrder Synchronize single order status func (m *OrderSyncManager) syncSingleOrder(trader Trader, order *store.TraderOrder) { status, err := trader.GetOrderStatus(order.Symbol, order.OrderID) if err != nil { - // 查询失败,检查订单创建时间,超过一定时间假设已成交 + // Query failed, check order creation time, assume filled after certain time if time.Since(order.CreatedAt) > 5*time.Minute { - logger.Infof("⚠️ 订单查询超时,假设已成交 (ID: %s)", order.OrderID) + logger.Infof("⚠️ Order query timeout, assuming filled (ID: %s)", order.OrderID) m.markOrderFilled(order, 0, 0, 0) } return @@ -136,7 +136,7 @@ func (m *OrderSyncManager) syncSingleOrder(trader Trader, order *store.TraderOrd executedQty, _ := status["executedQty"].(float64) commission, _ := status["commission"].(float64) - // 如果 API 未返回数量,使用原始数量 + // If API doesn't return quantity, use original quantity if executedQty == 0 { executedQty = order.Quantity } @@ -146,16 +146,16 @@ func (m *OrderSyncManager) syncSingleOrder(trader Trader, order *store.TraderOrd case "CANCELED", "EXPIRED": order.Status = statusStr if err := m.store.Order().Update(order); err != nil { - logger.Infof("⚠️ 更新订单状态失败: %v", err) + logger.Infof("⚠️ Failed to update order status: %v", err) } else { - logger.Infof("📦 订单状态更新: %s (ID: %s)", statusStr, order.OrderID) + logger.Infof("📦 Order status updated: %s (ID: %s)", statusStr, order.OrderID) } } } -// markOrderFilled 标记订单已成交 +// markOrderFilled Mark order as filled func (m *OrderSyncManager) markOrderFilled(order *store.TraderOrder, avgPrice, executedQty, commission float64) { - // 如果 avgPrice 为 0,使用订单价格 + // If avgPrice is 0, use order price if avgPrice == 0 { avgPrice = order.Price } @@ -163,14 +163,14 @@ func (m *OrderSyncManager) markOrderFilled(order *store.TraderOrder, avgPrice, e executedQty = order.Quantity } - // 计算已实现盈亏(仅平仓订单) + // Calculate realized PnL (only for closing orders) var realizedPnL float64 if (order.Action == "close_long" || order.Action == "close_short") && order.EntryPrice > 0 && avgPrice > 0 { if order.Action == "close_long" { - // 平多盈亏 = (平仓价 - 开仓价) * 数量 + // Long close PnL = (close price - entry price) * quantity realizedPnL = (avgPrice - order.EntryPrice) * executedQty } else { - // 平空盈亏 = (开仓价 - 平仓价) * 数量 + // Short close PnL = (entry price - close price) * quantity realizedPnL = (order.EntryPrice - avgPrice) * executedQty } } @@ -183,19 +183,19 @@ func (m *OrderSyncManager) markOrderFilled(order *store.TraderOrder, avgPrice, e order.FilledAt = time.Now() if err := m.store.Order().Update(order); err != nil { - logger.Infof("⚠️ 更新订单状态失败: %v", err) + logger.Infof("⚠️ Failed to update order status: %v", err) } else { if realizedPnL != 0 { - logger.Infof("✅ 订单已成交 (ID: %s, avgPrice: %.4f, qty: %.4f, PnL: %.2f)", + logger.Infof("✅ Order filled (ID: %s, avgPrice: %.4f, qty: %.4f, PnL: %.2f)", order.OrderID, avgPrice, executedQty, realizedPnL) } else { - logger.Infof("✅ 订单已成交 (ID: %s, avgPrice: %.4f, qty: %.4f)", + logger.Infof("✅ Order filled (ID: %s, avgPrice: %.4f, qty: %.4f)", order.OrderID, avgPrice, executedQty) } } } -// getOrCreateTrader 获取或创建 trader 实例 +// getOrCreateTrader Get or create trader instance func (m *OrderSyncManager) getOrCreateTrader(traderID string) (Trader, error) { m.cacheMutex.RLock() trader, exists := m.traderCache[traderID] @@ -205,17 +205,17 @@ func (m *OrderSyncManager) getOrCreateTrader(traderID string) (Trader, error) { return trader, nil } - // 需要创建新的 trader 实例 - // 首先获取 trader 配置 + // Need to create new trader instance + // First get trader config config, err := m.getTraderConfig(traderID) if err != nil { - return nil, fmt.Errorf("获取 trader 配置失败: %w", err) + return nil, fmt.Errorf("failed to get trader config: %w", err) } - // 根据交易所类型创建 trader + // Create trader based on exchange type trader, err = m.createTrader(config) if err != nil { - return nil, fmt.Errorf("创建 trader 实例失败: %w", err) + return nil, fmt.Errorf("failed to create trader instance: %w", err) } m.cacheMutex.Lock() @@ -225,7 +225,7 @@ func (m *OrderSyncManager) getOrCreateTrader(traderID string) (Trader, error) { return trader, nil } -// getTraderConfig 获取 trader 配置 +// getTraderConfig Get trader configuration func (m *OrderSyncManager) getTraderConfig(traderID string) (*store.TraderFullConfig, error) { m.cacheMutex.RLock() config, exists := m.configCache[traderID] @@ -235,11 +235,11 @@ func (m *OrderSyncManager) getTraderConfig(traderID string) (*store.TraderFullCo return config, nil } - // 从数据库获取 - 需要找到 trader 对应的 userID - // 首先查询所有 traders 找到对应的 userID + // Get from database - need to find trader's corresponding userID + // First query all traders to find corresponding userID traders, err := m.store.Trader().ListAll() if err != nil { - return nil, fmt.Errorf("获取 trader 列表失败: %w", err) + return nil, fmt.Errorf("failed to get trader list: %w", err) } var userID string @@ -251,7 +251,7 @@ func (m *OrderSyncManager) getTraderConfig(traderID string) (*store.TraderFullCo } if userID == "" { - return nil, fmt.Errorf("找不到 trader: %s", traderID) + return nil, fmt.Errorf("trader not found: %s", traderID) } config, err = m.store.Trader().GetFullConfig(userID, traderID) @@ -266,11 +266,11 @@ func (m *OrderSyncManager) getTraderConfig(traderID string) (*store.TraderFullCo return config, nil } -// createTrader 根据配置创建 trader 实例 +// createTrader Create trader instance based on configuration func (m *OrderSyncManager) createTrader(config *store.TraderFullConfig) (Trader, error) { exchange := config.Exchange - // 使用 exchange.ID 判断具体的交易所,而不是 exchange.Type (cex/dex) + // Use exchange.ID to determine specific exchange, not exchange.Type (cex/dex) switch exchange.ID { case "binance": return NewFuturesTrader(exchange.APIKey, exchange.SecretKey, config.Trader.UserID), nil @@ -299,11 +299,11 @@ func (m *OrderSyncManager) createTrader(config *store.TraderFullConfig) (Trader, return NewLighterTrader(exchange.LighterPrivateKey, exchange.LighterWalletAddr, exchange.Testnet) default: - return nil, fmt.Errorf("不支持的交易所: %s", exchange.ID) + return nil, fmt.Errorf("unsupported exchange: %s", exchange.ID) } } -// InvalidateCache 使缓存失效(当配置变更时调用) +// InvalidateCache Invalidate cache (call when configuration changes) func (m *OrderSyncManager) InvalidateCache(traderID string) { m.cacheMutex.Lock() defer m.cacheMutex.Unlock() diff --git a/trader/position_sync.go b/trader/position_sync.go index 05b586e0..17a2602d 100644 --- a/trader/position_sync.go +++ b/trader/position_sync.go @@ -8,19 +8,19 @@ import ( "time" ) -// PositionSyncManager 仓位状态同步管理器 -// 负责定期同步交易所仓位,检测手动平仓等变化 +// PositionSyncManager Position status synchronization manager +// Responsible for periodically synchronizing exchange positions, detecting manual closures and other changes type PositionSyncManager struct { store *store.Store interval time.Duration stopCh chan struct{} wg sync.WaitGroup - traderCache map[string]Trader // trader_id -> Trader 实例缓存 - configCache map[string]*store.TraderFullConfig // trader_id -> 配置缓存 + traderCache map[string]Trader // trader_id -> Trader instance cache + configCache map[string]*store.TraderFullConfig // trader_id -> config cache cacheMutex sync.RWMutex } -// NewPositionSyncManager 创建仓位同步管理器 +// NewPositionSyncManager Create position synchronization manager func NewPositionSyncManager(st *store.Store, interval time.Duration) *PositionSyncManager { if interval == 0 { interval = 10 * time.Second @@ -34,32 +34,32 @@ func NewPositionSyncManager(st *store.Store, interval time.Duration) *PositionSy } } -// Start 启动仓位同步服务 +// Start Start position synchronization service func (m *PositionSyncManager) Start() { m.wg.Add(1) go m.run() - logger.Info("📊 仓位同步管理器已启动") + logger.Info("📊 Position sync manager started") } -// Stop 停止仓位同步服务 +// Stop Stop position synchronization service func (m *PositionSyncManager) Stop() { close(m.stopCh) m.wg.Wait() - // 清理缓存 + // Clear cache m.cacheMutex.Lock() m.traderCache = make(map[string]Trader) m.configCache = make(map[string]*store.TraderFullConfig) m.cacheMutex.Unlock() - logger.Info("📊 仓位同步管理器已停止") + logger.Info("📊 Position sync manager stopped") } -// run 主循环 +// run Main loop func (m *PositionSyncManager) run() { defer m.wg.Done() - // 启动时立即执行一次 + // Execute immediately on startup m.syncPositions() ticker := time.NewTicker(m.interval) @@ -75,12 +75,12 @@ func (m *PositionSyncManager) run() { } } -// syncPositions 同步所有仓位状态 +// syncPositions Synchronize all position statuses func (m *PositionSyncManager) syncPositions() { - // 获取所有 OPEN 状态的仓位 + // Get all OPEN status positions localPositions, err := m.store.Position().GetAllOpenPositions() if err != nil { - logger.Infof("⚠️ 获取本地仓位失败: %v", err) + logger.Infof("⚠️ Failed to get local positions: %v", err) return } @@ -88,35 +88,35 @@ func (m *PositionSyncManager) syncPositions() { return } - // 按 trader_id 分组 + // Group by trader_id positionsByTrader := make(map[string][]*store.TraderPosition) for _, pos := range localPositions { positionsByTrader[pos.TraderID] = append(positionsByTrader[pos.TraderID], pos) } - // 逐个 trader 处理 + // Process each trader for traderID, traderPositions := range positionsByTrader { m.syncTraderPositions(traderID, traderPositions) } } -// syncTraderPositions 同步单个 trader 的仓位 +// syncTraderPositions Synchronize positions for a single trader func (m *PositionSyncManager) syncTraderPositions(traderID string, localPositions []*store.TraderPosition) { - // 获取或创建 trader 实例 + // Get or create trader instance trader, err := m.getOrCreateTrader(traderID) if err != nil { - logger.Infof("⚠️ 获取 trader 实例失败 (ID: %s): %v", traderID, err) + logger.Infof("⚠️ Failed to get trader instance (ID: %s): %v", traderID, err) return } - // 获取交易所当前仓位 + // Get current exchange positions exchangePositions, err := trader.GetPositions() if err != nil { - logger.Infof("⚠️ 获取交易所仓位失败 (ID: %s): %v", traderID, err) + logger.Infof("⚠️ Failed to get exchange positions (ID: %s): %v", traderID, err) return } - // 构建交易所仓位 map: symbol_side -> position + // Build exchange position map: symbol_side -> position exchangeMap := make(map[string]map[string]interface{}) for _, pos := range exchangePositions { symbol, _ := pos["symbol"].(string) @@ -128,41 +128,41 @@ func (m *PositionSyncManager) syncTraderPositions(traderID string, localPosition exchangeMap[key] = pos } - // 对比本地和交易所仓位 + // Compare local and exchange positions for _, localPos := range localPositions { key := fmt.Sprintf("%s_%s", localPos.Symbol, localPos.Side) exchangePos, exists := exchangeMap[key] if !exists { - // 交易所没有这个仓位了 → 已被平仓 + // Exchange doesn't have this position → it has been closed m.closeLocalPosition(localPos, trader, "manual") continue } - // 检查数量是否为0或很小 + // Check if quantity is 0 or very small qty := getFloatFromMap(exchangePos, "positionAmt") if qty < 0 { - qty = -qty // 空仓数量是负的 + qty = -qty // Short position quantity is negative } if qty < 0.0000001 { - // 数量为0,仓位已平 + // Quantity is 0, position closed m.closeLocalPosition(localPos, trader, "manual") } } } -// closeLocalPosition 标记本地仓位为已平仓 +// closeLocalPosition Mark local position as closed func (m *PositionSyncManager) closeLocalPosition(pos *store.TraderPosition, trader Trader, reason string) { - // 尝试获取最后成交价作为平仓价 - exitPrice := pos.EntryPrice // 默认用开仓价 + // Try to get last trade price as exit price + exitPrice := pos.EntryPrice // Default to entry price - // 尝试从交易所获取最新价格 + // Try to get latest price from exchange if price, err := trader.GetMarketPrice(pos.Symbol); err == nil && price > 0 { exitPrice = price } - // 计算盈亏 + // Calculate PnL var realizedPnL float64 if pos.Side == "LONG" { realizedPnL = (exitPrice - pos.EntryPrice) * pos.Quantity @@ -170,25 +170,25 @@ func (m *PositionSyncManager) closeLocalPosition(pos *store.TraderPosition, trad realizedPnL = (pos.EntryPrice - exitPrice) * pos.Quantity } - // 更新数据库 + // Update database err := m.store.Position().ClosePosition( pos.ID, exitPrice, - "", // 手动平仓没有订单ID + "", // Manual close has no order ID realizedPnL, - 0, // 手动平仓无法获取手续费 + 0, // Manual close cannot get fee reason, ) if err != nil { - logger.Infof("⚠️ 更新仓位状态失败: %v", err) + logger.Infof("⚠️ Failed to update position status: %v", err) } else { - logger.Infof("📊 仓位已平仓 [%s] %s %s @ %.4f → %.4f, PnL: %.2f (%s)", + logger.Infof("📊 Position closed [%s] %s %s @ %.4f → %.4f, PnL: %.2f (%s)", pos.TraderID[:8], pos.Symbol, pos.Side, pos.EntryPrice, exitPrice, realizedPnL, reason) } } -// getOrCreateTrader 获取或创建 trader 实例 +// getOrCreateTrader Get or create trader instance func (m *PositionSyncManager) getOrCreateTrader(traderID string) (Trader, error) { m.cacheMutex.RLock() trader, exists := m.traderCache[traderID] @@ -198,15 +198,15 @@ func (m *PositionSyncManager) getOrCreateTrader(traderID string) (Trader, error) return trader, nil } - // 需要创建新的 trader 实例 + // Need to create new trader instance config, err := m.getTraderConfig(traderID) if err != nil { - return nil, fmt.Errorf("获取 trader 配置失败: %w", err) + return nil, fmt.Errorf("failed to get trader config: %w", err) } trader, err = m.createTrader(config) if err != nil { - return nil, fmt.Errorf("创建 trader 实例失败: %w", err) + return nil, fmt.Errorf("failed to create trader instance: %w", err) } m.cacheMutex.Lock() @@ -216,7 +216,7 @@ func (m *PositionSyncManager) getOrCreateTrader(traderID string) (Trader, error) return trader, nil } -// getTraderConfig 获取 trader 配置 +// getTraderConfig Get trader configuration func (m *PositionSyncManager) getTraderConfig(traderID string) (*store.TraderFullConfig, error) { m.cacheMutex.RLock() config, exists := m.configCache[traderID] @@ -226,10 +226,10 @@ func (m *PositionSyncManager) getTraderConfig(traderID string) (*store.TraderFul return config, nil } - // 从数据库获取 + // Get from database traders, err := m.store.Trader().ListAll() if err != nil { - return nil, fmt.Errorf("获取 trader 列表失败: %w", err) + return nil, fmt.Errorf("failed to get trader list: %w", err) } var userID string @@ -241,7 +241,7 @@ func (m *PositionSyncManager) getTraderConfig(traderID string) (*store.TraderFul } if userID == "" { - return nil, fmt.Errorf("找不到 trader: %s", traderID) + return nil, fmt.Errorf("trader not found: %s", traderID) } config, err = m.store.Trader().GetFullConfig(userID, traderID) @@ -256,11 +256,11 @@ func (m *PositionSyncManager) getTraderConfig(traderID string) (*store.TraderFul return config, nil } -// createTrader 根据配置创建 trader 实例 +// createTrader Create trader instance based on configuration func (m *PositionSyncManager) createTrader(config *store.TraderFullConfig) (Trader, error) { exchange := config.Exchange - // 使用 exchange.ID 判断具体的交易所,而不是 exchange.Type (cex/dex) + // Use exchange.ID to determine specific exchange, not exchange.Type (cex/dex) switch exchange.ID { case "binance": return NewFuturesTrader(exchange.APIKey, exchange.SecretKey, config.Trader.UserID), nil @@ -289,11 +289,11 @@ func (m *PositionSyncManager) createTrader(config *store.TraderFullConfig) (Trad return NewLighterTrader(exchange.LighterPrivateKey, exchange.LighterWalletAddr, exchange.Testnet) default: - return nil, fmt.Errorf("不支持的交易所: %s", exchange.ID) + return nil, fmt.Errorf("unsupported exchange: %s", exchange.ID) } } -// InvalidateCache 使缓存失效 +// InvalidateCache Invalidate cache func (m *PositionSyncManager) InvalidateCache(traderID string) { m.cacheMutex.Lock() defer m.cacheMutex.Unlock() @@ -302,7 +302,7 @@ func (m *PositionSyncManager) InvalidateCache(traderID string) { delete(m.configCache, traderID) } -// getFloatFromMap 从 map 中获取 float64 值 +// getFloatFromMap Get float64 value from map func getFloatFromMap(m map[string]interface{}, key string) float64 { if v, ok := m[key]; ok { switch val := v.(type) { diff --git a/trader/trader_test_suite.go b/trader/trader_test_suite.go index 67f2db8b..d1f8032c 100644 --- a/trader/trader_test_suite.go +++ b/trader/trader_test_suite.go @@ -7,20 +7,20 @@ import ( "github.com/stretchr/testify/assert" ) -// TraderTestSuite 通用的 Trader 接口测试套件(基础套件) -// 用于黑盒测试任何实现了 Trader 接口的交易器 +// TraderTestSuite Generic Trader interface test suite (base suite) +// Used for black-box testing any trader that implements the Trader interface // -// 使用方式: -// 1. 创建具体的测试套件结构体,嵌入 TraderTestSuite -// 2. 实现 SetupMocks() 方法来配置 gomonkey mock -// 3. 调用 RunAllTests() 运行所有通用测试 +// Usage: +// 1. Create a concrete test suite struct, embedding TraderTestSuite +// 2. Implement SetupMocks() method to configure gomonkey mocks +// 3. Call RunAllTests() to run all generic tests type TraderTestSuite struct { T *testing.T Trader Trader Patches *gomonkey.Patches } -// NewTraderTestSuite 创建新的基础测试套件 +// NewTraderTestSuite Create new base test suite func NewTraderTestSuite(t *testing.T, trader Trader) *TraderTestSuite { return &TraderTestSuite{ T: t, @@ -29,44 +29,44 @@ func NewTraderTestSuite(t *testing.T, trader Trader) *TraderTestSuite { } } -// Cleanup 清理 mock patches +// Cleanup Clean up mock patches func (s *TraderTestSuite) Cleanup() { if s.Patches != nil { s.Patches.Reset() } } -// RunAllTests 运行所有通用接口测试 -// 注意:调用此方法前,请先通过 SetupMocks 设置好所需的 mock +// RunAllTests Run all generic interface tests +// Note: Before calling this method, please set up required mocks via SetupMocks func (s *TraderTestSuite) RunAllTests() { - // 基础查询方法 + // Basic query methods s.T.Run("GetBalance", func(t *testing.T) { s.TestGetBalance() }) s.T.Run("GetPositions", func(t *testing.T) { s.TestGetPositions() }) s.T.Run("GetMarketPrice", func(t *testing.T) { s.TestGetMarketPrice() }) - // 配置方法 + // Configuration methods s.T.Run("SetLeverage", func(t *testing.T) { s.TestSetLeverage() }) s.T.Run("SetMarginMode", func(t *testing.T) { s.TestSetMarginMode() }) s.T.Run("FormatQuantity", func(t *testing.T) { s.TestFormatQuantity() }) - // 核心交易方法 + // Core trading methods s.T.Run("OpenLong", func(t *testing.T) { s.TestOpenLong() }) s.T.Run("OpenShort", func(t *testing.T) { s.TestOpenShort() }) s.T.Run("CloseLong", func(t *testing.T) { s.TestCloseLong() }) s.T.Run("CloseShort", func(t *testing.T) { s.TestCloseShort() }) - // 止损止盈 + // Stop-loss and take-profit s.T.Run("SetStopLoss", func(t *testing.T) { s.TestSetStopLoss() }) s.T.Run("SetTakeProfit", func(t *testing.T) { s.TestSetTakeProfit() }) - // 订单管理 + // Order management s.T.Run("CancelAllOrders", func(t *testing.T) { s.TestCancelAllOrders() }) s.T.Run("CancelStopOrders", func(t *testing.T) { s.TestCancelStopOrders() }) s.T.Run("CancelStopLossOrders", func(t *testing.T) { s.TestCancelStopLossOrders() }) s.T.Run("CancelTakeProfitOrders", func(t *testing.T) { s.TestCancelTakeProfitOrders() }) } -// TestGetBalance 测试获取账户余额 +// TestGetBalance Test getting account balance func (s *TraderTestSuite) TestGetBalance() { tests := []struct { name string @@ -74,7 +74,7 @@ func (s *TraderTestSuite) TestGetBalance() { validate func(*testing.T, map[string]interface{}) }{ { - name: "成功获取余额", + name: "Successfully get balance", wantError: false, validate: func(t *testing.T, result map[string]interface{}) { assert.NotNil(t, result) @@ -99,7 +99,7 @@ func (s *TraderTestSuite) TestGetBalance() { } } -// TestGetPositions 测试获取持仓 +// TestGetPositions Test getting positions func (s *TraderTestSuite) TestGetPositions() { tests := []struct { name string @@ -107,11 +107,11 @@ func (s *TraderTestSuite) TestGetPositions() { validate func(*testing.T, []map[string]interface{}) }{ { - name: "成功获取持仓列表", + name: "Successfully get position list", wantError: false, validate: func(t *testing.T, positions []map[string]interface{}) { assert.NotNil(t, positions) - // 持仓可以为空数组 + // Positions can be empty array for _, pos := range positions { assert.Contains(t, pos, "symbol") assert.Contains(t, pos, "side") @@ -136,7 +136,7 @@ func (s *TraderTestSuite) TestGetPositions() { } } -// TestGetMarketPrice 测试获取市场价格 +// TestGetMarketPrice Test getting market price func (s *TraderTestSuite) TestGetMarketPrice() { tests := []struct { name string @@ -145,7 +145,7 @@ func (s *TraderTestSuite) TestGetMarketPrice() { validate func(*testing.T, float64) }{ { - name: "成功获取BTC价格", + name: "Successfully get BTC price", symbol: "BTCUSDT", wantError: false, validate: func(t *testing.T, price float64) { @@ -153,7 +153,7 @@ func (s *TraderTestSuite) TestGetMarketPrice() { }, }, { - name: "无效交易对返回错误", + name: "Invalid trading pair returns error", symbol: "INVALIDUSDT", wantError: true, validate: nil, @@ -175,7 +175,7 @@ func (s *TraderTestSuite) TestGetMarketPrice() { } } -// TestSetLeverage 测试设置杠杆 +// TestSetLeverage Test setting leverage func (s *TraderTestSuite) TestSetLeverage() { tests := []struct { name string @@ -184,13 +184,13 @@ func (s *TraderTestSuite) TestSetLeverage() { wantError bool }{ { - name: "设置10倍杠杆", + name: "Set 10x leverage", symbol: "BTCUSDT", leverage: 10, wantError: false, }, { - name: "设置1倍杠杆", + name: "Set 1x leverage", symbol: "ETHUSDT", leverage: 1, wantError: false, @@ -209,7 +209,7 @@ func (s *TraderTestSuite) TestSetLeverage() { } } -// TestSetMarginMode 测试设置仓位模式 +// TestSetMarginMode Test setting margin mode func (s *TraderTestSuite) TestSetMarginMode() { tests := []struct { name string @@ -218,13 +218,13 @@ func (s *TraderTestSuite) TestSetMarginMode() { wantError bool }{ { - name: "设置全仓模式", + name: "Set cross margin mode", symbol: "BTCUSDT", isCrossMargin: true, wantError: false, }, { - name: "设置逐仓模式", + name: "Set isolated margin mode", symbol: "ETHUSDT", isCrossMargin: false, wantError: false, @@ -243,7 +243,7 @@ func (s *TraderTestSuite) TestSetMarginMode() { } } -// TestFormatQuantity 测试数量格式化 +// TestFormatQuantity Test formatting quantity func (s *TraderTestSuite) TestFormatQuantity() { tests := []struct { name string @@ -253,7 +253,7 @@ func (s *TraderTestSuite) TestFormatQuantity() { validate func(*testing.T, string) }{ { - name: "格式化BTC数量", + name: "Format BTC quantity", symbol: "BTCUSDT", quantity: 1.23456789, wantError: false, @@ -262,7 +262,7 @@ func (s *TraderTestSuite) TestFormatQuantity() { }, }, { - name: "格式化小数量", + name: "Format small quantity", symbol: "ETHUSDT", quantity: 0.001, wantError: false, @@ -287,7 +287,7 @@ func (s *TraderTestSuite) TestFormatQuantity() { } } -// TestCancelAllOrders 测试取消所有订单 +// TestCancelAllOrders Test canceling all orders func (s *TraderTestSuite) TestCancelAllOrders() { tests := []struct { name string @@ -295,7 +295,7 @@ func (s *TraderTestSuite) TestCancelAllOrders() { wantError bool }{ { - name: "取消BTC所有订单", + name: "Cancel all BTC orders", symbol: "BTCUSDT", wantError: false, }, @@ -314,10 +314,10 @@ func (s *TraderTestSuite) TestCancelAllOrders() { } // ============================================================ -// 核心交易方法测试 +// Core trading method tests // ============================================================ -// TestOpenLong 测试开多仓 +// TestOpenLong Test opening long position func (s *TraderTestSuite) TestOpenLong() { tests := []struct { name string @@ -328,7 +328,7 @@ func (s *TraderTestSuite) TestOpenLong() { validate func(*testing.T, map[string]interface{}) }{ { - name: "成功开多仓", + name: "Successfully open long", symbol: "BTCUSDT", quantity: 0.01, leverage: 10, @@ -340,9 +340,9 @@ func (s *TraderTestSuite) TestOpenLong() { }, }, { - name: "小数量开仓", + name: "Small quantity long", symbol: "ETHUSDT", - quantity: 0.004, // 增加到 0.004 以满足 Binance Futures 的 10 USDT 最小订单金额要求 (0.004 * 3000 = 12 USDT) + quantity: 0.004, // Increased to 0.004 to meet Binance Futures minimum order value of 10 USDT (0.004 * 3000 = 12 USDT) leverage: 5, wantError: false, validate: func(t *testing.T, result map[string]interface{}) { @@ -366,7 +366,7 @@ func (s *TraderTestSuite) TestOpenLong() { } } -// TestOpenShort 测试开空仓 +// TestOpenShort Test opening short position func (s *TraderTestSuite) TestOpenShort() { tests := []struct { name string @@ -377,7 +377,7 @@ func (s *TraderTestSuite) TestOpenShort() { validate func(*testing.T, map[string]interface{}) }{ { - name: "成功开空仓", + name: "Successfully open short", symbol: "BTCUSDT", quantity: 0.01, leverage: 10, @@ -389,9 +389,9 @@ func (s *TraderTestSuite) TestOpenShort() { }, }, { - name: "小数量开空仓", + name: "Small quantity short", symbol: "ETHUSDT", - quantity: 0.004, // 增加到 0.004 以满足 Binance Futures 的 10 USDT 最小订单金额要求 (0.004 * 3000 = 12 USDT) + quantity: 0.004, // Increased to 0.004 to meet Binance Futures minimum order value of 10 USDT (0.004 * 3000 = 12 USDT) leverage: 5, wantError: false, validate: func(t *testing.T, result map[string]interface{}) { @@ -415,7 +415,7 @@ func (s *TraderTestSuite) TestOpenShort() { } } -// TestCloseLong 测试平多仓 +// TestCloseLong Test closing long position func (s *TraderTestSuite) TestCloseLong() { tests := []struct { name string @@ -425,7 +425,7 @@ func (s *TraderTestSuite) TestCloseLong() { validate func(*testing.T, map[string]interface{}) }{ { - name: "平指定数量", + name: "Close specified quantity", symbol: "BTCUSDT", quantity: 0.01, wantError: false, @@ -435,10 +435,10 @@ func (s *TraderTestSuite) TestCloseLong() { }, }, { - name: "全部平仓_quantity为0_无持仓返回错误", + name: "Close all with quantity=0 returns error when no position", symbol: "ETHUSDT", quantity: 0, - wantError: true, // 当没有持仓时,quantity=0 应该返回错误 + wantError: true, // When no position exists, quantity=0 should return error validate: nil, }, } @@ -458,7 +458,7 @@ func (s *TraderTestSuite) TestCloseLong() { } } -// TestCloseShort 测试平空仓 +// TestCloseShort Test closing short position func (s *TraderTestSuite) TestCloseShort() { tests := []struct { name string @@ -468,7 +468,7 @@ func (s *TraderTestSuite) TestCloseShort() { validate func(*testing.T, map[string]interface{}) }{ { - name: "平指定数量", + name: "Close specified quantity", symbol: "BTCUSDT", quantity: 0.01, wantError: false, @@ -478,10 +478,10 @@ func (s *TraderTestSuite) TestCloseShort() { }, }, { - name: "全部平仓_quantity为0_无持仓返回错误", + name: "Close all with quantity=0 returns error when no position", symbol: "ETHUSDT", quantity: 0, - wantError: true, // 当没有持仓时,quantity=0 应该返回错误 + wantError: true, // When no position exists, quantity=0 should return error validate: nil, }, } @@ -502,10 +502,10 @@ func (s *TraderTestSuite) TestCloseShort() { } // ============================================================ -// 止损止盈测试 +// Stop-loss and take-profit tests // ============================================================ -// TestSetStopLoss 测试设置止损 +// TestSetStopLoss Test setting stop-loss func (s *TraderTestSuite) TestSetStopLoss() { tests := []struct { name string @@ -516,7 +516,7 @@ func (s *TraderTestSuite) TestSetStopLoss() { wantError bool }{ { - name: "多头止损", + name: "Long stop-loss", symbol: "BTCUSDT", positionSide: "LONG", quantity: 0.01, @@ -524,7 +524,7 @@ func (s *TraderTestSuite) TestSetStopLoss() { wantError: false, }, { - name: "空头止损", + name: "Short stop-loss", symbol: "ETHUSDT", positionSide: "SHORT", quantity: 0.1, @@ -545,7 +545,7 @@ func (s *TraderTestSuite) TestSetStopLoss() { } } -// TestSetTakeProfit 测试设置止盈 +// TestSetTakeProfit Test setting take-profit func (s *TraderTestSuite) TestSetTakeProfit() { tests := []struct { name string @@ -556,7 +556,7 @@ func (s *TraderTestSuite) TestSetTakeProfit() { wantError bool }{ { - name: "多头止盈", + name: "Long take-profit", symbol: "BTCUSDT", positionSide: "LONG", quantity: 0.01, @@ -564,7 +564,7 @@ func (s *TraderTestSuite) TestSetTakeProfit() { wantError: false, }, { - name: "空头止盈", + name: "Short take-profit", symbol: "ETHUSDT", positionSide: "SHORT", quantity: 0.1, @@ -585,7 +585,7 @@ func (s *TraderTestSuite) TestSetTakeProfit() { } } -// TestCancelStopOrders 测试取消止盈止损单 +// TestCancelStopOrders Test canceling stop-loss/take-profit orders func (s *TraderTestSuite) TestCancelStopOrders() { tests := []struct { name string @@ -593,7 +593,7 @@ func (s *TraderTestSuite) TestCancelStopOrders() { wantError bool }{ { - name: "取消BTC止盈止损单", + name: "Cancel BTC stop orders", symbol: "BTCUSDT", wantError: false, }, @@ -611,7 +611,7 @@ func (s *TraderTestSuite) TestCancelStopOrders() { } } -// TestCancelStopLossOrders 测试取消止损单 +// TestCancelStopLossOrders Test canceling stop-loss orders func (s *TraderTestSuite) TestCancelStopLossOrders() { tests := []struct { name string @@ -619,7 +619,7 @@ func (s *TraderTestSuite) TestCancelStopLossOrders() { wantError bool }{ { - name: "取消BTC止损单", + name: "Cancel BTC stop-loss orders", symbol: "BTCUSDT", wantError: false, }, @@ -637,7 +637,7 @@ func (s *TraderTestSuite) TestCancelStopLossOrders() { } } -// TestCancelTakeProfitOrders 测试取消止盈单 +// TestCancelTakeProfitOrders Test canceling take-profit orders func (s *TraderTestSuite) TestCancelTakeProfitOrders() { tests := []struct { name string @@ -645,7 +645,7 @@ func (s *TraderTestSuite) TestCancelTakeProfitOrders() { wantError bool }{ { - name: "取消BTC止盈单", + name: "Cancel BTC take-profit orders", symbol: "BTCUSDT", wantError: false, },