From 736d2d385d6fc1145ef08b1cd09eb2d9f41b7d65 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Thu, 12 Mar 2026 16:12:08 +0800 Subject: [PATCH] refactor: optimize codebase encoding --- .env.example | 2 +- .github/workflows/pr-docker-check.yml | 38 +- .../pr-docker-compose-healthcheck.yml | 2 +- Dockerfile.railway | 20 +- api/handler_klines.go | 14 +- backtest/config.go | 2 +- docker-compose.yml | 2 +- kernel/engine.go | 12 +- kernel/engine_prompt.go | 8 +- kernel/formatter.go | 97 +- kernel/prompt_builder.go | 48 +- kernel/prompt_builder_test.go | 8 +- kernel/schema.go | 78 +- market/types.go | 10 +- provider/nofxos/ai500.go | 2 +- provider/nofxos/netflow.go | 2 +- railway/start.sh | 12 +- store/strategy.go | 4 +- telegram/agent/agent.go | 2 +- telegram/agent/manager.go | 2 +- trader/auto_trader_loop.go | 4 +- trader/lighter/orders.go | 2 +- .../components/auth/LoginRequiredOverlay.tsx | 74 +- .../components/backtest/BacktestChartTab.tsx | 25 +- .../backtest/BacktestConfigForm.tsx | 46 +- .../backtest/BacktestOverviewTab.tsx | 27 +- web/src/components/backtest/BacktestPage.tsx | 32 +- .../components/backtest/BacktestRunList.tsx | 7 +- web/src/components/charts/AdvancedChart.tsx | 229 +-- web/src/components/charts/ChartTabs.tsx | 49 +- web/src/components/charts/ChartWithOrders.tsx | 97 +- web/src/components/charts/ComparisonChart.tsx | 14 +- web/src/components/common/ConfirmDialog.tsx | 10 +- web/src/components/common/MetricTooltip.tsx | 4 +- .../components/strategy/CoinSourceEditor.tsx | 111 +- .../components/strategy/GridConfigEditor.tsx | 168 +-- web/src/components/strategy/GridRiskPanel.tsx | 117 +- .../components/strategy/IndicatorEditor.tsx | 163 +-- .../strategy/PromptSectionsEditor.tsx | 37 +- .../strategy/PublishSettingsEditor.tsx | 31 +- .../components/strategy/RiskControlEditor.tsx | 81 +- web/src/components/trader/AITradersPage.tsx | 66 +- .../components/trader/ExchangeConfigModal.tsx | 36 +- .../components/trader/ModelConfigModal.tsx | 66 +- .../components/trader/TelegramConfigModal.tsx | 116 +- .../trader/TraderConfigViewModal.tsx | 37 +- web/src/contexts/AuthContext.tsx | 22 +- web/src/i18n/strategy-translations.ts | 26 + web/src/i18n/translations.ts | 1241 +++++++++++++++++ web/src/lib/api/backtest.ts | 4 +- web/src/lib/api/config.ts | 72 +- web/src/lib/api/data.ts | 25 +- web/src/lib/api/helpers.ts | 2 +- web/src/lib/api/strategies.ts | 18 +- web/src/lib/api/telegram.ts | 8 +- web/src/lib/api/traders.ts | 22 +- web/src/lib/clipboard.ts | 8 +- web/src/pages/DataPage.tsx | 3 +- web/src/pages/StrategyMarketPage.tsx | 118 +- web/src/pages/StrategyStudioPage.tsx | 179 +-- web/src/pages/TraderDashboardPage.tsx | 72 +- 61 files changed, 2301 insertions(+), 1533 deletions(-) diff --git a/.env.example b/.env.example index 22cd7ef2..dc2884e1 100644 --- a/.env.example +++ b/.env.example @@ -61,6 +61,6 @@ DB_NAME=nofx DB_SSLMODE=disable -# 数据库配置 - SQLite(默认) +# Database configuration - SQLite (default) DB_TYPE=sqlite DB_PATH=data/data.db \ No newline at end of file diff --git a/.github/workflows/pr-docker-check.yml b/.github/workflows/pr-docker-check.yml index 9399db59..adfbe555 100644 --- a/.github/workflows/pr-docker-check.yml +++ b/.github/workflows/pr-docker-check.yml @@ -1,7 +1,7 @@ name: PR Docker Build Check -# PR 时只做轻量级构建检查,不推送镜像 -# 策略: 快速验证 amd64 + 抽样检查 arm64 (backend only) +# Lightweight build check on PR only, no image push +# Strategy: Quick verify amd64 + spot check arm64 (backend only) on: pull_request: branches: @@ -18,7 +18,7 @@ on: - '.github/workflows/pr-docker-check.yml' jobs: - # 快速检查: 所有镜像的 amd64 版本 + # Quick check: amd64 builds for all images docker-build-amd64: name: Build Check (amd64) runs-on: ubuntu-22.04 @@ -31,7 +31,7 @@ jobs: include: - name: backend dockerfile: ./docker/Dockerfile.backend - test_run: true # 需要测试运行 + test_run: true # Needs test run - name: frontend dockerfile: ./docker/Dockerfile.frontend test_run: true @@ -51,7 +51,7 @@ jobs: file: ${{ matrix.dockerfile }} platforms: linux/amd64 push: false - load: true # 加载到本地 Docker,用于测试运行 + load: true # Load into local Docker for test run tags: nofx-${{ matrix.name }}:pr-test cache-from: type=gha,scope=${{ matrix.name }}-amd64 cache-to: type=gha,mode=max,scope=${{ matrix.name }}-amd64 @@ -66,12 +66,12 @@ jobs: run: | echo "🧪 Testing container startup..." - # 启动容器 + # Start container docker run -d --name test-${{ matrix.name }} \ --health-cmd="exit 0" \ nofx-${{ matrix.name }}:pr-test - # 等待容器启动 (最多 30 秒) + # Wait for container to start (up to 30 seconds) for i in {1..30}; do if docker ps | grep -q test-${{ matrix.name }}; then echo "✅ Container started successfully" @@ -93,7 +93,7 @@ jobs: echo "📦 Image size: ${SIZE_MB} MB" - # 警告阈值 + # Warning thresholds if [ "${{ matrix.name }}" = "backend" ] && [ $SIZE_MB -gt 500 ]; then echo "⚠️ Warning: Backend image is larger than 500MB" elif [ "${{ matrix.name }}" = "frontend" ] && [ $SIZE_MB -gt 200 ]; then @@ -102,10 +102,10 @@ jobs: echo "✅ Image size is reasonable" fi - # ARM64 原生构建检查: 使用 GitHub 原生 ARM64 runner (快速!) + # ARM64 native build check: Uses GitHub native ARM64 runner (fast!) docker-build-arm64-native: name: Build Check (arm64 native - backend) - runs-on: ubuntu-22.04-arm # 原生 ARM64 runner + runs-on: ubuntu-22.04-arm # Native ARM64 runner permissions: contents: read @@ -113,19 +113,19 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - # 原生 ARM64 不需要 QEMU,直接构建 + # Native ARM64 does not need QEMU, builds directly - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build backend image (arm64 native) uses: docker/build-push-action@v5 - timeout-minutes: 15 # 原生构建更快! + timeout-minutes: 15 # Native builds are faster! with: context: . file: ./docker/Dockerfile.backend platforms: linux/arm64 push: false - load: true # 加载到本地,用于测试 + load: true # Load locally for testing tags: nofx-backend:pr-test-arm64 cache-from: type=gha,scope=backend-arm64 cache-to: type=gha,mode=max,scope=backend-arm64 @@ -139,12 +139,12 @@ jobs: run: | echo "🧪 Testing ARM64 container startup..." - # 启动容器 + # Start container docker run -d --name test-backend-arm64 \ --health-cmd="exit 0" \ nofx-backend:pr-test-arm64 - # 等待启动 + # Wait for startup for i in {1..30}; do if docker ps | grep -q test-backend-arm64; then echo "✅ ARM64 container started successfully" @@ -165,14 +165,14 @@ jobs: echo "Using GitHub native ARM64 runner - no QEMU needed!" echo "Build time is ~3x faster than emulation" - # 汇总检查结果 + # Aggregate check results check-summary: name: Docker Build Summary needs: [docker-build-amd64, docker-build-arm64-native] runs-on: ubuntu-22.04 if: always() permissions: - pull-requests: write # 用于发布评论 + pull-requests: write # For posting comments steps: - name: Check build results id: check @@ -180,7 +180,7 @@ jobs: echo "## 🐳 Docker Build Check Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - # 检查 amd64 构建 + # Check amd64 build if [[ "${{ needs.docker-build-amd64.result }}" == "success" ]]; then echo "✅ **AMD64 builds**: All passed" >> $GITHUB_STEP_SUMMARY AMD64_OK=true @@ -189,7 +189,7 @@ jobs: AMD64_OK=false fi - # 检查 arm64 构建 + # Check arm64 build if [[ "${{ needs.docker-build-arm64-native.result }}" == "success" ]]; then echo "✅ **ARM64 build** (native): Backend passed (frontend will be verified after merge)" >> $GITHUB_STEP_SUMMARY ARM64_OK=true diff --git a/.github/workflows/pr-docker-compose-healthcheck.yml b/.github/workflows/pr-docker-compose-healthcheck.yml index 139477e5..0ff392f1 100644 --- a/.github/workflows/pr-docker-compose-healthcheck.yml +++ b/.github/workflows/pr-docker-compose-healthcheck.yml @@ -1,6 +1,6 @@ name: PR Docker Compose Healthcheck -# 驗證 docker-compose.yml 的 healthcheck 配置在 Alpine 容器中正常工作 +# Verify docker-compose.yml healthcheck config works correctly in Alpine containers on: pull_request: branches: diff --git a/Dockerfile.railway b/Dockerfile.railway index e6127e20..71420aaa 100644 --- a/Dockerfile.railway +++ b/Dockerfile.railway @@ -1,37 +1,37 @@ -# Railway All-in-One: 复用现有 GHCR 镜像 -# 从现有镜像提取内容,合并到一个容器 +# Railway All-in-One: Reuse existing GHCR images +# Extract content from existing images and merge into a single container -# 从后端镜像提取二进制 +# Extract binary from backend image FROM ghcr.io/nofxaios/nofx/nofx-backend:latest AS backend -# 从前端镜像提取静态文件 +# Extract static files from frontend image FROM ghcr.io/nofxaios/nofx/nofx-frontend:latest AS frontend -# 最终镜像 +# Final image FROM alpine:latest RUN apk add --no-cache ca-certificates tzdata sqlite nginx openssl gettext -# 复制后端二进制 +# Copy backend binary COPY --from=backend /app/nofx /app/nofx -# 复制 TA-Lib 库 +# Copy TA-Lib libraries COPY --from=backend /usr/local/lib/libta_lib* /usr/local/lib/ RUN ldconfig /usr/local/lib 2>/dev/null || true -# 复制前端静态文件 +# Copy frontend static files COPY --from=frontend /usr/share/nginx/html /usr/share/nginx/html WORKDIR /app RUN mkdir -p /app/data -# 启动脚本(包含 nginx 配置生成) +# Startup script (includes nginx config generation) COPY railway/start.sh /app/start.sh RUN chmod +x /app/start.sh ENV DB_PATH=/app/data/data.db -# Railway 会自动设置 PORT 环境变量 +# Railway automatically sets the PORT environment variable EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ diff --git a/api/handler_klines.go b/api/handler_klines.go index cebee1c9..f5c787ee 100644 --- a/api/handler_klines.go +++ b/api/handler_klines.go @@ -187,7 +187,7 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i } // Convert coinank kline format to market.Kline format - // Coinank: Volume = BTC 数量, Quantity = USDT 成交额 + // Coinank: Volume = BTC quantity, Quantity = USDT turnover klines := make([]market.Kline, len(coinankKlines)) for i, ck := range coinankKlines { klines[i] = market.Kline{ @@ -196,8 +196,8 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i High: ck.High, Low: ck.Low, Close: ck.Close, - Volume: ck.Volume, // BTC 数量 - QuoteVolume: ck.Quantity, // USDT 成交额 + Volume: ck.Volume, // BTC quantity + QuoteVolume: ck.Quantity, // USDT turnover CloseTime: ck.EndTime, } } @@ -229,8 +229,8 @@ func (s *Server) getKlinesFromAlpaca(symbol, interval string, limit int) ([]mark High: bar.High, Low: bar.Low, Close: bar.Close, - Volume: float64(bar.Volume), // 股数 - QuoteVolume: float64(bar.Volume) * bar.Close, // 成交额 = 股数 * 收盘价 (USD) + Volume: float64(bar.Volume), // share count + QuoteVolume: float64(bar.Volume) * bar.Close, // turnover = shares * close price (USD) CloseTime: bar.Timestamp.UnixMilli(), } } @@ -311,8 +311,8 @@ func (s *Server) getKlinesFromHyperliquid(symbol, interval string, limit int) ([ High: high, Low: low, Close: close, - Volume: volume, // 合约数量 - QuoteVolume: volume * close, // 成交额 (USD) + Volume: volume, // contract quantity + QuoteVolume: volume * close, // turnover (USD) CloseTime: candle.CloseTime, } } diff --git a/backtest/config.go b/backtest/config.go index 5cb5d09b..629fac97 100644 --- a/backtest/config.go +++ b/backtest/config.go @@ -195,7 +195,7 @@ func (cfg *BacktestConfig) ToStrategyConfig() *store.StrategyConfig { if cfg.loadedStrategy != nil { result := *cfg.loadedStrategy // Make a copy - // Override coin source with backtest symbols (回测指定的币对优先) + // Override coin source with backtest symbols (backtest-specified pairs take priority) if len(cfg.Symbols) > 0 { result.CoinSource.SourceType = "static" result.CoinSource.StaticCoins = cfg.Symbols diff --git a/docker-compose.yml b/docker-compose.yml index bebeef92..82723977 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: dockerfile: ./docker/Dockerfile.backend container_name: nofx-trading restart: unless-stopped - stop_grace_period: 30s # 允许应用有 30 秒时间优雅关闭 + stop_grace_period: 30s # Allow the app 30 seconds for graceful shutdown ports: - "${NOFX_BACKEND_PORT:-8080}:8080" - "6060:6060" # pprof profiling diff --git a/kernel/engine.go b/kernel/engine.go index 79a5edd7..1fe9c958 100644 --- a/kernel/engine.go +++ b/kernel/engine.go @@ -247,7 +247,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) { return e.filterExcludedCoins(candidates), nil case "ai500": - // 检查 use_ai500 标志,如果为 false 则回退到静态币种 + // Check use_ai500 flag; if false, fall back to static coins if !coinSource.UseAI500 { logger.Infof("⚠️ source_type is 'ai500' but use_ai500 is false, falling back to static coins") for _, symbol := range coinSource.StaticCoins { @@ -263,11 +263,11 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) { if err != nil { return nil, err } - // 空列表是正常情况,直接返回 + // Empty list is a normal condition, return directly return e.filterExcludedCoins(coins), nil case "oi_top": - // 检查 use_oi_top 标志,如果为 false 则回退到静态币种 + // Check use_oi_top flag; if false, fall back to static coins if !coinSource.UseOITop { logger.Infof("⚠️ source_type is 'oi_top' but use_oi_top is false, falling back to static coins") for _, symbol := range coinSource.StaticCoins { @@ -283,11 +283,11 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) { if err != nil { return nil, err } - // 空列表是正常情况,直接返回 + // Empty list is a normal condition, return directly return e.filterExcludedCoins(coins), nil case "oi_low": - // 持仓减少榜,适合做空 + // OI decrease ranking, suitable for short positions if !coinSource.UseOILow { logger.Infof("⚠️ source_type is 'oi_low' but use_oi_low is false, falling back to static coins") for _, symbol := range coinSource.StaticCoins { @@ -303,7 +303,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) { if err != nil { return nil, err } - // 空列表是正常情况,直接返回 + // Empty list is a normal condition, return directly return e.filterExcludedCoins(coins), nil case "hyper_all": diff --git a/kernel/engine_prompt.go b/kernel/engine_prompt.go index 15c79a38..9f1b7ee8 100644 --- a/kernel/engine_prompt.go +++ b/kernel/engine_prompt.go @@ -441,7 +441,7 @@ func (e *StrategyEngine) formatPositionInfo(index int, pos PositionInfo, ctx *Co func (e *StrategyEngine) formatCoinSourceTag(sources []string) string { if len(sources) > 1 { - // 多信号源组合 + // Multiple signal source combination hasAI500 := false hasOITop := false hasOILow := false @@ -482,9 +482,9 @@ func (e *StrategyEngine) formatCoinSourceTag(sources []string) string { case "ai500": return " (AI500)" case "oi_top": - return " (OI_Top 持仓增加)" + return " (OI_Top OI increase)" case "oi_low": - return " (OI_Low 持仓减少)" + return " (OI_Low OI decrease)" case "static": return " (Manual selection)" case "hyper_all": @@ -504,7 +504,7 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string { var sb strings.Builder indicators := e.config.Indicators - // 明确标注币种 + // Clearly label the coin symbol sb.WriteString(fmt.Sprintf("=== %s Market Data ===\n\n", data.Symbol)) sb.WriteString(fmt.Sprintf("current_price = %.4f", data.CurrentPrice)) diff --git a/kernel/formatter.go b/kernel/formatter.go index c4a2e454..ae197217 100644 --- a/kernel/formatter.go +++ b/kernel/formatter.go @@ -10,49 +10,50 @@ import ( ) // ============================================================================ -// AI Data Formatter - AI数据格式化器 +// AI Data Formatter // ============================================================================ -// 将交易上下文转换为AI友好的格式,确保AI能够100%理解数据 +// Converts trading context into AI-friendly format, ensuring AI fully +// understands the data regardless of language. // ============================================================================ -// FormatContextForAI 将交易上下文格式化为AI可理解的文本(包含Schema) +// FormatContextForAI formats trading context into AI-readable text (including schema) func FormatContextForAI(ctx *Context, lang Language) string { var sb strings.Builder - // 1. 添加Schema说明(让AI理解数据格式) + // 1. Add schema description (so AI understands data format) sb.WriteString(GetSchemaPrompt(lang)) sb.WriteString("\n---\n\n") - // 2. 当前状态概览 + // 2. Current state overview sb.WriteString(formatContextData(ctx, lang)) return sb.String() } -// FormatContextDataOnly 仅格式化上下文数据,不包含Schema(用于已有Schema的场景) +// FormatContextDataOnly formats context data only, without schema (for use when schema is already present) func FormatContextDataOnly(ctx *Context, lang Language) string { return formatContextData(ctx, lang) } -// formatContextData 格式化核心数据部分 +// formatContextData formats the core data section func formatContextData(ctx *Context, lang Language) string { var sb strings.Builder - // 1. 当前状态概览 + // 1. Current state overview if lang == LangChinese { sb.WriteString(formatHeaderZH(ctx)) } else { sb.WriteString(formatHeaderEN(ctx)) } - // 3. 账户信息 + // 3. Account information if lang == LangChinese { sb.WriteString(formatAccountZH(ctx)) } else { sb.WriteString(formatAccountEN(ctx)) } - // 4. 历史交易统计 + // 4. Historical trading statistics if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 { if lang == LangChinese { sb.WriteString(formatTradingStatsZH(ctx.TradingStats)) @@ -61,7 +62,7 @@ func formatContextData(ctx *Context, lang Language) string { } } - // 5. 最近交易记录 + // 5. Recent trade records if len(ctx.RecentOrders) > 0 { if lang == LangChinese { sb.WriteString(formatRecentTradesZH(ctx.RecentOrders)) @@ -70,7 +71,7 @@ func formatContextData(ctx *Context, lang Language) string { } } - // 5. 当前持仓 + // 5. Current positions if len(ctx.Positions) > 0 { if lang == LangChinese { sb.WriteString(formatCurrentPositionsZH(ctx)) @@ -79,7 +80,7 @@ func formatContextData(ctx *Context, lang Language) string { } } - // 6. 候选币种(带市场数据) + // 6. Candidate coins (with market data) if len(ctx.CandidateCoins) > 0 { if lang == LangChinese { sb.WriteString(formatCandidateCoinsZH(ctx)) @@ -88,7 +89,7 @@ func formatContextData(ctx *Context, lang Language) string { } } - // 7. OI排名数据(如果有) + // 7. OI ranking data (if available) if ctx.OIRankingData != nil { nofxosLang := nofxos.LangEnglish if lang == LangChinese { @@ -100,15 +101,15 @@ func formatContextData(ctx *Context, lang Language) string { return sb.String() } -// ========== 中文格式化函数 ========== +// ========== Chinese Formatting Functions ========== -// formatHeaderZH 格式化头部信息(中文) +// formatHeaderZH formats header information (Chinese) func formatHeaderZH(ctx *Context) string { return fmt.Sprintf("# 📊 交易决策请求\n\n时间: %s | 周期: #%d | 运行时长: %d 分钟\n\n", ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes) } -// formatAccountZH 格式化账户信息(中文) +// formatAccountZH formats account information (Chinese) func formatAccountZH(ctx *Context) string { acc := ctx.Account var sb strings.Builder @@ -120,7 +121,7 @@ func formatAccountZH(ctx *Context) string { sb.WriteString(fmt.Sprintf("保证金使用率: %.1f%% | ", acc.MarginUsedPct)) sb.WriteString(fmt.Sprintf("持仓数: %d\n\n", acc.PositionCount)) - // 添加风险提示 + // Add risk warnings if acc.MarginUsedPct > 70 { sb.WriteString("⚠️ **风险警告**: 保证金使用率 > 70%,处于高风险状态!\n\n") } else if acc.MarginUsedPct > 50 { @@ -130,25 +131,25 @@ func formatAccountZH(ctx *Context) string { return sb.String() } -// formatTradingStatsZH 格式化历史交易统计(中文) +// formatTradingStatsZH formats historical trading statistics (Chinese) func formatTradingStatsZH(stats *TradingStats) string { var sb strings.Builder sb.WriteString("## 历史交易统计\n\n") - // 盈亏比计算 + // Win/loss ratio calculation var winLossRatio float64 if stats.AvgLoss > 0 { winLossRatio = stats.AvgWin / stats.AvgLoss } - // 指标定义说明(去掉胜率,聚焦核心指标) + // Metric definitions (focusing on core metrics, excluding win rate) sb.WriteString("**指标说明**:\n") sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利,>1.5为良好,>2为优秀)\n") sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好,>2优秀)\n") sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀)\n") sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n") - // 数据值 + // Data values sb.WriteString("**当前数据**:\n") sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades)) sb.WriteString(fmt.Sprintf("- 盈利因子: %.2f\n", stats.ProfitFactor)) @@ -159,10 +160,10 @@ func formatTradingStatsZH(stats *TradingStats) string { sb.WriteString(fmt.Sprintf("- 平均亏损: -%.2f USDT\n", stats.AvgLoss)) sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct)) - // 综合分析和决策建议 + // Comprehensive analysis and decision guidance sb.WriteString("**决策参考**:\n") - // 根据统计数据给出具体建议 + // Provide specific recommendations based on statistics if stats.TotalTrades < 10 { sb.WriteString("- 样本量较小(<10笔),统计结果参考意义有限\n") } @@ -191,13 +192,13 @@ func formatTradingStatsZH(stats *TradingStats) string { return sb.String() } -// formatRecentTradesZH 格式化最近交易(中文) +// formatRecentTradesZH formats recent trades (Chinese) func formatRecentTradesZH(orders []RecentOrder) string { var sb strings.Builder sb.WriteString("## 最近完成的交易\n\n") for i, order := range orders { - // 判断盈亏 + // Determine profit or loss profitOrLoss := "盈利" if order.RealizedPnL < 0 { profitOrLoss = "亏损" @@ -222,13 +223,13 @@ func formatRecentTradesZH(orders []RecentOrder) string { return sb.String() } -// formatCurrentPositionsZH 格式化当前持仓(中文) +// formatCurrentPositionsZH formats current positions (Chinese) func formatCurrentPositionsZH(ctx *Context) string { var sb strings.Builder sb.WriteString("## 当前持仓\n\n") for i, pos := range ctx.Positions { - // 计算回撤 + // Calculate drawdown drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side))) @@ -242,7 +243,7 @@ func formatCurrentPositionsZH(ctx *Context) string { sb.WriteString(fmt.Sprintf("保证金 %.0f USDT | ", pos.MarginUsed)) sb.WriteString(fmt.Sprintf("强平价 %.4f\n", pos.LiquidationPrice)) - // 添加分析提示 + // Add analysis hints if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 { sb.WriteString(fmt.Sprintf(" ⚠️ **止盈提示**: 当前盈亏从峰值 %.2f%% 回撤到 %.2f%%,回撤幅度 %.2f%%,建议考虑止盈\n", pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100)) @@ -252,7 +253,7 @@ func formatCurrentPositionsZH(ctx *Context) string { sb.WriteString(" ⚠️ **止损提示**: 亏损接近-5%止损线,建议考虑止损\n") } - // 显示当前价格(如果有市场数据) + // Show current price (if market data available) if ctx.MarketDataMap != nil { if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok { sb.WriteString(fmt.Sprintf(" 📈 当前价格: %.4f\n", mdata.CurrentPrice)) @@ -265,7 +266,7 @@ func formatCurrentPositionsZH(ctx *Context) string { return sb.String() } -// formatCandidateCoinsZH 格式化候选币种(中文) +// formatCandidateCoinsZH formats candidate coins (Chinese) func formatCandidateCoinsZH(ctx *Context) string { var sb strings.Builder sb.WriteString("## 候选币种\n\n") @@ -273,19 +274,19 @@ func formatCandidateCoinsZH(ctx *Context) string { for i, coin := range ctx.CandidateCoins { sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol)) - // 当前价格 + // Current price if ctx.MarketDataMap != nil { if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok { sb.WriteString(fmt.Sprintf("当前价格: %.4f\n\n", mdata.CurrentPrice)) - // K线数据(多时间框架) + // Kline data (multi-timeframe) if mdata.TimeframeData != nil { sb.WriteString(formatKlineDataZH(coin.Symbol, mdata.TimeframeData, ctx.Timeframes)) } } } - // OI数据(如果有) + // OI data (if available) if ctx.OITopDataMap != nil { if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok { sb.WriteString(fmt.Sprintf("**持仓量变化**: OI排名 #%d | 变化 %+.2f%% (%+.2fM USDT) | 价格变化 %+.2f%%\n\n", @@ -295,7 +296,7 @@ func formatCandidateCoinsZH(ctx *Context) string { oiData.PriceDeltaPercent, )) - // OI解读 + // OI interpretation oiChange := "增加" if oiData.OIDeltaPercent < 0 { oiChange = "减少" @@ -314,7 +315,7 @@ func formatCandidateCoinsZH(ctx *Context) string { return sb.String() } -// formatKlineDataZH 格式化K线数据(中文) +// formatKlineDataZH formats kline data (Chinese) func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string { var sb strings.Builder @@ -324,7 +325,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD sb.WriteString("```\n") sb.WriteString("时间(UTC) 开盘 最高 最低 收盘 成交量\n") - // 只显示最近30根K线 + // Only show the latest 30 klines startIdx := 0 if len(data.Klines) > 30 { startIdx = len(data.Klines) - 30 @@ -343,7 +344,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD )) } - // 标记最后一根K线 + // Mark the last kline if len(data.Klines) > 0 { sb.WriteString(" <- 当前\n") } @@ -356,7 +357,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD } -// getOIInterpretationZH 获取OI变化解读(中文) +// getOIInterpretationZH returns OI change interpretation (Chinese) func getOIInterpretationZH(oiChange, priceChange string) string { if oiChange == "增加" && priceChange == "上涨" { return OIInterpretation.OIUp_PriceUp.ZH @@ -369,15 +370,15 @@ func getOIInterpretationZH(oiChange, priceChange string) string { } } -// ========== 英文格式化函数 ========== +// ========== English Formatting Functions ========== -// formatHeaderEN 格式化头部信息(英文) +// formatHeaderEN formats header information (English) func formatHeaderEN(ctx *Context) string { return fmt.Sprintf("# 📊 Trading Decision Request\n\nTime: %s | Period: #%d | Runtime: %d minutes\n\n", ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes) } -// formatAccountEN 格式化账户信息(英文) +// formatAccountEN formats account information (English) func formatAccountEN(ctx *Context) string { acc := ctx.Account var sb strings.Builder @@ -399,7 +400,7 @@ func formatAccountEN(ctx *Context) string { return sb.String() } -// formatTradingStatsEN 格式化历史交易统计(英文) +// formatTradingStatsEN formats historical trading statistics (English) func formatTradingStatsEN(stats *TradingStats) string { var sb strings.Builder sb.WriteString("## Historical Trading Statistics\n\n") @@ -460,7 +461,7 @@ func formatTradingStatsEN(stats *TradingStats) string { return sb.String() } -// formatRecentTradesEN 格式化最近交易(英文) +// formatRecentTradesEN formats recent trades (English) func formatRecentTradesEN(orders []RecentOrder) string { var sb strings.Builder sb.WriteString("## Recent Completed Trades\n\n") @@ -490,7 +491,7 @@ func formatRecentTradesEN(orders []RecentOrder) string { return sb.String() } -// formatCurrentPositionsEN 格式化当前持仓(英文) +// formatCurrentPositionsEN formats current positions (English) func formatCurrentPositionsEN(ctx *Context) string { var sb strings.Builder sb.WriteString("## Current Positions\n\n") @@ -531,7 +532,7 @@ func formatCurrentPositionsEN(ctx *Context) string { return sb.String() } -// formatCandidateCoinsEN 格式化候选币种(英文) +// formatCandidateCoinsEN formats candidate coins (English) func formatCandidateCoinsEN(ctx *Context) string { var sb strings.Builder sb.WriteString("## Candidate Coins\n\n") @@ -576,7 +577,7 @@ func formatCandidateCoinsEN(ctx *Context) string { return sb.String() } -// formatKlineDataEN 格式化K线数据(英文) +// formatKlineDataEN formats kline data (English) func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string { var sb strings.Builder @@ -621,7 +622,7 @@ func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesD } -// getOIInterpretationEN 获取OI变化解读(英文) +// getOIInterpretationEN returns OI change interpretation (English) func getOIInterpretationEN(oiChange, priceChange string) string { if oiChange == "increase" && priceChange == "up" { return OIInterpretation.OIUp_PriceUp.EN diff --git a/kernel/prompt_builder.go b/kernel/prompt_builder.go index dbeaa257..60554bcc 100644 --- a/kernel/prompt_builder.go +++ b/kernel/prompt_builder.go @@ -6,22 +6,22 @@ import ( ) // ============================================================================ -// AI Prompt Builder - AI提示词构建器 +// AI Prompt Builder // ============================================================================ -// 构建完整的AI提示词,包括系统提示词和用户提示词 +// Builds complete AI prompts including system prompts and user prompts. // ============================================================================ -// PromptBuilder 提示词构建器 +// PromptBuilder builds AI prompts in the configured language type PromptBuilder struct { lang Language } -// NewPromptBuilder 创建提示词构建器 +// NewPromptBuilder creates a new prompt builder for the given language func NewPromptBuilder(lang Language) *PromptBuilder { return &PromptBuilder{lang: lang} } -// BuildSystemPrompt 构建系统提示词 +// BuildSystemPrompt builds the system prompt func (pb *PromptBuilder) BuildSystemPrompt() string { if pb.lang == LangChinese { return pb.buildSystemPromptZH() @@ -29,19 +29,19 @@ func (pb *PromptBuilder) BuildSystemPrompt() string { return pb.buildSystemPromptEN() } -// BuildUserPrompt 构建用户提示词(包含完整的交易上下文) +// BuildUserPrompt builds the user prompt with full trading context func (pb *PromptBuilder) BuildUserPrompt(ctx *Context) string { - // 使用Formatter格式化交易上下文 + // Use Formatter to format the trading context formattedData := FormatContextForAI(ctx, pb.lang) - // 添加决策要求 + // Append decision requirements if pb.lang == LangChinese { return formattedData + pb.getDecisionRequirementsZH() } return formattedData + pb.getDecisionRequirementsEN() } -// ========== 中文提示词 ========== +// ========== Chinese Prompts ========== func (pb *PromptBuilder) buildSystemPromptZH() string { return `你是一个专业的量化交易AI助手,负责分析市场数据并做出交易决策。 @@ -176,7 +176,7 @@ func (pb *PromptBuilder) getDecisionRequirementsZH() string { **请立即输出你的决策(JSON格式)**:` } -// ========== 英文提示词 ========== +// ========== English Prompts ========== func (pb *PromptBuilder) buildSystemPromptEN() string { return `You are a professional quantitative trading AI assistant responsible for analyzing market data and making trading decisions. @@ -311,9 +311,9 @@ func (pb *PromptBuilder) getDecisionRequirementsEN() string { **Please output your decision (JSON format) immediately**:` } -// ========== 辅助函数 ========== +// ========== Helper Functions ========== -// FormatDecisionExample 格式化决策示例(用于文档) +// FormatDecisionExample formats a decision example (for documentation) func FormatDecisionExample(lang Language) string { example := Decision{ Symbol: "BTCUSDT", @@ -323,32 +323,32 @@ func FormatDecisionExample(lang Language) string { StopLoss: 42000, TakeProfit: 48000, Confidence: 85, - Reasoning: "详细的推理过程...", + Reasoning: "Detailed reasoning process...", } data, _ := json.MarshalIndent([]Decision{example}, "", " ") return string(data) } -// ValidateDecisionFormat 验证决策格式是否正确 +// ValidateDecisionFormat validates that the decision format is correct func ValidateDecisionFormat(decisions []Decision) error { if len(decisions) == 0 { - return fmt.Errorf("决策列表不能为空") + return fmt.Errorf("decision list cannot be empty") } for i, d := range decisions { - // 必需字段检查 + // Required field checks if d.Symbol == "" { - return fmt.Errorf("决策#%d: symbol不能为空", i+1) + return fmt.Errorf("decision #%d: symbol cannot be empty", i+1) } if d.Action == "" { - return fmt.Errorf("决策#%d: action不能为空", i+1) + return fmt.Errorf("decision #%d: action cannot be empty", i+1) } if d.Reasoning == "" { - return fmt.Errorf("决策#%d: reasoning不能为空", i+1) + return fmt.Errorf("decision #%d: reasoning cannot be empty", i+1) } - // 动作类型检查 + // Action type validation validActions := map[string]bool{ "HOLD": true, "PARTIAL_CLOSE": true, @@ -358,16 +358,16 @@ func ValidateDecisionFormat(decisions []Decision) error { "WAIT": true, } if !validActions[d.Action] { - return fmt.Errorf("决策#%d: 无效的action类型: %s", i+1, d.Action) + return fmt.Errorf("decision #%d: invalid action type: %s", i+1, d.Action) } - // 开新仓位的必需参数检查 + // Required parameters for opening new positions if d.Action == "OPEN_NEW" { if d.Leverage == 0 { - return fmt.Errorf("决策#%d: OPEN_NEW动作需要提供leverage", i+1) + return fmt.Errorf("decision #%d: OPEN_NEW action requires leverage", i+1) } if d.PositionSizeUSD == 0 { - return fmt.Errorf("决策#%d: OPEN_NEW动作需要提供position_size_usd", i+1) + return fmt.Errorf("decision #%d: OPEN_NEW action requires position_size_usd", i+1) } } } diff --git a/kernel/prompt_builder_test.go b/kernel/prompt_builder_test.go index e36a2a73..ad369548 100644 --- a/kernel/prompt_builder_test.go +++ b/kernel/prompt_builder_test.go @@ -170,8 +170,8 @@ func TestValidateDecisionFormat(t *testing.T) { t.Error("Empty decisions should return error") } - if !strings.Contains(err.Error(), "不能为空") { - t.Errorf("Error message should mention '不能为空', got: %v", err) + if !strings.Contains(err.Error(), "cannot be empty") { + t.Errorf("Error message should mention 'cannot be empty', got: %v", err) } }) @@ -238,8 +238,8 @@ func TestValidateDecisionFormat(t *testing.T) { t.Error("Invalid action should return error") } - if !strings.Contains(err.Error(), "无效的action") { - t.Errorf("Error should mention '无效的action', got: %v", err) + if !strings.Contains(err.Error(), "invalid action") { + t.Errorf("Error should mention 'invalid action', got: %v", err) } }) diff --git a/kernel/schema.go b/kernel/schema.go index 9eaf899c..91119da1 100644 --- a/kernel/schema.go +++ b/kernel/schema.go @@ -1,17 +1,17 @@ package kernel // ============================================================================ -// Trading Data Schema - 交易数据字典 +// Trading Data Schema // ============================================================================ -// 双语数据字典,支持中文和英文 -// 确保AI能够100%理解数据格式,无论使用哪种语言 +// Bilingual data dictionary supporting Chinese and English. +// Ensures AI can fully understand data formats regardless of language. // ============================================================================ const ( SchemaVersion = "1.0.0" ) -// Language 语言类型 +// Language represents the language type type Language string const ( @@ -19,20 +19,20 @@ const ( LangEnglish Language = "en-US" ) -// ========== 双语字段定义 ========== +// ========== Bilingual Field Definitions ========== -// BilingualFieldDef 双语字段定义 +// BilingualFieldDef defines a field with bilingual name, formula, and description type BilingualFieldDef struct { - NameZH string // 中文名称 + NameZH string // Chinese name NameEN string // English name - Unit string // 单位 - FormulaZH string // 中文公式 + Unit string // unit of measurement + FormulaZH string // Chinese formula FormulaEN string // English formula - DescZH string // 中文描述 + DescZH string // Chinese description DescEN string // English description } -// GetName 获取字段名称(根据语言) +// GetName returns the field name based on language func (d BilingualFieldDef) GetName(lang Language) string { if lang == LangChinese { return d.NameZH @@ -40,7 +40,7 @@ func (d BilingualFieldDef) GetName(lang Language) string { return d.NameEN } -// GetFormula 获取公式(根据语言) +// GetFormula returns the formula based on language func (d BilingualFieldDef) GetFormula(lang Language) string { if lang == LangChinese { return d.FormulaZH @@ -48,7 +48,7 @@ func (d BilingualFieldDef) GetFormula(lang Language) string { return d.FormulaEN } -// GetDesc 获取描述(根据语言) +// GetDesc returns the description based on language func (d BilingualFieldDef) GetDesc(lang Language) string { if lang == LangChinese { return d.DescZH @@ -56,9 +56,9 @@ func (d BilingualFieldDef) GetDesc(lang Language) string { return d.DescEN } -// ========== 数据字典 ========== +// ========== Data Dictionary ========== -// DataDictionary 数据字典:定义所有字段的含义 +// DataDictionary defines the meaning of all fields var DataDictionary = map[string]map[string]BilingualFieldDef{ "AccountMetrics": { "Equity": { @@ -217,18 +217,18 @@ var DataDictionary = map[string]map[string]BilingualFieldDef{ }, } -// ========== 双语规则定义 ========== +// ========== Bilingual Rule Definitions ========== -// BilingualRuleDef 双语规则定义 +// BilingualRuleDef defines a trading rule with bilingual description and reason type BilingualRuleDef struct { - Value interface{} // 规则值 - DescZH string // 中文描述 + Value interface{} // rule value + DescZH string // Chinese description DescEN string // English description - ReasonZH string // 中文原因 + ReasonZH string // Chinese reason ReasonEN string // English reason } -// GetDesc 获取描述(根据语言) +// GetDesc returns the description based on language func (d BilingualRuleDef) GetDesc(lang Language) string { if lang == LangChinese { return d.DescZH @@ -236,7 +236,7 @@ func (d BilingualRuleDef) GetDesc(lang Language) string { return d.DescEN } -// GetReason 获取原因(根据语言) +// GetReason returns the reason based on language func (d BilingualRuleDef) GetReason(lang Language) string { if lang == LangChinese { return d.ReasonZH @@ -244,9 +244,9 @@ func (d BilingualRuleDef) GetReason(lang Language) string { return d.ReasonEN } -// ========== 交易规则 ========== +// ========== Trading Rules ========== -// TradingRules 交易规则定义 +// TradingRules defines the trading rules var TradingRules = struct { RiskManagement map[string]BilingualRuleDef EntrySignals map[string]BilingualRuleDef @@ -340,9 +340,9 @@ var TradingRules = struct { }, } -// ========== OI解读 ========== +// ========== OI Interpretation ========== -// OIInterpretation OI变化的市场解读(双语) +// OIInterpretation defines bilingual market interpretations for OI changes type OIInterpretationType struct { OIUp_PriceUp struct { ZH string @@ -393,9 +393,9 @@ var OIInterpretation = OIInterpretationType{ }, } -// ========== 常见错误 ========== +// ========== Common Mistakes ========== -// CommonMistake 常见错误定义 +// CommonMistake defines a common mistake with bilingual fields type CommonMistake struct { ErrorZH string ErrorEN string @@ -440,9 +440,9 @@ var CommonMistakes = []CommonMistake{ }, } -// ========== Prompt生成函数 ========== +// ========== Prompt Generation Functions ========== -// GetSchemaPrompt 生成Schema说明文本,用于AI Prompt +// GetSchemaPrompt generates schema description text for AI prompts func GetSchemaPrompt(lang Language) string { if lang == LangChinese { return getSchemaPromptZH() @@ -450,36 +450,36 @@ func GetSchemaPrompt(lang Language) string { return getSchemaPromptEN() } -// getSchemaPromptZH 生成中文Prompt +// getSchemaPromptZH generates the Chinese prompt func getSchemaPromptZH() string { prompt := "# 📖 数据字典与交易规则\n\n" prompt += "## 📊 字段含义说明\n\n" - // 账户指标 + // Account metrics prompt += "### 账户指标\n" for key, field := range DataDictionary["AccountMetrics"] { prompt += formatFieldDefZH(key, field) } - // 交易指标 + // Trade metrics prompt += "\n### 交易指标\n" for key, field := range DataDictionary["TradeMetrics"] { prompt += formatFieldDefZH(key, field) } - // 持仓指标 + // Position metrics prompt += "\n### 持仓指标\n" for key, field := range DataDictionary["PositionMetrics"] { prompt += formatFieldDefZH(key, field) } - // 市场数据 + // Market data prompt += "\n### 市场数据\n" for key, field := range DataDictionary["MarketData"] { prompt += formatFieldDefZH(key, field) } - // OI解读 + // OI interpretation prompt += "\n## 💹 持仓量(OI)变化解读\n\n" prompt += "- **OI增加 + 价格上涨**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n" prompt += "- **OI增加 + 价格下跌**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n" @@ -489,7 +489,7 @@ func getSchemaPromptZH() string { return prompt } -// getSchemaPromptEN 生成英文Prompt +// getSchemaPromptEN generates the English prompt func getSchemaPromptEN() string { prompt := "# 📖 Data Dictionary & Trading Rules\n\n" prompt += "## 📊 Field Definitions\n\n" @@ -528,7 +528,7 @@ func getSchemaPromptEN() string { return prompt } -// formatFieldDefZH 格式化中文字段定义 +// formatFieldDefZH formats a field definition in Chinese func formatFieldDefZH(key string, field BilingualFieldDef) string { result := "- **" + key + "**(" + field.NameZH + "): " + field.DescZH if field.FormulaZH != "" { @@ -541,7 +541,7 @@ func formatFieldDefZH(key string, field BilingualFieldDef) string { return result } -// formatFieldDefEN 格式化英文字段定义 +// formatFieldDefEN formats a field definition in English func formatFieldDefEN(key string, field BilingualFieldDef) string { result := "- **" + key + "** (" + field.NameEN + "): " + field.DescEN if field.FormulaEN != "" { diff --git a/market/types.go b/market/types.go index 95c335da..eaa0cbfe 100644 --- a/market/types.go +++ b/market/types.go @@ -210,11 +210,11 @@ type BoxData struct { type RegimeLevel string const ( - RegimeLevelNarrow RegimeLevel = "narrow" // 窄幅震荡 - RegimeLevelStandard RegimeLevel = "standard" // 标准震荡 - RegimeLevelWide RegimeLevel = "wide" // 宽幅震荡 - RegimeLevelVolatile RegimeLevel = "volatile" // 剧烈震荡 - RegimeLevelTrending RegimeLevel = "trending" // 趋势 + RegimeLevelNarrow RegimeLevel = "narrow" // narrow range oscillation + RegimeLevelStandard RegimeLevel = "standard" // standard oscillation + RegimeLevelWide RegimeLevel = "wide" // wide range oscillation + RegimeLevelVolatile RegimeLevel = "volatile" // extreme volatility + RegimeLevelTrending RegimeLevel = "trending" // trending ) // BreakoutLevel represents which box level has been broken diff --git a/provider/nofxos/ai500.go b/provider/nofxos/ai500.go index 5e1c6450..40c475db 100644 --- a/provider/nofxos/ai500.go +++ b/provider/nofxos/ai500.go @@ -73,7 +73,7 @@ func (c *Client) fetchAI500() ([]CoinData, error) { return nil, fmt.Errorf("API returned failure status") } - // 空列表是正常情况,不是错误 + // Empty list is a normal condition, not an error if len(response.Data.Coins) == 0 { log.Printf("ℹ️ AI500 returned empty coin list (no coins meet criteria currently)") return []CoinData{}, nil diff --git a/provider/nofxos/netflow.go b/provider/nofxos/netflow.go index 1731421d..1d9958e6 100644 --- a/provider/nofxos/netflow.go +++ b/provider/nofxos/netflow.go @@ -23,7 +23,7 @@ type NetFlowResponse struct { Netflows []NetFlowPosition `json:"netflows"` Count int `json:"count"` Type string `json:"type"` // institution or personal - Trade string `json:"trade"` // 合约 or 现货 + Trade string `json:"trade"` // futures or spot TimeRange string `json:"time_range"` RankType string `json:"rank_type"` // top or low Limit int `json:"limit"` diff --git a/railway/start.sh b/railway/start.sh index 89f5ee56..931670fa 100644 --- a/railway/start.sh +++ b/railway/start.sh @@ -1,11 +1,11 @@ #!/bin/sh set -e -# Railway 会设置 PORT 环境变量 +# Railway sets the PORT environment variable export PORT=${PORT:-8080} echo "🚀 Starting NOFX on port $PORT..." -# 生成加密密钥(如果没有设置) +# Generate encryption keys (if not already set) if [ -z "$RSA_PRIVATE_KEY" ]; then export RSA_PRIVATE_KEY=$(openssl genrsa 2048 2>/dev/null) fi @@ -13,7 +13,7 @@ if [ -z "$DATA_ENCRYPTION_KEY" ]; then export DATA_ENCRYPTION_KEY=$(openssl rand -base64 32) fi -# 生成 nginx 配置 +# Generate nginx config cat > /etc/nginx/http.d/default.conf << NGINX_EOF server { listen $PORT; @@ -44,14 +44,14 @@ server { } NGINX_EOF -# 启动后端(端口 8081) +# Start backend (port 8081) API_SERVER_PORT=8081 /app/nofx & sleep 2 -# 启动 nginx(后台) +# Start nginx (background) nginx echo "✅ NOFX started successfully" -# 保持容器运行 +# Keep the container running tail -f /dev/null diff --git a/store/strategy.go b/store/strategy.go index a406399e..80eaa171 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -111,11 +111,11 @@ type CoinSourceConfig struct { UseAI500 bool `json:"use_ai500"` // AI500 coin pool maximum count AI500Limit int `json:"ai500_limit,omitempty"` - // whether to use OI Top (持仓增加榜,适合做多) + // whether to use OI Top (OI increase ranking, suitable for long positions) UseOITop bool `json:"use_oi_top"` // OI Top maximum count OITopLimit int `json:"oi_top_limit,omitempty"` - // whether to use OI Low (持仓减少榜,适合做空) + // whether to use OI Low (OI decrease ranking, suitable for short positions) UseOILow bool `json:"use_oi_low"` // OI Low maximum count OILowLimit int `json:"oi_low_limit,omitempty"` diff --git a/telegram/agent/agent.go b/telegram/agent/agent.go index 8c4455d2..2f54ddff 100644 --- a/telegram/agent/agent.go +++ b/telegram/agent/agent.go @@ -273,7 +273,7 @@ func (a *Agent) Run(userMessage string, onChunk func(string)) string { // Safety: max iterations reached. logger.Warnf("Agent: max iterations (%d) reached for message: %q", maxIterations, userMessage) - reply := "操作已完成,请检查您的账户查看最新状态。" + reply := "Operation completed. Please check your account for the latest status. / 操作已完成,请检查您的账户查看最新状态。" a.memory.Add("user", userMessage) a.memory.Add("assistant", reply) return reply diff --git a/telegram/agent/manager.go b/telegram/agent/manager.go index d461c28b..90ee9322 100644 --- a/telegram/agent/manager.go +++ b/telegram/agent/manager.go @@ -45,7 +45,7 @@ func (m *Manager) Run(chatID int64, userMessage string, onChunk func(string)) st case lane <- struct{}{}: case <-time.After(60 * time.Second): logger.Warnf("Agent: lane wait timeout for chat %d — previous message still processing", chatID) - return "上一条消息仍在处理中,请稍等片刻后再试。" + return "Previous message is still being processed. Please wait a moment and try again. / 上一条消息仍在处理中,请稍等片刻后再试。" } defer func() { <-lane }() return a.Run(userMessage, onChunk) diff --git a/trader/auto_trader_loop.go b/trader/auto_trader_loop.go index e274c0da..fe1cedcd 100644 --- a/trader/auto_trader_loop.go +++ b/trader/auto_trader_loop.go @@ -63,10 +63,10 @@ func (at *AutoTrader) runCycle() error { // NOTE: Must be called BEFORE candidate coins check to ensure equity is always recorded at.saveEquitySnapshot(ctx) - // 如果没有候选币种,记录但不报错 + // If no candidate coins available, log but do not error if len(ctx.CandidateCoins) == 0 { logger.Infof("ℹ️ No candidate coins available, skipping this cycle") - record.Success = true // 不是错误,只是没有候选币 + record.Success = true // Not an error, just no candidate coins record.ExecutionLog = append(record.ExecutionLog, "No candidate coins available, cycle skipped") record.AccountState = store.AccountSnapshot{ TotalBalance: ctx.Account.TotalEquity, diff --git a/trader/lighter/orders.go b/trader/lighter/orders.go index ff2375ca..e6678f1d 100644 --- a/trader/lighter/orders.go +++ b/trader/lighter/orders.go @@ -115,7 +115,7 @@ func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[str resp, err := t.client.Do(req) if err != nil { - // ✅ 正确做法:查询失败返回错误,而不是假设成交 + // Correct approach: return error on query failure, do not assume order is filled return nil, fmt.Errorf("failed to query order status: %w", err) } defer resp.Body.Close() diff --git a/web/src/components/auth/LoginRequiredOverlay.tsx b/web/src/components/auth/LoginRequiredOverlay.tsx index b362cbf9..2bfba9ed 100644 --- a/web/src/components/auth/LoginRequiredOverlay.tsx +++ b/web/src/components/auth/LoginRequiredOverlay.tsx @@ -2,6 +2,7 @@ import { motion, AnimatePresence } from 'framer-motion' import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react' import { DeepVoidBackground } from '../common/DeepVoidBackground' import { useLanguage } from '../../contexts/LanguageContext' +import { t } from '../../i18n/translations' interface LoginRequiredOverlayProps { isOpen: boolean @@ -12,52 +13,19 @@ interface LoginRequiredOverlayProps { export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequiredOverlayProps) { const { language } = useLanguage() - const texts = { - zh: { - title: '系统访问受限', - subtitle: featureName ? `访问「${featureName}」需要更高权限` : '此模块需要授权访问', - description: '初始化身份验证协议以解锁完整系统功能:AI 交易员配置、策略市场数据流、回测模拟核心。', - benefits: [ - 'AI 交易员控制权', - '高频策略核心市场', - '历史数据回测引擎', - '全系统数据可视化' - ], - login: '执行登录指令', - register: '注册新用户 ID', - later: '中止操作' - }, - en: { - title: 'SYSTEM ACCESS DENIED', - subtitle: featureName ? `Module "${featureName}" requires elevated privileges` : 'Authorization required for this module', - description: 'Initialize authentication protocol to unlock full system capabilities: AI Trader configuration, Strategy Market data streams, and Backtest Simulation core.', - benefits: [ - 'AI Trader Control', - 'HFT Strategy Market', - 'Historical Backtest Engine', - 'Full System Visualization' - ], - login: 'EXECUTE LOGIN', - register: 'REGISTER NEW ID', - later: 'ABORT' - }, - id: { - title: 'AKSES SISTEM DITOLAK', - subtitle: featureName ? `Modul "${featureName}" memerlukan hak akses lebih tinggi` : 'Otorisasi diperlukan untuk modul ini', - description: 'Inisialisasi protokol autentikasi untuk membuka kemampuan sistem penuh: konfigurasi Trader AI, aliran data Pasar Strategi, dan inti Simulasi Backtest.', - benefits: [ - 'Kontrol Trader AI', - 'Pasar Strategi HFT', - 'Mesin Backtest Historis', - 'Visualisasi Sistem Penuh' - ], - login: 'JALANKAN LOGIN', - register: 'DAFTAR ID BARU', - later: 'BATALKAN' - } - } + const tr = (key: string, params?: Record) => + t(`loginRequired.${key}`, language, params) - const t = texts[language] + const subtitle = featureName + ? tr('subtitleWithFeature', { featureName }) + : tr('subtitleDefault') + + const benefits = [ + tr('benefit1'), + tr('benefit2'), + tr('benefit3'), + tr('benefit4'), + ] return ( @@ -108,7 +76,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
- {language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'} + {tr('accessDenied')}
@@ -116,19 +84,19 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ {/* Terminal Text */}
-

{t.title}

-

{t.subtitle}

+

{tr('title')}

+

{subtitle}

$ - {t.description} + {tr('description')}

- {t.benefits.map((benefit, i) => ( + {benefits.map((benefit, i) => (
{benefit}
@@ -143,7 +111,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-black font-bold text-xs uppercase tracking-widest hover:bg-yellow-400 transition-all shadow-neon hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group" > - {t.login} + {tr('loginButton')} -> @@ -152,7 +120,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-nofx-gold/20 text-nofx-text-muted hover:text-white hover:border-nofx-gold font-bold text-xs uppercase tracking-widest transition-all hover:bg-nofx-gold/10" > - {t.register} + {tr('registerButton')}
@@ -161,7 +129,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ onClick={onClose} className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30" > - [ {t.later} ] + [ {tr('abort')} ]
diff --git a/web/src/components/backtest/BacktestChartTab.tsx b/web/src/components/backtest/BacktestChartTab.tsx index 0a187e8c..80b0408a 100644 --- a/web/src/components/backtest/BacktestChartTab.tsx +++ b/web/src/components/backtest/BacktestChartTab.tsx @@ -29,6 +29,7 @@ import { CandlestickChart as CandlestickIcon, } from 'lucide-react' import { api } from '../../lib/api' +import { t, type Language } from '../../i18n/translations' import type { BacktestEquityPoint, BacktestTradeEvent, @@ -136,7 +137,7 @@ export function EquityChart({ equity, trades }: EquityChartProps) { interface CandlestickChartProps { runId: string trades: BacktestTradeEvent[] - language: string + language: Language } export function CandlestickChartComponent({ runId, trades, language }: CandlestickChartProps) { @@ -289,7 +290,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti if (symbols.length === 0) { return (
- {language === 'zh' ? '没有交易记录' : 'No trades to display'} + {t('backtestChart.noTrades', language)}
) } @@ -300,7 +301,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
- {language === 'zh' ? '币种' : 'Symbol'} + {t('backtestChart.symbol', language)} onFormChange('strategyId', e.target.value)} > - + {strategies?.map((s) => (
@@ -307,7 +307,7 @@ export function BacktestConfigForm({ className="w-full py-2.5 rounded-lg font-medium flex items-center justify-center gap-2 transition-all disabled:opacity-50" style={{ background: '#F0B90B', color: '#0B0E11' }} > - {zh ? '下一步' : 'Next'} + {globalT('backtestConfigForm.next', lang)} @@ -359,7 +359,7 @@ export function BacktestConfigForm({
{TIMEFRAME_OPTIONS.map((tf) => { @@ -428,7 +428,7 @@ export function BacktestConfigForm({ style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} > - {zh ? '上一步' : 'Back'} + {globalT('backtestConfigForm.back', lang)}
@@ -520,7 +520,7 @@ export function BacktestConfigForm({
{['baseline', 'aggressive', 'conservative', 'scalping'].map((p) => ( @@ -570,7 +570,7 @@ export function BacktestConfigForm({ style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} > - {zh ? '上一步' : 'Back'} + {globalT('backtestConfigForm.back', lang)}
- {language === 'zh' ? '数量' : 'Qty'}: {pos.quantity.toFixed(4)} ·{' '} - {language === 'zh' ? '保证金' : 'Margin'}: ${pos.margin_used.toFixed(2)} + {t('backtestOverview.qty', language)}: {pos.quantity.toFixed(4)} ·{' '} + {t('backtestOverview.margin', language)}: ${pos.margin_used.toFixed(2)}
@@ -223,10 +224,10 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
- {language === 'zh' ? '开仓' : 'Entry'}: ${pos.entry_price.toFixed(2)} + {t('backtestOverview.entry', language)}: ${pos.entry_price.toFixed(2)} - {language === 'zh' ? '现价' : 'Mark'}: ${pos.mark_price.toFixed(2)} + {t('backtestOverview.mark', language)}: ${pos.mark_price.toFixed(2)}
@@ -255,7 +256,7 @@ interface BacktestOverviewTabProps { equity: BacktestEquityPoint[] | undefined trades: BacktestTradeEvent[] | undefined metrics: BacktestMetrics | undefined - language: string + language: Language tr: (key: string) => string } @@ -285,7 +286,7 @@ export function BacktestOverviewTab({
- {language === 'zh' ? '胜率' : 'Win Rate'} + {t('backtestOverview.winRate', language)}
@@ -294,7 +295,7 @@ export function BacktestOverviewTab({
- {language === 'zh' ? '盈亏因子' : 'Profit Factor'} + {t('backtestOverview.profitFactor', language)}
@@ -303,7 +304,7 @@ export function BacktestOverviewTab({
- {language === 'zh' ? '总交易数' : 'Total Trades'} + {t('backtestOverview.totalTrades', language)}
{metrics.trades ?? 0} @@ -311,7 +312,7 @@ export function BacktestOverviewTab({
- {language === 'zh' ? '最佳币种' : 'Best Symbol'} + {t('backtestOverview.bestSymbol', language)}
{metrics.best_symbol?.replace('USDT', '') || '-'} diff --git a/web/src/components/backtest/BacktestPage.tsx b/web/src/components/backtest/BacktestPage.tsx index e46253b5..c1a031cd 100644 --- a/web/src/components/backtest/BacktestPage.tsx +++ b/web/src/components/backtest/BacktestPage.tsx @@ -244,9 +244,9 @@ export function BacktestPage() { const handleDelete = async () => { if (!selectedRunId) return const confirmed = await confirmToast(tr('toasts.confirmDelete', { id: selectedRunId }), { - title: language === 'zh' ? '确认删除' : 'Confirm Delete', - okText: language === 'zh' ? '删除' : 'Delete', - cancelText: language === 'zh' ? '取消' : 'Cancel', + title: t('backtestPageExtra.confirmDelete', language), + okText: t('backtestPageExtra.delete', language), + cancelText: t('backtestPageExtra.cancel', language), }) if (!confirmed) return try { @@ -328,7 +328,7 @@ export function BacktestPage() { style={{ background: '#F0B90B', color: '#0B0E11' }} > - {language === 'zh' ? '新建回测' : 'New Backtest'} + {t('backtestPageExtra.newBacktest', language)}
@@ -474,14 +474,14 @@ export function BacktestPage() {
= 0 ? 'up' : 'down'} color={(metrics?.total_return_pct ?? 0) >= 0 ? '#0ECB81' : '#F6465D'} @@ -490,7 +490,7 @@ export function BacktestPage() { /> {tab === 'overview' - ? language === 'zh' - ? '概览' - : 'Overview' + ? t('backtestPageExtra.tabOverview', language) : tab === 'chart' - ? language === 'zh' - ? '图表' - : 'Chart' + ? t('backtestPageExtra.tabChart', language) : tab === 'trades' - ? language === 'zh' - ? '交易' - : 'Trades' - : language === 'zh' - ? 'AI决策' - : 'Decisions'} + ? t('backtestPageExtra.tabTrades', language) + : t('backtestPageExtra.tabDecisions', language)} {viewTab === tab && ( ) => string onSelectRun: (runId: string) => void onToggleCompare: (runId: string) => void @@ -84,7 +85,7 @@ export function BacktestRunList({ {tr('runList.title')} - {runs.length} {language === 'zh' ? '条' : 'runs'} + {runs.length} {t('backtestPageExtra.runs', language)}
@@ -131,7 +132,7 @@ export function BacktestRunList({ ? 'rgba(240,185,11,0.2)' : 'transparent', }} - title={language === 'zh' ? '添加到对比' : 'Add to compare'} + title={t('backtestPageExtra.addToCompare', language)} > void // 币种切换回调 + exchange?: string // Exchange type: binance, bybit, okx, bitget, hyperliquid, aster, lighter + onSymbolChange?: (symbol: string) => void // Symbol change callback } -// 指标配置 +// Indicator configuration interface IndicatorConfig { id: string name: string @@ -62,31 +63,31 @@ interface IndicatorConfig { params?: any } -// 获取成交额货币单位 +// Get quote currency unit const getQuoteUnit = (exchange: string): string => { if (['alpaca'].includes(exchange)) { return 'USD' } if (['forex', 'metals'].includes(exchange)) { - return '' // 外汇/贵金属没有真实成交量 + return '' // Forex/metals have no real volume } - return 'USDT' // 加密货币默认 USDT + return 'USDT' // Crypto defaults to USDT } -// 获取成交量数量单位 -const getBaseUnit = (exchange: string, symbol: string): string => { +// Get base volume unit +const getBaseUnit = (exchange: string, symbol: string, language: string): string => { if (['alpaca'].includes(exchange)) { - return '股' + return t('advancedChart.shares', language as 'en' | 'zh' | 'id') } if (['forex', 'metals'].includes(exchange)) { return '' } - // 加密货币:从 symbol 提取基础资产 + // Crypto: extract base asset from symbol const base = symbol.replace(/USDT$|USD$|BUSD$/, '') - return base || '个' + return base || t('advancedChart.units', language as 'en' | 'zh' | 'id') } -// 格式化大数字 +// Format large numbers const formatVolume = (value: number): string => { if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B' if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M' @@ -99,43 +100,43 @@ export function AdvancedChart({ interval = '5m', traderID, height = 550, - exchange = 'binance', // 默认使用 binance + exchange = 'binance', // Default to binance onSymbolChange: _onSymbolChange, // Available for future use }: AdvancedChartProps) { void _onSymbolChange // Prevent unused warning const { language } = useLanguage() const quoteUnit = getQuoteUnit(exchange) - const baseUnit = getBaseUnit(exchange, symbol) + const baseUnit = getBaseUnit(exchange, symbol, language) const chartContainerRef = useRef(null) const chartRef = useRef(null) const candlestickSeriesRef = useRef | null>(null) const volumeSeriesRef = useRef | null>(null) const indicatorSeriesRef = useRef>>(new Map()) const seriesMarkersRef = useRef(null) // Markers primitive for v5 - const currentMarkersDataRef = useRef([]) // 存储当前的标记数据 - const klineDataRef = useRef>(new Map()) // 存储 kline 额外数据 - const priceLinesRef = useRef([]) // 存储挂单价格线 + const currentMarkersDataRef = useRef([]) // Store current marker data + const klineDataRef = useRef>(new Map()) // Store kline extra data + const priceLinesRef = useRef([]) // Store open order price lines const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [showIndicatorPanel, setShowIndicatorPanel] = useState(false) - const [showOrderMarkers, setShowOrderMarkers] = useState(true) // 订单标记显示开关,默认显示 - const isInitialLoadRef = useRef(true) // 跟踪是否为初始加载 + const [showOrderMarkers, setShowOrderMarkers] = useState(true) // Order marker toggle, default on + const isInitialLoadRef = useRef(true) // Track if this is initial load const [tooltipData, setTooltipData] = useState(null) const tooltipRef = useRef(null) - // 行情统计数据(当前K线) + // Market stats (current candle) const [marketStats, setMarketStats] = useState<{ price: number priceChange: number priceChangePercent: number high: number low: number - volume: number // 数量(BTC/股数) - quoteVolume: number // 成交额(USDT/USD) + volume: number // Quantity (BTC/shares) + quoteVolume: number // Turnover (USDT/USD) } | null>(null) - // 指标配置 + // Indicator configuration const [indicators, setIndicators] = useState([ { id: 'volume', name: 'Volume', enabled: true, color: '#3B82F6' }, { id: 'ma5', name: 'MA5', enabled: false, color: '#FF6B6B', params: { period: 5 } }, @@ -147,7 +148,7 @@ export function AdvancedChart({ { id: 'bb', name: 'Bollinger Bands', enabled: false, color: '#9B59B6' }, ]) - // 从服务获取K线数据 + // Fetch kline data from service const fetchKlineData = async (symbol: string, interval: string) => { try { const limit = 1500 @@ -158,18 +159,18 @@ export function AdvancedChart({ throw new Error('Failed to fetch kline data') } - // 转换数据格式 + // Convert data format const rawData = result.data.map((candle: any) => ({ time: Math.floor(candle.openTime / 1000) as UTCTimestamp, open: candle.open, high: candle.high, low: candle.low, close: candle.close, - volume: candle.volume, // 数量(BTC/股数) - quoteVolume: candle.quoteVolume, // 成交额(USDT/USD) + volume: candle.volume, // Quantity (BTC/shares) + quoteVolume: candle.quoteVolume, // Turnover (USDT/USD) })) - // 按时间排序并去重(lightweight-charts 要求数据按时间升序且无重复) + // Sort by time and deduplicate (lightweight-charts requires ascending, unique times) const sortedData = rawData.sort((a: any, b: any) => a.time - b.time) const dedupedData = sortedData.filter((item: any, index: number, arr: any[]) => index === 0 || item.time !== arr[index - 1].time @@ -186,16 +187,16 @@ export function AdvancedChart({ } } - // 解析时间:支持 Unix 时间戳(数字)或字符串格式 + // Parse time: supports Unix timestamp (number) or string format const parseCustomTime = (time: any): number => { if (!time) { console.warn('[AdvancedChart] Empty time value') return 0 } - // 如果已经是数字(Unix 时间戳) + // If already a number (Unix timestamp) if (typeof time === 'number') { - // 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳) + // Determine ms vs seconds: if > 10^12, treat as milliseconds if (time > 1000000000000) { const seconds = Math.floor(time / 1000) console.log('[AdvancedChart] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')') @@ -208,7 +209,7 @@ export function AdvancedChart({ const timeStr = String(time) console.log('[AdvancedChart] Parsing time string:', timeStr) - // 尝试标准ISO格式 + // Try standard ISO format const isoTime = new Date(timeStr).getTime() if (!isNaN(isoTime) && isoTime > 0) { const timestamp = Math.floor(isoTime / 1000) @@ -216,7 +217,7 @@ export function AdvancedChart({ return timestamp } - // 解析自定义格式 "MM-DD HH:mm UTC" (兼容旧数据) + // Parse custom format "MM-DD HH:mm UTC" (for legacy data) const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/) if (match) { const currentYear = new Date().getFullYear() @@ -237,11 +238,11 @@ export function AdvancedChart({ return 0 } - // 获取订单数据 + // Fetch order data const fetchOrders = async (traderID: string, symbol: string): Promise => { try { console.log('[AdvancedChart] Fetching orders for trader:', traderID, 'symbol:', symbol) - // 获取已成交的订单,增加到200条以显示更多历史订单 + // Fetch filled orders, up to 200 for more history const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=200`) console.log('[AdvancedChart] Orders API response:', result) @@ -258,14 +259,14 @@ export function AdvancedChart({ orders.forEach((order: any) => { console.log('[AdvancedChart] Processing order:', order) - // 处理字段名:支持PascalCase和snake_case + // Handle field names: support PascalCase and snake_case const filledAt = order.filled_at || order.FilledAt || order.created_at || order.CreatedAt const avgPrice = order.avg_fill_price || order.AvgFillPrice || order.price || order.Price const orderAction = order.order_action || order.OrderAction const side = (order.side || order.Side)?.toLowerCase() // BUY/SELL const symbol = order.symbol || order.Symbol - // 跳过没有成交时间或价格的订单 + // Skip orders without fill time or price if (!filledAt || !avgPrice || avgPrice === 0) { console.warn('[AdvancedChart] Skipping order - missing data:', { filledAt, avgPrice }) return @@ -277,7 +278,7 @@ export function AdvancedChart({ return } - // 根据 order_action 判断是开仓还是平仓 + // Determine open/close from order_action let action: 'open' | 'close' = 'open' let positionSide: 'long' | 'short' = 'long' @@ -290,7 +291,7 @@ export function AdvancedChart({ positionSide = orderAction.includes('LONG') ? 'long' : 'short' } } else { - // 如果没有 order_action,根据 side 判断 + // If no order_action, infer from side positionSide = side === 'buy' ? 'long' : 'short' } @@ -307,7 +308,7 @@ export function AdvancedChart({ time: timeSeconds, price: avgPrice, side: positionSide, - rawSide: side, // 原始 side 字段 (buy/sell) + rawSide: side, // Original side field (buy/sell) action: action, symbol, }) @@ -321,7 +322,7 @@ export function AdvancedChart({ } } - // 获取交易所挂单 (止盈止损订单) + // Fetch exchange open orders (TP/SL) const fetchOpenOrders = async (traderID: string, symbol: string): Promise => { try { console.log('[AdvancedChart] Fetching open orders for trader:', traderID, 'symbol:', symbol) @@ -341,7 +342,7 @@ export function AdvancedChart({ } } - // 初始化图表 + // Initialize chart useEffect(() => { if (!chartContainerRef.current) return @@ -424,7 +425,7 @@ export function AdvancedChart({ chartRef.current = chart - // 创建K线系列 + // Create candlestick series const candlestickSeries = chart.addSeries(CandlestickSeries, { upColor: '#0ECB81', downColor: '#F6465D', @@ -435,7 +436,7 @@ export function AdvancedChart({ }) candlestickSeriesRef.current = candlestickSeries as any - // 创建成交量系列 + // Create volume series const volumeSeries = chart.addSeries(HistogramSeries, { color: '#26a69a', priceFormat: { @@ -447,7 +448,7 @@ export function AdvancedChart({ }) volumeSeriesRef.current = volumeSeries as any - // 响应式调整 (ResizeObserver) + // Responsive resize (ResizeObserver) const resizeObserver = new ResizeObserver((entries) => { if (entries.length === 0 || !entries[0].contentRect) return const { width, height } = entries[0].contentRect @@ -458,7 +459,7 @@ export function AdvancedChart({ resizeObserver.observe(chartContainerRef.current) } - // 监听鼠标移动,显示 OHLC 信息 + // Listen for crosshair movement to show OHLC info chart.subscribeCrosshairMove((param) => { if (!param.time || !param.point || !candlestickSeriesRef.current) { setTooltipData(null) @@ -473,7 +474,7 @@ export function AdvancedChart({ const candleData = data as any - // 从存储的数据中获取 volume 和 quoteVolume + // Get volume and quoteVolume from stored data const klineExtra = klineDataRef.current.get(param.time as number) || { volume: 0, quoteVolume: 0 } setTooltipData({ @@ -496,18 +497,18 @@ export function AdvancedChart({ }, []) // Chart is created once, ResizeObserver handles dimension changes - // 加载数据和指标 + // Load data and indicators useEffect(() => { - // 当 symbol 或 interval 改变时,重置初始加载标志(以便自动适配新数据) + // Reset initial load flag when symbol/interval changes (for auto-fit) isInitialLoadRef.current = true - // 清除旧的标记数据,避免旧数据影响新图表 + // Clear old marker data to prevent stale data in new chart currentMarkersDataRef.current = [] if (seriesMarkersRef.current) { try { seriesMarkersRef.current.setMarkers([]) } catch (e) { - // 忽略错误,稍后会重新创建 + // Ignore errors, will be recreated later } seriesMarkersRef.current = null } @@ -516,30 +517,30 @@ export function AdvancedChart({ if (!candlestickSeriesRef.current) return console.log('[AdvancedChart] Loading data for', symbol, interval, isRefresh ? '(refresh)' : '') - // 只在首次加载时显示 loading,刷新时不显示避免闪烁 + // Only show loading on first load, avoid flicker on refresh if (!isRefresh) { setLoading(true) } setError(null) try { - // 1. 获取K线数据 + // 1. Fetch kline data const klineData = await fetchKlineData(symbol, interval) console.log('[AdvancedChart] Loaded', klineData.length, 'klines') candlestickSeriesRef.current.setData(klineData) - // 存储 volume/quoteVolume 数据供 tooltip 使用 + // Store volume/quoteVolume data for tooltip klineDataRef.current.clear() klineData.forEach((k: any) => { klineDataRef.current.set(k.time, { volume: k.volume || 0, quoteVolume: k.quoteVolume || 0 }) }) - // 1.5 计算行情统计数据 + // 1.5 Calculate market stats if (klineData.length > 1) { const latestKline = klineData[klineData.length - 1] const prevKline = klineData[klineData.length - 2] - // 涨跌幅:当前K线收盘价 vs 前一根K线收盘价 + // Price change: current candle close vs previous candle close const priceChange = latestKline.close - prevKline.close const priceChangePercent = (priceChange / prevKline.close) * 100 @@ -565,7 +566,7 @@ export function AdvancedChart({ }) } - // 2. 显示成交量 + // 2. Display volume if (volumeSeriesRef.current) { const volumeEnabled = indicators.find(i => i.id === 'volume')?.enabled if (volumeEnabled) { @@ -576,15 +577,15 @@ export function AdvancedChart({ })) volumeSeriesRef.current.setData(volumeData) } else { - // 关闭成交量时清空数据 + // Clear data when volume is disabled volumeSeriesRef.current.setData([]) } } - // 3. 添加指标 + // 3. Add indicators updateIndicators(klineData) - // 4. 获取并显示订单标记 + // 4. Fetch and display order markers if (traderID && candlestickSeriesRef.current) { console.log('[AdvancedChart] Starting to fetch orders...') const orders = await fetchOrders(traderID, symbol) @@ -593,17 +594,17 @@ export function AdvancedChart({ if (orders.length > 0) { console.log('[AdvancedChart] Creating markers from', orders.length, 'orders') - // 提取 K 线时间数组(已排序) + // Extract sorted kline time array const klineTimes = klineData.map((k: any) => k.time as number) const klineMinTime = klineTimes[0] || 0 const klineMaxTime = klineTimes[klineTimes.length - 1] || 0 console.log('[AdvancedChart] Kline time range:', klineMinTime, '-', klineMaxTime, '(', klineTimes.length, 'candles)') - // 二分查找:找到订单时间所属的 K 线蜡烛 - // 返回 time <= orderTime 的最大 K 线时间 + // Binary search: find the kline candle for the order time + // Return the largest kline time <= orderTime const findCandleTime = (orderTime: number): number | null => { if (orderTime < klineMinTime || orderTime > klineMaxTime) { - return null // 超出范围 + return null // Out of range } let left = 0 @@ -621,11 +622,11 @@ export function AdvancedChart({ return klineTimes[left] } - // 按 K 线时间分组统计订单 + // Group orders by kline time const ordersByCandle = new Map() orders.forEach(order => { - // 使用二分查找找到对应的 K 线蜡烛时间 + // Use binary search to find matching kline candle time const candleTime = findCandleTime(order.time) if (candleTime === null) { @@ -643,7 +644,7 @@ export function AdvancedChart({ ordersByCandle.set(candleTime, existing) }) - // 为每个有订单的 K 线创建标记 + // Create markers for each kline with orders const markers: Array<{ time: Time position: 'belowBar' | 'aboveBar' @@ -654,7 +655,7 @@ export function AdvancedChart({ }> = [] ordersByCandle.forEach((counts, candleTime) => { - // 显示买入标记(绿色,在K线下方) + // Show buy markers (green, below bar) if (counts.buys > 0) { markers.push({ time: candleTime as Time, @@ -665,7 +666,7 @@ export function AdvancedChart({ size: 1, }) } - // 显示卖出标记(红色,在K线上方) + // Show sell markers (red, above bar) if (counts.sells > 0) { markers.push({ time: candleTime as Time, @@ -678,7 +679,7 @@ export function AdvancedChart({ } }) - // 按时间排序(lightweight-charts 要求标记按时间顺序) + // Sort by time (lightweight-charts requires chronological order) markers.sort((a, b) => (a.time as number) - (b.time as number)) console.log('[AdvancedChart] Valid markers:', markers.length, 'out of', orders.length) @@ -687,17 +688,17 @@ export function AdvancedChart({ console.log('[AdvancedChart] Markers data:', JSON.stringify(markers, null, 2)) try { - // 存储标记数据供后续切换使用 + // Store marker data for later toggle use currentMarkersDataRef.current = markers - // 使用 v5 API: createSeriesMarkers + // Using v5 API: createSeriesMarkers const markersToShow = showOrderMarkers ? markers : [] if (seriesMarkersRef.current) { - // 如果已经存在,更新标记 + // If already exists, update markers seriesMarkersRef.current.setMarkers(markersToShow) } else { - // 首次创建标记 + // First time creating markers seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markersToShow) } console.log('[AdvancedChart] ✅ Markers updated! Count:', markersToShow.length, 'Visible:', showOrderMarkers) @@ -721,7 +722,7 @@ export function AdvancedChart({ }) } - // 只在初始加载时自动适配视图,避免刷新时抖动 + // Auto-fit view only on initial load, avoid jitter on refresh if (isInitialLoadRef.current) { chartRef.current?.timeScale().fitContent() isInitialLoadRef.current = false @@ -734,26 +735,26 @@ export function AdvancedChart({ } } - loadData(false) // 首次加载 + loadData(false) // Initial load - // 实时自动刷新 (5秒更新一次) + // Real-time auto-refresh (every 5 seconds) const refreshInterval = setInterval(() => loadData(true), 5000) return () => clearInterval(refreshInterval) }, [symbol, interval, traderID, exchange]) - // 单独刷新挂单价格线 (60秒刷新一次,避免频繁调用交易所API) + // Refresh open order price lines separately (every 60s, avoid frequent exchange API calls) useEffect(() => { if (!traderID || !candlestickSeriesRef.current) return - // 加载挂单并显示价格线 + // Load open orders and display price lines const loadOpenOrders = async () => { try { - // 先清除旧的价格线 + // Clear old price lines first priceLinesRef.current.forEach(line => { try { candlestickSeriesRef.current?.removePriceLine(line) } catch (e) { - // 忽略清除错误 + // Ignore clear error } }) priceLinesRef.current = [] @@ -763,28 +764,28 @@ export function AdvancedChart({ if (openOrders.length > 0 && candlestickSeriesRef.current) { openOrders.forEach(order => { - // 获取触发价格 (止损/止盈用 stop_price,限价单用 price) + // Get trigger price (SL/TP use stop_price, limit orders use price) const linePrice = order.stop_price > 0 ? order.stop_price : order.price if (linePrice <= 0) return - // 判断订单类型 + // Determine order type const isStopLoss = order.type.includes('STOP') || order.type.includes('SL') const isTakeProfit = order.type.includes('TAKE_PROFIT') || order.type.includes('TP') const isLimit = order.type === 'LIMIT' - // 设置价格线样式 - let lineColor = '#F0B90B' // 默认黄色 - const lineStyle = 2 // 虚线 + // Set price line style + let lineColor = '#F0B90B' // Default yellow + const lineStyle = 2 // dashed let title = '' if (isStopLoss) { - lineColor = '#F6465D' // 红色 - 止损 + lineColor = '#F6465D' // red - stop loss title = `SL ${order.quantity}` } else if (isTakeProfit) { - lineColor = '#0ECB81' // 绿色 - 止盈 + lineColor = '#0ECB81' // green - take profit title = `TP ${order.quantity}` } else if (isLimit) { - lineColor = '#F0B90B' // 黄色 - 限价单 + lineColor = '#F0B90B' // yellow - limit order title = `Limit ${order.side} ${order.quantity}` } else { title = `${order.type} ${order.quantity}` @@ -810,10 +811,10 @@ export function AdvancedChart({ } } - // 初始加载 (延迟1秒等待图表初始化完成) + // Initial load (delay 1s to wait for chart initialization) const initialTimeout = setTimeout(loadOpenOrders, 1000) - // 60秒刷新一次挂单 + // Refresh open orders every 60 seconds const openOrdersInterval = setInterval(loadOpenOrders, 60000) return () => { @@ -822,7 +823,7 @@ export function AdvancedChart({ } }, [symbol, traderID]) - // 单独处理订单标记的显示/隐藏,避免重新加载数据 + // Handle order marker show/hide separately to avoid reloading data useEffect(() => { if (!seriesMarkersRef.current) return @@ -835,17 +836,17 @@ export function AdvancedChart({ } }, [showOrderMarkers]) - // 更新指标 + // Update indicators const updateIndicators = (klineData: Kline[]) => { if (!chartRef.current) return - // 清除旧指标 + // Clear old indicators indicatorSeriesRef.current.forEach(series => { chartRef.current?.removeSeries(series as any) }) indicatorSeriesRef.current.clear() - // 添加启用的指标 + // Add enabled indicators indicators.forEach(indicator => { if (!indicator.enabled || !chartRef.current) return @@ -864,7 +865,7 @@ export function AdvancedChart({ color: indicator.color, lineWidth: 2, title: indicator.name, - lineStyle: 2, // 虚线 + lineStyle: 2, // dashed }) series.setData(emaData as any) indicatorSeriesRef.current.set(indicator.id, series) @@ -900,7 +901,7 @@ export function AdvancedChart({ }) } - // 切换指标 + // Toggle indicator const toggleIndicator = (id: string) => { setIndicators(prev => prev.map(ind => (ind.id === id ? { ...ind, enabled: !ind.enabled } : ind)) @@ -980,7 +981,7 @@ export function AdvancedChart({
{loading && ( - {language === 'zh' ? '更新中...' : 'Updating...'} + {t('advancedChart.updating', language)} )}
- {/* 指标面板 - 专业化设计 */} + {/* Indicator panel - professional design */} {showIndicatorPanel && (
- {/* 标题栏 */} + {/* Title bar */}

- {language === 'zh' ? '技术指标' : 'Technical Indicators'} + {t('advancedChart.technicalIndicators', language)}

- {/* 指标列表 */} + {/* Indicator list */}
{indicators.map(indicator => (
)} - {/* 图表容器 */} + {/* Chart container */}
@@ -1151,7 +1152,7 @@ export function AdvancedChart({
)} - {/* NOFX 水印 */} + {/* NOFX watermark */}
- {/* 错误提示 */} + {/* Error message */} {error && (
(null) - // 当交易所ID变化时,自动切换市场类型 + // Auto-switch market type when exchange ID changes useEffect(() => { const newMarketType = getMarketTypeFromExchange(exchangeId) setMarketType(newMarketType) }, [exchangeId]) - // 根据市场类型确定交易所 + // Determine exchange from market type const marketConfig = MARKET_CONFIG[marketType] - // 优先使用传入的 exchangeId(非 hyperliquid 时) + // Prefer passed-in exchangeId (when not hyperliquid) const currentExchange = marketType === 'hyperliquid' ? 'hyperliquid' : (exchangeId || marketConfig.exchange) - // 获取可用币种列表 + // Fetch available symbol list useEffect(() => { if (marketConfig.hasDropdown) { fetch(`/api/symbols?exchange=${marketConfig.exchange}`) .then(res => res.json()) .then(data => { if (data.symbols) { - // 按类别排序: crypto > stock > forex > commodity > index + // Sort by category: crypto > stock > forex > commodity > index const categoryOrder: Record = { crypto: 0, stock: 1, forex: 2, commodity: 3, index: 4 } const sorted = [...data.symbols].sort((a: SymbolInfo, b: SymbolInfo) => { const orderA = categoryOrder[a.category] ?? 5 @@ -96,7 +97,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C } }, [marketType, marketConfig.exchange, marketConfig.hasDropdown]) - // 点击外部关闭下拉 + // Close dropdown on outside click useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -107,33 +108,33 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C return () => document.removeEventListener('mousedown', handleClickOutside) }, []) - // 切换市场类型时更新默认符号 + // Update default symbol when switching market type const handleMarketTypeChange = (type: MarketType) => { setMarketType(type) setChartSymbol(MARKET_CONFIG[type].defaultSymbol) setShowDropdown(false) } - // 过滤后的币种列表 + // Filtered symbol list const filteredSymbols = availableSymbols.filter(s => s.symbol.toLowerCase().includes(searchFilter.toLowerCase()) ) - // 当从外部选择币种时,自动切换到K线图 + // Auto-switch to kline chart when symbol selected externally useEffect(() => { if (selectedSymbol) { - console.log('[ChartTabs] 收到币种选择:', selectedSymbol, 'updateKey:', updateKey) + console.log('[ChartTabs] Symbol selected:', selectedSymbol, 'updateKey:', updateKey) setChartSymbol(selectedSymbol) setActiveTab('kline') } }, [selectedSymbol, updateKey]) - // 处理手动输入符号 + // Handle manual symbol input const handleSymbolSubmit = (e: React.FormEvent) => { e.preventDefault() if (symbolInput.trim()) { let symbol = symbolInput.trim().toUpperCase() - // 加密货币自动加 USDT 后缀 + // Auto-append USDT suffix for crypto if (marketType === 'crypto' && !symbol.endsWith('USDT')) { symbol = symbol + 'USDT' } @@ -198,7 +199,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C }`} > {config.icon} - {language === 'zh' ? config.label.zh : config.label.en} + {ts(chartTabs[config.labelKey], language)} ) })} diff --git a/web/src/components/charts/ChartWithOrders.tsx b/web/src/components/charts/ChartWithOrders.tsx index 23495bf2..7c0f6195 100644 --- a/web/src/components/charts/ChartWithOrders.tsx +++ b/web/src/components/charts/ChartWithOrders.tsx @@ -10,8 +10,9 @@ import { } from 'lightweight-charts' import { useLanguage } from '../../contexts/LanguageContext' import { httpClient } from '../../lib/httpClient' +import { t } from '../../i18n/translations' -// 订单接口定义 +// Order marker interface interface OrderMarker { time: number // Unix timestamp (seconds) price: number @@ -21,7 +22,7 @@ interface OrderMarker { symbol: string } -// K线数据接口 +// Kline data interface interface KlineData { time: UTCTimestamp open: number @@ -34,9 +35,9 @@ interface KlineData { interface ChartWithOrdersProps { symbol: string interval?: string // 1m, 5m, 15m, 1h, 4h, 1d - traderID?: string // 用于获取该trader的订单 + traderID?: string // Used to fetch orders for this trader height?: number - exchange?: string // 交易所类型:binance, bybit, okx, bitget, hyperliquid, aster, lighter + exchange?: string // Exchange type: binance, bybit, okx, bitget, hyperliquid, aster, lighter } export function ChartWithOrders({ @@ -44,7 +45,7 @@ export function ChartWithOrders({ interval = '5m', traderID, height = 500, - exchange = 'binance', // 默认使用 binance + exchange = 'binance', // Default to binance }: ChartWithOrdersProps) { const { language } = useLanguage() const chartContainerRef = useRef(null) @@ -56,16 +57,16 @@ export function ChartWithOrders({ const [tooltipData, setTooltipData] = useState(null) const tooltipRef = useRef(null) - // 解析时间:支持 Unix 时间戳(数字)或字符串格式 + // Parse time: supports Unix timestamp (number) or string format const parseCustomTime = (time: any): number => { if (!time) { console.warn('[ChartWithOrders] Empty time value') return 0 } - // 如果已经是数字(Unix 时间戳) + // If already a number (Unix timestamp) if (typeof time === 'number') { - // 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳) + // Determine ms vs seconds: if > 10^12, treat as milliseconds if (time > 1000000000000) { const seconds = Math.floor(time / 1000) console.log('[ChartWithOrders] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')') @@ -78,7 +79,7 @@ export function ChartWithOrders({ const timeStr = String(time) console.log('[ChartWithOrders] Parsing time string:', timeStr) - // 尝试标准ISO格式 + // Try standard ISO format const isoTime = new Date(timeStr).getTime() if (!isNaN(isoTime) && isoTime > 0) { const timestamp = Math.floor(isoTime / 1000) @@ -86,7 +87,7 @@ export function ChartWithOrders({ return timestamp } - // 解析自定义格式 "MM-DD HH:mm UTC" (兼容旧数据) + // Parse custom format "MM-DD HH:mm UTC" (for legacy data) const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/) if (match) { const currentYear = new Date().getFullYear() @@ -107,10 +108,10 @@ export function ChartWithOrders({ return 0 } - // 从我们的服务获取K线数据 + // Fetch kline data from our service const fetchKlineData = async (symbol: string, interval: string): Promise => { try { - const limit = 2000 // 获取最近2000根K线 (更多历史数据) + const limit = 2000 // Fetch recent 2000 candles (more historical data) const klineUrl = `/api/klines?symbol=${symbol}&interval=${interval}&limit=${limit}&exchange=${exchange}` const result = await httpClient.get(klineUrl) @@ -121,10 +122,10 @@ export function ChartWithOrders({ const data = result.data - // 转换后端数据格式到 lightweight-charts 格式 - // 后端返回的是 market.Kline 格式: {OpenTime, Open, High, Low, Close, Volume, ...} + // Convert backend data format to lightweight-charts format + // Backend returns market.Kline format: {OpenTime, Open, High, Low, Close, Volume, ...} return data.map((candle: any) => ({ - time: Math.floor(candle.openTime / 1000) as UTCTimestamp, // 毫秒转秒 + time: Math.floor(candle.openTime / 1000) as UTCTimestamp, // ms to seconds open: candle.open, high: candle.high, low: candle.low, @@ -137,10 +138,10 @@ export function ChartWithOrders({ } } - // 获取订单数据 + // Fetch order data const fetchOrders = async (traderID: string, symbol: string): Promise => { try { - // 从后端 API 获取该 trader 的订单记录(只获取已成交的订单) + // Fetch filled orders for this trader from backend API const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=50`) if (!result.success || !result.data) { @@ -151,7 +152,7 @@ export function ChartWithOrders({ const orders = result.data const markers: OrderMarker[] = [] - // 转换订单数据为标记格式 + // Convert order data to marker format orders.forEach((order: any) => { const createdAt = order.created_at || order.CreatedAt const filledAt = order.filled_at || order.FilledAt @@ -162,14 +163,14 @@ export function ChartWithOrders({ const status = order.status || order.Status const symbol = order.symbol || order.Symbol - // 使用成交时间(如果有)或创建时间 + // Use fill time (if available) or creation time const orderTime = filledAt || createdAt if (!orderTime) return const timeSeconds = parseCustomTime(orderTime) if (timeSeconds === 0) return - // 使用平均成交价(如果有)或订单价格 + // Use average fill price (if available) or order price const orderPrice = avgPrice || price if (!orderPrice || orderPrice === 0) return @@ -191,7 +192,7 @@ export function ChartWithOrders({ } } - // 初始化图表 + // Initialize chart useEffect(() => { if (!chartContainerRef.current) { console.error('[ChartWithOrders] Container ref is null') @@ -201,7 +202,7 @@ export function ChartWithOrders({ console.log('[ChartWithOrders] Initializing chart for', symbol, interval) try { - // 创建图表 + // Create chart const chart = createChart(chartContainerRef.current, { width: chartContainerRef.current.clientWidth, height: height, @@ -240,7 +241,7 @@ export function ChartWithOrders({ chartRef.current = chart - // 创建K线系列 (使用 v5 API) + // Create candlestick series (using v5 API) const candlestickSeries = chart.addSeries(CandlestickSeries, { upColor: '#0ECB81', downColor: '#F6465D', @@ -252,7 +253,7 @@ export function ChartWithOrders({ candlestickSeriesRef.current = candlestickSeries as any - // 响应式调整 + // Responsive resize const handleResize = () => { if (chartContainerRef.current && chartRef.current) { chartRef.current.applyOptions({ @@ -263,7 +264,7 @@ export function ChartWithOrders({ window.addEventListener('resize', handleResize) - // 监听鼠标移动,显示 OHLC 信息 + // Listen for crosshair movement to show OHLC info chart.subscribeCrosshairMove((param) => { if (!param.time || !param.point || !candlestickSeriesRef.current) { setTooltipData(null) @@ -298,7 +299,7 @@ export function ChartWithOrders({ } }, [height]) - // 加载数据 + // Load data useEffect(() => { const loadData = async () => { if (!candlestickSeriesRef.current) { @@ -311,22 +312,22 @@ export function ChartWithOrders({ setError(null) try { - // 1. 获取K线数据 + // 1. Fetch kline data console.log('[ChartWithOrders] Fetching kline data...') const klineData = await fetchKlineData(symbol, interval) console.log('[ChartWithOrders] Kline data received:', klineData.length, 'candles') candlestickSeriesRef.current.setData(klineData) - // 构建 K 线时间集合,用于快速查找 + // Build kline time set for quick lookup const klineTimeSet = new Set(klineData.map(k => k.time as number)) const klineMinTime = klineData.length > 0 ? klineData[0].time : 0 const klineMaxTime = klineData.length > 0 ? klineData[klineData.length - 1].time : 0 console.log('[ChartWithOrders] Kline time range:', klineMinTime, '-', klineMaxTime, 'candles:', klineData.length) - // 计算时间周期的秒数 + // Calculate interval in seconds const getIntervalSeconds = (interval: string): number => { const match = interval.match(/(\d+)([smhd])/) - if (!match) return 60 // 默认1分钟 + if (!match) return 60 // Default 1 minute const [, num, unit] = match const n = parseInt(num) switch (unit) { @@ -340,7 +341,7 @@ export function ChartWithOrders({ const intervalSeconds = getIntervalSeconds(interval) console.log('[ChartWithOrders] Interval:', interval, '=', intervalSeconds, 'seconds') - // 2. 获取订单数据并添加标记 + // 2. Fetch order data and add markers if (traderID) { console.log('[ChartWithOrders] Fetching orders for trader:', traderID, 'symbol:', symbol) const orders = await fetchOrders(traderID, symbol) @@ -350,7 +351,7 @@ export function ChartWithOrders({ console.log('[ChartWithOrders] No orders to display') } - // 转换订单为图表标记,并对齐到 K 线时间 + // Convert orders to chart markers, aligned to kline time const markers: Array<{ time: Time position: 'belowBar' @@ -362,10 +363,10 @@ export function ChartWithOrders({ }> = [] orders.forEach((order) => { - // 将订单时间对齐到 K 线周期(向下取整) + // Align order time to kline interval (floor) const alignedTime = Math.floor(order.time / intervalSeconds) * intervalSeconds - // 检查对齐后的时间是否在 K 线数据中存在 + // Check if aligned time exists in kline data if (!klineTimeSet.has(alignedTime)) { console.warn('[ChartWithOrders] ⚠️ Skipping order - no matching kline:', order.time, '→', alignedTime, '(', new Date(order.time * 1000).toISOString(), ')') @@ -389,12 +390,12 @@ export function ChartWithOrders({ console.log('[ChartWithOrders] Setting', markers.length, 'markers on chart') try { - // 使用 v5 API: createSeriesMarkers + // Using v5 API: createSeriesMarkers if (seriesMarkersRef.current) { - // 如果已经存在,更新标记 + // If already exists, update markers seriesMarkersRef.current.setMarkers(markers) } else { - // 首次创建标记 + // First time creating markers seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markers) } console.log('[ChartWithOrders] ✅ Markers set successfully!') @@ -403,23 +404,23 @@ export function ChartWithOrders({ } } - // 自动适配视图 + // Auto-fit view chartRef.current?.timeScale().fitContent() setLoading(false) } catch (err) { console.error('Error loading chart data:', err) - setError(language === 'zh' ? '加载图表数据失败' : 'Failed to load chart data') + setError(t('chartWithOrders.failedToLoad', language)) setLoading(false) } } loadData() - // 自动刷新 - 每30秒更新一次K线数据 + // Auto-refresh - update kline data every 30 seconds const refreshInterval = setInterval(() => { loadData() - }, 30000) // 30秒 + }, 30000) // 30 seconds return () => { clearInterval(refreshInterval) @@ -428,7 +429,7 @@ export function ChartWithOrders({ return (
- {/* 标题栏 */} + {/* Title bar */}
📈 @@ -438,12 +439,12 @@ export function ChartWithOrders({
{loading && (
- {language === 'zh' ? '加载中...' : 'Loading...'} + {t('chartWithOrders.loading', language)}
)}
- {/* 图表容器 */} + {/* Chart container */}
@@ -498,7 +499,7 @@ export function ChartWithOrders({ )}
- {/* 错误提示 */} + {/* Error display */} {error && (
)} - {/* 图例说明 */} + {/* Legend */}
B - {language === 'zh' ? 'BUY (买入)' : 'BUY'} + {t('chartWithOrders.buy', language)}
S - {language === 'zh' ? 'SELL (卖出)' : 'SELL'} + {t('chartWithOrders.sell', language)}
diff --git a/web/src/components/charts/ComparisonChart.tsx b/web/src/components/charts/ComparisonChart.tsx index 4c91ba29..8ad4809a 100644 --- a/web/src/components/charts/ComparisonChart.tsx +++ b/web/src/components/charts/ComparisonChart.tsx @@ -21,12 +21,12 @@ import { BarChart3, TrendingUp, TrendingDown, Zap } from 'lucide-react' // Time period options: 1D, 3D, 7D, 30D, All const TIME_PERIODS = [ - { key: '1d', hours: 24, label: { en: '1D', zh: '1天' } }, - { key: '3d', hours: 72, label: { en: '3D', zh: '3天' } }, - { key: '7d', hours: 168, label: { en: '7D', zh: '7天' } }, - { key: '30d', hours: 720, label: { en: '30D', zh: '30天' } }, - { key: 'all', hours: 0, label: { en: 'All', zh: '全部' } }, -] + { key: '1d', hours: 24 }, + { key: '3d', hours: 72 }, + { key: '7d', hours: 168 }, + { key: '30d', hours: 720 }, + { key: 'all', hours: 0 }, +] as const interface ComparisonChartProps { traders: CompetitionTraderData[] @@ -352,7 +352,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`, }} > - {language === 'zh' ? period.label.zh : period.label.en} + {t(`comparisonChart.${period.key}`, language)} ))}
diff --git a/web/src/components/common/ConfirmDialog.tsx b/web/src/components/common/ConfirmDialog.tsx index 2769d63d..ebd24e59 100644 --- a/web/src/components/common/ConfirmDialog.tsx +++ b/web/src/components/common/ConfirmDialog.tsx @@ -58,8 +58,8 @@ export function ConfirmDialogProvider({ const [state, setState] = useState({ isOpen: false, message: '', - okText: '确认', - cancelText: '取消', + okText: 'Confirm', + cancelText: 'Cancel', }) const confirm = useCallback((options: ConfirmOptions): Promise => { @@ -68,14 +68,14 @@ export function ConfirmDialogProvider({ isOpen: true, title: options.title, message: options.message, - okText: options.okText || '确认', - cancelText: options.cancelText || '取消', + okText: options.okText || 'Confirm', + cancelText: options.cancelText || 'Cancel', resolve, }) }) }, []) - // 注册全局 confirm 函数 + // Register global confirm function useEffect(() => { setGlobalConfirm(confirm) }, [confirm]) diff --git a/web/src/components/common/MetricTooltip.tsx b/web/src/components/common/MetricTooltip.tsx index 626a2a60..50187e98 100644 --- a/web/src/components/common/MetricTooltip.tsx +++ b/web/src/components/common/MetricTooltip.tsx @@ -3,6 +3,7 @@ import { createPortal } from 'react-dom' import { HelpCircle } from 'lucide-react' import katex from 'katex' import 'katex/dist/katex.min.css' +import { t } from '../../i18n/translations' export interface MetricDefinition { key: string @@ -241,6 +242,7 @@ export function MetricTooltip({ const name = language === 'zh' ? metric.nameZh : metric.nameEn const description = language === 'zh' ? metric.descriptionZh : metric.descriptionEn + const formulaLabel = t('metricTooltip.formula', language as 'en' | 'zh' | 'id') const tooltipContent = (
- {language === 'zh' ? '计算公式' : 'Formula'} + {formulaLabel}
{ - const translations: Record> = { - sourceType: { zh: '数据来源类型', en: 'Source Type' }, - static: { zh: '静态列表', en: 'Static List' }, - ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider' }, - oi_top: { zh: 'OI 持仓增加', en: 'OI Increase' }, - oi_low: { zh: 'OI 持仓减少', en: 'OI Decrease' }, - mixed: { zh: '混合模式', en: 'Mixed Mode' }, - staticCoins: { zh: '自定义币种', en: 'Custom Coins' }, - addCoin: { zh: '添加币种', en: 'Add Coin' }, - useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' }, - ai500Limit: { zh: '数量上限', en: 'Limit' }, - useOITop: { zh: '启用 OI 持仓增加榜', en: 'Enable OI Increase' }, - oiTopLimit: { zh: '数量上限', en: 'Limit' }, - useOILow: { zh: '启用 OI 持仓减少榜', en: 'Enable OI Decrease' }, - oiLowLimit: { zh: '数量上限', en: 'Limit' }, - staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' }, - ai500Desc: { - zh: '使用 AI500 智能筛选的热门币种', - en: 'Use AI500 smart-filtered popular coins', - }, - oiTopDesc: { - zh: '持仓增加榜,适合做多', - en: 'OI increase ranking, for long', - }, - oi_lowDesc: { - zh: '持仓减少榜,适合做空', - en: 'OI decrease ranking, for short', - }, - mixedDesc: { - zh: '组合多种数据源', - en: 'Combine multiple sources', - }, - mixedConfig: { zh: '组合数据源配置', en: 'Combined Sources Configuration' }, - mixedSummary: { zh: '已选组合', en: 'Selected Sources' }, - maxCoins: { zh: '最多', en: 'Up to' }, - coins: { zh: '个币种', en: 'coins' }, - dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' }, - excludedCoins: { zh: '排除币种', en: 'Excluded Coins' }, - excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded' }, - addExcludedCoin: { zh: '添加排除', en: 'Add Excluded' }, - nofxosNote: { zh: '使用 NofxOS API Key(在指标配置中设置)', en: 'Uses NofxOS API Key (set in Indicators config)' }, - } - return translations[key]?.[language] || key - } - const sourceTypes = [ { value: 'static', icon: List, color: '#848E9C' }, { value: 'ai500', icon: Database, color: '#F0B90B' }, @@ -82,15 +37,15 @@ export function CoinSourceEditor({ totalLimit += config.ai500_limit || 10 } if (config.use_oi_top) { - sources.push(`${language === 'zh' ? 'OI增' : 'OI↑'}(${config.oi_top_limit || 10})`) + sources.push(`${ts(coinSource.oiIncreaseShort, language)}(${config.oi_top_limit || 10})`) totalLimit += config.oi_top_limit || 10 } if (config.use_oi_low) { - sources.push(`${language === 'zh' ? 'OI减' : 'OI↓'}(${config.oi_low_limit || 10})`) + sources.push(`${ts(coinSource.oiDecreaseShort, language)}(${config.oi_low_limit || 10})`) totalLimit += config.oi_low_limit || 10 } if ((config.static_coins || []).length > 0) { - sources.push(`${language === 'zh' ? '自定义' : 'Custom'}(${config.static_coins?.length || 0})`) + sources.push(`${ts(coinSource.custom, language)}(${config.static_coins?.length || 0})`) totalLimit += config.static_coins?.length || 0 } @@ -191,7 +146,7 @@ export function CoinSourceEditor({ {/* Source Type Selector */}
{sourceTypes.map(({ value, icon: Icon, color }) => ( @@ -209,10 +164,10 @@ export function CoinSourceEditor({ >
- {t(value)} + {ts(coinSource[value as keyof typeof coinSource], language)}
- {t(`${value}Desc`)} + {ts(coinSource[`${value}Desc` as keyof typeof coinSource], language)}
))} @@ -223,7 +178,7 @@ export function CoinSourceEditor({ {config.source_type === 'static' && (
{(config.static_coins || []).map((coin) => ( @@ -258,7 +213,7 @@ export function CoinSourceEditor({ className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors bg-nofx-gold text-black hover:bg-yellow-500" > - {t('addCoin')} + {ts(coinSource.addCoin, language)}
)} @@ -270,11 +225,11 @@ export function CoinSourceEditor({

- {t('excludedCoinsDesc')} + {ts(coinSource.excludedCoinsDesc, language)}

{(config.excluded_coins || []).map((coin) => ( @@ -295,7 +250,7 @@ export function CoinSourceEditor({ ))} {(config.excluded_coins || []).length === 0 && ( - {language === 'zh' ? '无' : 'None'} + {ts(coinSource.excludedNone, language)} )}
@@ -314,7 +269,7 @@ export function CoinSourceEditor({ className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm bg-nofx-danger text-white hover:bg-red-600" > - {t('addExcludedCoin')} + {ts(coinSource.addExcludedCoin, language)}
)} @@ -329,7 +284,7 @@ export function CoinSourceEditor({
- AI500 {t('dataSourceConfig')} + AI500 {ts(coinSource.dataSourceConfig, language)}
@@ -346,13 +301,13 @@ export function CoinSourceEditor({ disabled={disabled} className="w-5 h-5 rounded accent-nofx-gold" /> - {t('useAI500')} + {ts(coinSource.useAI500, language)} {config.use_ai500 && (
- {t('ai500Limit')}: + {ts(coinSource.ai500Limit, language)}: - {t('nofxosNote')} + {ts(coinSource.nofxosNote, language)}

@@ -443,7 +398,7 @@ export function CoinSourceEditor({
- OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig')} + {ts(coinSource.oiDecreaseTitle, language)} {ts(coinSource.dataSourceConfig, language)}
@@ -460,13 +415,13 @@ export function CoinSourceEditor({ disabled={disabled} className="w-5 h-5 rounded accent-red-500" /> - {t('useOILow')} + {ts(coinSource.useOILow, language)} {config.use_oi_low && (
- {t('oiLowLimit')}: + {ts(coinSource.oiLowLimit, language)}:

- {t('totalInvestmentDesc')} + {ts(gridConfig.totalInvestmentDesc, language)}

- {t('leverageDesc')} + {ts(gridConfig.leverageDesc, language)}

- {t('gridParameters')} + {ts(gridConfig.gridParameters, language)}

@@ -209,10 +145,10 @@ export function GridConfigEditor({ {/* Grid Count */}

- {t('gridCountDesc')} + {ts(gridConfig.gridCountDesc, language)}

- {t('distributionDesc')} + {ts(gridConfig.distributionDesc, language)}

@@ -254,7 +190,7 @@ export function GridConfigEditor({

- {t('priceBounds')} + {ts(gridConfig.priceBounds, language)}

@@ -263,10 +199,10 @@ export function GridConfigEditor({

- {t('useAtrBoundsDesc')} + {ts(gridConfig.useAtrBoundsDesc, language)}