refactor: optimize codebase encoding

This commit is contained in:
tinkle-community
2026-03-12 16:12:08 +08:00
parent 2314ece9d1
commit 736d2d385d
61 changed files with 2301 additions and 1533 deletions
+1 -1
View File
@@ -61,6 +61,6 @@ DB_NAME=nofx
DB_SSLMODE=disable DB_SSLMODE=disable
# 数据库配置 - SQLite(默认) # Database configuration - SQLite (default)
DB_TYPE=sqlite DB_TYPE=sqlite
DB_PATH=data/data.db DB_PATH=data/data.db
+19 -19
View File
@@ -1,7 +1,7 @@
name: PR Docker Build Check name: PR Docker Build Check
# PR 时只做轻量级构建检查,不推送镜像 # Lightweight build check on PR only, no image push
# 策略: 快速验证 amd64 + 抽样检查 arm64 (backend only) # Strategy: Quick verify amd64 + spot check arm64 (backend only)
on: on:
pull_request: pull_request:
branches: branches:
@@ -18,7 +18,7 @@ on:
- '.github/workflows/pr-docker-check.yml' - '.github/workflows/pr-docker-check.yml'
jobs: jobs:
# 快速检查: 所有镜像的 amd64 版本 # Quick check: amd64 builds for all images
docker-build-amd64: docker-build-amd64:
name: Build Check (amd64) name: Build Check (amd64)
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@@ -31,7 +31,7 @@ jobs:
include: include:
- name: backend - name: backend
dockerfile: ./docker/Dockerfile.backend dockerfile: ./docker/Dockerfile.backend
test_run: true # 需要测试运行 test_run: true # Needs test run
- name: frontend - name: frontend
dockerfile: ./docker/Dockerfile.frontend dockerfile: ./docker/Dockerfile.frontend
test_run: true test_run: true
@@ -51,7 +51,7 @@ jobs:
file: ${{ matrix.dockerfile }} file: ${{ matrix.dockerfile }}
platforms: linux/amd64 platforms: linux/amd64
push: false push: false
load: true # 加载到本地 Docker,用于测试运行 load: true # Load into local Docker for test run
tags: nofx-${{ matrix.name }}:pr-test tags: nofx-${{ matrix.name }}:pr-test
cache-from: type=gha,scope=${{ matrix.name }}-amd64 cache-from: type=gha,scope=${{ matrix.name }}-amd64
cache-to: type=gha,mode=max,scope=${{ matrix.name }}-amd64 cache-to: type=gha,mode=max,scope=${{ matrix.name }}-amd64
@@ -66,12 +66,12 @@ jobs:
run: | run: |
echo "🧪 Testing container startup..." echo "🧪 Testing container startup..."
# 启动容器 # Start container
docker run -d --name test-${{ matrix.name }} \ docker run -d --name test-${{ matrix.name }} \
--health-cmd="exit 0" \ --health-cmd="exit 0" \
nofx-${{ matrix.name }}:pr-test nofx-${{ matrix.name }}:pr-test
# 等待容器启动 (最多 30 秒) # Wait for container to start (up to 30 seconds)
for i in {1..30}; do for i in {1..30}; do
if docker ps | grep -q test-${{ matrix.name }}; then if docker ps | grep -q test-${{ matrix.name }}; then
echo "✅ Container started successfully" echo "✅ Container started successfully"
@@ -93,7 +93,7 @@ jobs:
echo "📦 Image size: ${SIZE_MB} MB" echo "📦 Image size: ${SIZE_MB} MB"
# 警告阈值 # Warning thresholds
if [ "${{ matrix.name }}" = "backend" ] && [ $SIZE_MB -gt 500 ]; then if [ "${{ matrix.name }}" = "backend" ] && [ $SIZE_MB -gt 500 ]; then
echo "⚠️ Warning: Backend image is larger than 500MB" echo "⚠️ Warning: Backend image is larger than 500MB"
elif [ "${{ matrix.name }}" = "frontend" ] && [ $SIZE_MB -gt 200 ]; then elif [ "${{ matrix.name }}" = "frontend" ] && [ $SIZE_MB -gt 200 ]; then
@@ -102,10 +102,10 @@ jobs:
echo "✅ Image size is reasonable" echo "✅ Image size is reasonable"
fi fi
# ARM64 原生构建检查: 使用 GitHub 原生 ARM64 runner (快速!) # ARM64 native build check: Uses GitHub native ARM64 runner (fast!)
docker-build-arm64-native: docker-build-arm64-native:
name: Build Check (arm64 native - backend) name: Build Check (arm64 native - backend)
runs-on: ubuntu-22.04-arm # 原生 ARM64 runner runs-on: ubuntu-22.04-arm # Native ARM64 runner
permissions: permissions:
contents: read contents: read
@@ -113,19 +113,19 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
# 原生 ARM64 不需要 QEMU,直接构建 # Native ARM64 does not need QEMU, builds directly
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build backend image (arm64 native) - name: Build backend image (arm64 native)
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
timeout-minutes: 15 # 原生构建更快! timeout-minutes: 15 # Native builds are faster!
with: with:
context: . context: .
file: ./docker/Dockerfile.backend file: ./docker/Dockerfile.backend
platforms: linux/arm64 platforms: linux/arm64
push: false push: false
load: true # 加载到本地,用于测试 load: true # Load locally for testing
tags: nofx-backend:pr-test-arm64 tags: nofx-backend:pr-test-arm64
cache-from: type=gha,scope=backend-arm64 cache-from: type=gha,scope=backend-arm64
cache-to: type=gha,mode=max,scope=backend-arm64 cache-to: type=gha,mode=max,scope=backend-arm64
@@ -139,12 +139,12 @@ jobs:
run: | run: |
echo "🧪 Testing ARM64 container startup..." echo "🧪 Testing ARM64 container startup..."
# 启动容器 # Start container
docker run -d --name test-backend-arm64 \ docker run -d --name test-backend-arm64 \
--health-cmd="exit 0" \ --health-cmd="exit 0" \
nofx-backend:pr-test-arm64 nofx-backend:pr-test-arm64
# 等待启动 # Wait for startup
for i in {1..30}; do for i in {1..30}; do
if docker ps | grep -q test-backend-arm64; then if docker ps | grep -q test-backend-arm64; then
echo "✅ ARM64 container started successfully" echo "✅ ARM64 container started successfully"
@@ -165,14 +165,14 @@ jobs:
echo "Using GitHub native ARM64 runner - no QEMU needed!" echo "Using GitHub native ARM64 runner - no QEMU needed!"
echo "Build time is ~3x faster than emulation" echo "Build time is ~3x faster than emulation"
# 汇总检查结果 # Aggregate check results
check-summary: check-summary:
name: Docker Build Summary name: Docker Build Summary
needs: [docker-build-amd64, docker-build-arm64-native] needs: [docker-build-amd64, docker-build-arm64-native]
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: always() if: always()
permissions: permissions:
pull-requests: write # 用于发布评论 pull-requests: write # For posting comments
steps: steps:
- name: Check build results - name: Check build results
id: check id: check
@@ -180,7 +180,7 @@ jobs:
echo "## 🐳 Docker Build Check Results" >> $GITHUB_STEP_SUMMARY echo "## 🐳 Docker Build Check Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
# 检查 amd64 构建 # Check amd64 build
if [[ "${{ needs.docker-build-amd64.result }}" == "success" ]]; then if [[ "${{ needs.docker-build-amd64.result }}" == "success" ]]; then
echo "✅ **AMD64 builds**: All passed" >> $GITHUB_STEP_SUMMARY echo "✅ **AMD64 builds**: All passed" >> $GITHUB_STEP_SUMMARY
AMD64_OK=true AMD64_OK=true
@@ -189,7 +189,7 @@ jobs:
AMD64_OK=false AMD64_OK=false
fi fi
# 检查 arm64 构建 # Check arm64 build
if [[ "${{ needs.docker-build-arm64-native.result }}" == "success" ]]; then if [[ "${{ needs.docker-build-arm64-native.result }}" == "success" ]]; then
echo "✅ **ARM64 build** (native): Backend passed (frontend will be verified after merge)" >> $GITHUB_STEP_SUMMARY echo "✅ **ARM64 build** (native): Backend passed (frontend will be verified after merge)" >> $GITHUB_STEP_SUMMARY
ARM64_OK=true ARM64_OK=true
@@ -1,6 +1,6 @@
name: PR Docker Compose Healthcheck name: PR Docker Compose Healthcheck
# 驗證 docker-compose.yml healthcheck 配置在 Alpine 容器中正常工作 # Verify docker-compose.yml healthcheck config works correctly in Alpine containers
on: on:
pull_request: pull_request:
branches: branches:
+10 -10
View File
@@ -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 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 FROM ghcr.io/nofxaios/nofx/nofx-frontend:latest AS frontend
# 最终镜像 # Final image
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache ca-certificates tzdata sqlite nginx openssl gettext RUN apk add --no-cache ca-certificates tzdata sqlite nginx openssl gettext
# 复制后端二进制 # Copy backend binary
COPY --from=backend /app/nofx /app/nofx COPY --from=backend /app/nofx /app/nofx
# 复制 TA-Lib # Copy TA-Lib libraries
COPY --from=backend /usr/local/lib/libta_lib* /usr/local/lib/ COPY --from=backend /usr/local/lib/libta_lib* /usr/local/lib/
RUN ldconfig /usr/local/lib 2>/dev/null || true RUN ldconfig /usr/local/lib 2>/dev/null || true
# 复制前端静态文件 # Copy frontend static files
COPY --from=frontend /usr/share/nginx/html /usr/share/nginx/html COPY --from=frontend /usr/share/nginx/html /usr/share/nginx/html
WORKDIR /app WORKDIR /app
RUN mkdir -p /app/data RUN mkdir -p /app/data
# 启动脚本(包含 nginx 配置生成) # Startup script (includes nginx config generation)
COPY railway/start.sh /app/start.sh COPY railway/start.sh /app/start.sh
RUN chmod +x /app/start.sh RUN chmod +x /app/start.sh
ENV DB_PATH=/app/data/data.db ENV DB_PATH=/app/data/data.db
# Railway 会自动设置 PORT 环境变量 # Railway automatically sets the PORT environment variable
EXPOSE 8080 EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
+7 -7
View File
@@ -187,7 +187,7 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i
} }
// Convert coinank kline format to market.Kline format // 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)) klines := make([]market.Kline, len(coinankKlines))
for i, ck := range coinankKlines { for i, ck := range coinankKlines {
klines[i] = market.Kline{ klines[i] = market.Kline{
@@ -196,8 +196,8 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i
High: ck.High, High: ck.High,
Low: ck.Low, Low: ck.Low,
Close: ck.Close, Close: ck.Close,
Volume: ck.Volume, // BTC 数量 Volume: ck.Volume, // BTC quantity
QuoteVolume: ck.Quantity, // USDT 成交额 QuoteVolume: ck.Quantity, // USDT turnover
CloseTime: ck.EndTime, CloseTime: ck.EndTime,
} }
} }
@@ -229,8 +229,8 @@ func (s *Server) getKlinesFromAlpaca(symbol, interval string, limit int) ([]mark
High: bar.High, High: bar.High,
Low: bar.Low, Low: bar.Low,
Close: bar.Close, Close: bar.Close,
Volume: float64(bar.Volume), // 股数 Volume: float64(bar.Volume), // share count
QuoteVolume: float64(bar.Volume) * bar.Close, // 成交额 = 股数 * 收盘价 (USD) QuoteVolume: float64(bar.Volume) * bar.Close, // turnover = shares * close price (USD)
CloseTime: bar.Timestamp.UnixMilli(), CloseTime: bar.Timestamp.UnixMilli(),
} }
} }
@@ -311,8 +311,8 @@ func (s *Server) getKlinesFromHyperliquid(symbol, interval string, limit int) ([
High: high, High: high,
Low: low, Low: low,
Close: close, Close: close,
Volume: volume, // 合约数量 Volume: volume, // contract quantity
QuoteVolume: volume * close, // 成交额 (USD) QuoteVolume: volume * close, // turnover (USD)
CloseTime: candle.CloseTime, CloseTime: candle.CloseTime,
} }
} }
+1 -1
View File
@@ -195,7 +195,7 @@ func (cfg *BacktestConfig) ToStrategyConfig() *store.StrategyConfig {
if cfg.loadedStrategy != nil { if cfg.loadedStrategy != nil {
result := *cfg.loadedStrategy // Make a copy 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 { if len(cfg.Symbols) > 0 {
result.CoinSource.SourceType = "static" result.CoinSource.SourceType = "static"
result.CoinSource.StaticCoins = cfg.Symbols result.CoinSource.StaticCoins = cfg.Symbols
+1 -1
View File
@@ -6,7 +6,7 @@ services:
dockerfile: ./docker/Dockerfile.backend dockerfile: ./docker/Dockerfile.backend
container_name: nofx-trading container_name: nofx-trading
restart: unless-stopped restart: unless-stopped
stop_grace_period: 30s # 允许应用有 30 秒时间优雅关闭 stop_grace_period: 30s # Allow the app 30 seconds for graceful shutdown
ports: ports:
- "${NOFX_BACKEND_PORT:-8080}:8080" - "${NOFX_BACKEND_PORT:-8080}:8080"
- "6060:6060" # pprof profiling - "6060:6060" # pprof profiling
+6 -6
View File
@@ -247,7 +247,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
return e.filterExcludedCoins(candidates), nil return e.filterExcludedCoins(candidates), nil
case "ai500": case "ai500":
// 检查 use_ai500 标志,如果为 false 则回退到静态币种 // Check use_ai500 flag; if false, fall back to static coins
if !coinSource.UseAI500 { if !coinSource.UseAI500 {
logger.Infof("⚠️ source_type is 'ai500' but use_ai500 is false, falling back to static coins") logger.Infof("⚠️ source_type is 'ai500' but use_ai500 is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins { for _, symbol := range coinSource.StaticCoins {
@@ -263,11 +263,11 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 空列表是正常情况,直接返回 // Empty list is a normal condition, return directly
return e.filterExcludedCoins(coins), nil return e.filterExcludedCoins(coins), nil
case "oi_top": case "oi_top":
// 检查 use_oi_top 标志,如果为 false 则回退到静态币种 // Check use_oi_top flag; if false, fall back to static coins
if !coinSource.UseOITop { if !coinSource.UseOITop {
logger.Infof("⚠️ source_type is 'oi_top' but use_oi_top is false, falling back to static coins") logger.Infof("⚠️ source_type is 'oi_top' but use_oi_top is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins { for _, symbol := range coinSource.StaticCoins {
@@ -283,11 +283,11 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 空列表是正常情况,直接返回 // Empty list is a normal condition, return directly
return e.filterExcludedCoins(coins), nil return e.filterExcludedCoins(coins), nil
case "oi_low": case "oi_low":
// 持仓减少榜,适合做空 // OI decrease ranking, suitable for short positions
if !coinSource.UseOILow { if !coinSource.UseOILow {
logger.Infof("⚠️ source_type is 'oi_low' but use_oi_low is false, falling back to static coins") logger.Infof("⚠️ source_type is 'oi_low' but use_oi_low is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins { for _, symbol := range coinSource.StaticCoins {
@@ -303,7 +303,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 空列表是正常情况,直接返回 // Empty list is a normal condition, return directly
return e.filterExcludedCoins(coins), nil return e.filterExcludedCoins(coins), nil
case "hyper_all": case "hyper_all":
+4 -4
View File
@@ -441,7 +441,7 @@ func (e *StrategyEngine) formatPositionInfo(index int, pos PositionInfo, ctx *Co
func (e *StrategyEngine) formatCoinSourceTag(sources []string) string { func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
if len(sources) > 1 { if len(sources) > 1 {
// 多信号源组合 // Multiple signal source combination
hasAI500 := false hasAI500 := false
hasOITop := false hasOITop := false
hasOILow := false hasOILow := false
@@ -482,9 +482,9 @@ func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
case "ai500": case "ai500":
return " (AI500)" return " (AI500)"
case "oi_top": case "oi_top":
return " (OI_Top 持仓增加)" return " (OI_Top OI increase)"
case "oi_low": case "oi_low":
return " (OI_Low 持仓减少)" return " (OI_Low OI decrease)"
case "static": case "static":
return " (Manual selection)" return " (Manual selection)"
case "hyper_all": case "hyper_all":
@@ -504,7 +504,7 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string {
var sb strings.Builder var sb strings.Builder
indicators := e.config.Indicators indicators := e.config.Indicators
// 明确标注币种 // Clearly label the coin symbol
sb.WriteString(fmt.Sprintf("=== %s Market Data ===\n\n", data.Symbol)) sb.WriteString(fmt.Sprintf("=== %s Market Data ===\n\n", data.Symbol))
sb.WriteString(fmt.Sprintf("current_price = %.4f", data.CurrentPrice)) sb.WriteString(fmt.Sprintf("current_price = %.4f", data.CurrentPrice))
+49 -48
View File
@@ -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 { func FormatContextForAI(ctx *Context, lang Language) string {
var sb strings.Builder var sb strings.Builder
// 1. 添加Schema说明(让AI理解数据格式) // 1. Add schema description (so AI understands data format)
sb.WriteString(GetSchemaPrompt(lang)) sb.WriteString(GetSchemaPrompt(lang))
sb.WriteString("\n---\n\n") sb.WriteString("\n---\n\n")
// 2. 当前状态概览 // 2. Current state overview
sb.WriteString(formatContextData(ctx, lang)) sb.WriteString(formatContextData(ctx, lang))
return sb.String() 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 { func FormatContextDataOnly(ctx *Context, lang Language) string {
return formatContextData(ctx, lang) return formatContextData(ctx, lang)
} }
// formatContextData 格式化核心数据部分 // formatContextData formats the core data section
func formatContextData(ctx *Context, lang Language) string { func formatContextData(ctx *Context, lang Language) string {
var sb strings.Builder var sb strings.Builder
// 1. 当前状态概览 // 1. Current state overview
if lang == LangChinese { if lang == LangChinese {
sb.WriteString(formatHeaderZH(ctx)) sb.WriteString(formatHeaderZH(ctx))
} else { } else {
sb.WriteString(formatHeaderEN(ctx)) sb.WriteString(formatHeaderEN(ctx))
} }
// 3. 账户信息 // 3. Account information
if lang == LangChinese { if lang == LangChinese {
sb.WriteString(formatAccountZH(ctx)) sb.WriteString(formatAccountZH(ctx))
} else { } else {
sb.WriteString(formatAccountEN(ctx)) sb.WriteString(formatAccountEN(ctx))
} }
// 4. 历史交易统计 // 4. Historical trading statistics
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 { if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
if lang == LangChinese { if lang == LangChinese {
sb.WriteString(formatTradingStatsZH(ctx.TradingStats)) 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 len(ctx.RecentOrders) > 0 {
if lang == LangChinese { if lang == LangChinese {
sb.WriteString(formatRecentTradesZH(ctx.RecentOrders)) 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 len(ctx.Positions) > 0 {
if lang == LangChinese { if lang == LangChinese {
sb.WriteString(formatCurrentPositionsZH(ctx)) 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 len(ctx.CandidateCoins) > 0 {
if lang == LangChinese { if lang == LangChinese {
sb.WriteString(formatCandidateCoinsZH(ctx)) 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 { if ctx.OIRankingData != nil {
nofxosLang := nofxos.LangEnglish nofxosLang := nofxos.LangEnglish
if lang == LangChinese { if lang == LangChinese {
@@ -100,15 +101,15 @@ func formatContextData(ctx *Context, lang Language) string {
return sb.String() return sb.String()
} }
// ========== 中文格式化函数 ========== // ========== Chinese Formatting Functions ==========
// formatHeaderZH 格式化头部信息(中文) // formatHeaderZH formats header information (Chinese)
func formatHeaderZH(ctx *Context) string { func formatHeaderZH(ctx *Context) string {
return fmt.Sprintf("# 📊 交易决策请求\n\n时间: %s | 周期: #%d | 运行时长: %d 分钟\n\n", return fmt.Sprintf("# 📊 交易决策请求\n\n时间: %s | 周期: #%d | 运行时长: %d 分钟\n\n",
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes) ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
} }
// formatAccountZH 格式化账户信息(中文) // formatAccountZH formats account information (Chinese)
func formatAccountZH(ctx *Context) string { func formatAccountZH(ctx *Context) string {
acc := ctx.Account acc := ctx.Account
var sb strings.Builder var sb strings.Builder
@@ -120,7 +121,7 @@ func formatAccountZH(ctx *Context) string {
sb.WriteString(fmt.Sprintf("保证金使用率: %.1f%% | ", acc.MarginUsedPct)) sb.WriteString(fmt.Sprintf("保证金使用率: %.1f%% | ", acc.MarginUsedPct))
sb.WriteString(fmt.Sprintf("持仓数: %d\n\n", acc.PositionCount)) sb.WriteString(fmt.Sprintf("持仓数: %d\n\n", acc.PositionCount))
// 添加风险提示 // Add risk warnings
if acc.MarginUsedPct > 70 { if acc.MarginUsedPct > 70 {
sb.WriteString("⚠️ **风险警告**: 保证金使用率 > 70%,处于高风险状态!\n\n") sb.WriteString("⚠️ **风险警告**: 保证金使用率 > 70%,处于高风险状态!\n\n")
} else if acc.MarginUsedPct > 50 { } else if acc.MarginUsedPct > 50 {
@@ -130,25 +131,25 @@ func formatAccountZH(ctx *Context) string {
return sb.String() return sb.String()
} }
// formatTradingStatsZH 格式化历史交易统计(中文) // formatTradingStatsZH formats historical trading statistics (Chinese)
func formatTradingStatsZH(stats *TradingStats) string { func formatTradingStatsZH(stats *TradingStats) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## 历史交易统计\n\n") sb.WriteString("## 历史交易统计\n\n")
// 盈亏比计算 // Win/loss ratio calculation
var winLossRatio float64 var winLossRatio float64
if stats.AvgLoss > 0 { if stats.AvgLoss > 0 {
winLossRatio = stats.AvgWin / stats.AvgLoss winLossRatio = stats.AvgWin / stats.AvgLoss
} }
// 指标定义说明(去掉胜率,聚焦核心指标) // Metric definitions (focusing on core metrics, excluding win rate)
sb.WriteString("**指标说明**:\n") sb.WriteString("**指标说明**:\n")
sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利,>1.5为良好,>2为优秀)\n") sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利,>1.5为良好,>2为优秀)\n")
sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好,>2优秀)\n") sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好,>2优秀)\n")
sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀)\n") sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀)\n")
sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n") sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n")
// 数据值 // Data values
sb.WriteString("**当前数据**:\n") sb.WriteString("**当前数据**:\n")
sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades)) sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades))
sb.WriteString(fmt.Sprintf("- 盈利因子: %.2f\n", stats.ProfitFactor)) 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("- 平均亏损: -%.2f USDT\n", stats.AvgLoss))
sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct)) sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct))
// 综合分析和决策建议 // Comprehensive analysis and decision guidance
sb.WriteString("**决策参考**:\n") sb.WriteString("**决策参考**:\n")
// 根据统计数据给出具体建议 // Provide specific recommendations based on statistics
if stats.TotalTrades < 10 { if stats.TotalTrades < 10 {
sb.WriteString("- 样本量较小(<10笔),统计结果参考意义有限\n") sb.WriteString("- 样本量较小(<10笔),统计结果参考意义有限\n")
} }
@@ -191,13 +192,13 @@ func formatTradingStatsZH(stats *TradingStats) string {
return sb.String() return sb.String()
} }
// formatRecentTradesZH 格式化最近交易(中文) // formatRecentTradesZH formats recent trades (Chinese)
func formatRecentTradesZH(orders []RecentOrder) string { func formatRecentTradesZH(orders []RecentOrder) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## 最近完成的交易\n\n") sb.WriteString("## 最近完成的交易\n\n")
for i, order := range orders { for i, order := range orders {
// 判断盈亏 // Determine profit or loss
profitOrLoss := "盈利" profitOrLoss := "盈利"
if order.RealizedPnL < 0 { if order.RealizedPnL < 0 {
profitOrLoss = "亏损" profitOrLoss = "亏损"
@@ -222,13 +223,13 @@ func formatRecentTradesZH(orders []RecentOrder) string {
return sb.String() return sb.String()
} }
// formatCurrentPositionsZH 格式化当前持仓(中文) // formatCurrentPositionsZH formats current positions (Chinese)
func formatCurrentPositionsZH(ctx *Context) string { func formatCurrentPositionsZH(ctx *Context) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## 当前持仓\n\n") sb.WriteString("## 当前持仓\n\n")
for i, pos := range ctx.Positions { for i, pos := range ctx.Positions {
// 计算回撤 // Calculate drawdown
drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct
sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side))) 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("保证金 %.0f USDT | ", pos.MarginUsed))
sb.WriteString(fmt.Sprintf("强平价 %.4f\n", pos.LiquidationPrice)) sb.WriteString(fmt.Sprintf("强平价 %.4f\n", pos.LiquidationPrice))
// 添加分析提示 // Add analysis hints
if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 { if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 {
sb.WriteString(fmt.Sprintf(" ⚠️ **止盈提示**: 当前盈亏从峰值 %.2f%% 回撤到 %.2f%%,回撤幅度 %.2f%%,建议考虑止盈\n", sb.WriteString(fmt.Sprintf(" ⚠️ **止盈提示**: 当前盈亏从峰值 %.2f%% 回撤到 %.2f%%,回撤幅度 %.2f%%,建议考虑止盈\n",
pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100)) pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100))
@@ -252,7 +253,7 @@ func formatCurrentPositionsZH(ctx *Context) string {
sb.WriteString(" ⚠️ **止损提示**: 亏损接近-5%止损线,建议考虑止损\n") sb.WriteString(" ⚠️ **止损提示**: 亏损接近-5%止损线,建议考虑止损\n")
} }
// 显示当前价格(如果有市场数据) // Show current price (if market data available)
if ctx.MarketDataMap != nil { if ctx.MarketDataMap != nil {
if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok { if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok {
sb.WriteString(fmt.Sprintf(" 📈 当前价格: %.4f\n", mdata.CurrentPrice)) sb.WriteString(fmt.Sprintf(" 📈 当前价格: %.4f\n", mdata.CurrentPrice))
@@ -265,7 +266,7 @@ func formatCurrentPositionsZH(ctx *Context) string {
return sb.String() return sb.String()
} }
// formatCandidateCoinsZH 格式化候选币种(中文) // formatCandidateCoinsZH formats candidate coins (Chinese)
func formatCandidateCoinsZH(ctx *Context) string { func formatCandidateCoinsZH(ctx *Context) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## 候选币种\n\n") sb.WriteString("## 候选币种\n\n")
@@ -273,19 +274,19 @@ func formatCandidateCoinsZH(ctx *Context) string {
for i, coin := range ctx.CandidateCoins { for i, coin := range ctx.CandidateCoins {
sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol)) sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol))
// 当前价格 // Current price
if ctx.MarketDataMap != nil { if ctx.MarketDataMap != nil {
if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok { if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok {
sb.WriteString(fmt.Sprintf("当前价格: %.4f\n\n", mdata.CurrentPrice)) sb.WriteString(fmt.Sprintf("当前价格: %.4f\n\n", mdata.CurrentPrice))
// K线数据(多时间框架) // Kline data (multi-timeframe)
if mdata.TimeframeData != nil { if mdata.TimeframeData != nil {
sb.WriteString(formatKlineDataZH(coin.Symbol, mdata.TimeframeData, ctx.Timeframes)) sb.WriteString(formatKlineDataZH(coin.Symbol, mdata.TimeframeData, ctx.Timeframes))
} }
} }
} }
// OI数据(如果有) // OI data (if available)
if ctx.OITopDataMap != nil { if ctx.OITopDataMap != nil {
if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok { if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok {
sb.WriteString(fmt.Sprintf("**持仓量变化**: OI排名 #%d | 变化 %+.2f%% (%+.2fM USDT) | 价格变化 %+.2f%%\n\n", sb.WriteString(fmt.Sprintf("**持仓量变化**: OI排名 #%d | 变化 %+.2f%% (%+.2fM USDT) | 价格变化 %+.2f%%\n\n",
@@ -295,7 +296,7 @@ func formatCandidateCoinsZH(ctx *Context) string {
oiData.PriceDeltaPercent, oiData.PriceDeltaPercent,
)) ))
// OI解读 // OI interpretation
oiChange := "增加" oiChange := "增加"
if oiData.OIDeltaPercent < 0 { if oiData.OIDeltaPercent < 0 {
oiChange = "减少" oiChange = "减少"
@@ -314,7 +315,7 @@ func formatCandidateCoinsZH(ctx *Context) string {
return sb.String() return sb.String()
} }
// formatKlineDataZH 格式化K线数据(中文) // formatKlineDataZH formats kline data (Chinese)
func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string { func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
var sb strings.Builder var sb strings.Builder
@@ -324,7 +325,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD
sb.WriteString("```\n") sb.WriteString("```\n")
sb.WriteString("时间(UTC) 开盘 最高 最低 收盘 成交量\n") sb.WriteString("时间(UTC) 开盘 最高 最低 收盘 成交量\n")
// 只显示最近30根K线 // Only show the latest 30 klines
startIdx := 0 startIdx := 0
if len(data.Klines) > 30 { if len(data.Klines) > 30 {
startIdx = 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 { if len(data.Klines) > 0 {
sb.WriteString(" <- 当前\n") 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 { func getOIInterpretationZH(oiChange, priceChange string) string {
if oiChange == "增加" && priceChange == "上涨" { if oiChange == "增加" && priceChange == "上涨" {
return OIInterpretation.OIUp_PriceUp.ZH 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 { func formatHeaderEN(ctx *Context) string {
return fmt.Sprintf("# 📊 Trading Decision Request\n\nTime: %s | Period: #%d | Runtime: %d minutes\n\n", return fmt.Sprintf("# 📊 Trading Decision Request\n\nTime: %s | Period: #%d | Runtime: %d minutes\n\n",
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes) ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
} }
// formatAccountEN 格式化账户信息(英文) // formatAccountEN formats account information (English)
func formatAccountEN(ctx *Context) string { func formatAccountEN(ctx *Context) string {
acc := ctx.Account acc := ctx.Account
var sb strings.Builder var sb strings.Builder
@@ -399,7 +400,7 @@ func formatAccountEN(ctx *Context) string {
return sb.String() return sb.String()
} }
// formatTradingStatsEN 格式化历史交易统计(英文) // formatTradingStatsEN formats historical trading statistics (English)
func formatTradingStatsEN(stats *TradingStats) string { func formatTradingStatsEN(stats *TradingStats) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## Historical Trading Statistics\n\n") sb.WriteString("## Historical Trading Statistics\n\n")
@@ -460,7 +461,7 @@ func formatTradingStatsEN(stats *TradingStats) string {
return sb.String() return sb.String()
} }
// formatRecentTradesEN 格式化最近交易(英文) // formatRecentTradesEN formats recent trades (English)
func formatRecentTradesEN(orders []RecentOrder) string { func formatRecentTradesEN(orders []RecentOrder) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## Recent Completed Trades\n\n") sb.WriteString("## Recent Completed Trades\n\n")
@@ -490,7 +491,7 @@ func formatRecentTradesEN(orders []RecentOrder) string {
return sb.String() return sb.String()
} }
// formatCurrentPositionsEN 格式化当前持仓(英文) // formatCurrentPositionsEN formats current positions (English)
func formatCurrentPositionsEN(ctx *Context) string { func formatCurrentPositionsEN(ctx *Context) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## Current Positions\n\n") sb.WriteString("## Current Positions\n\n")
@@ -531,7 +532,7 @@ func formatCurrentPositionsEN(ctx *Context) string {
return sb.String() return sb.String()
} }
// formatCandidateCoinsEN 格式化候选币种(英文) // formatCandidateCoinsEN formats candidate coins (English)
func formatCandidateCoinsEN(ctx *Context) string { func formatCandidateCoinsEN(ctx *Context) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## Candidate Coins\n\n") sb.WriteString("## Candidate Coins\n\n")
@@ -576,7 +577,7 @@ func formatCandidateCoinsEN(ctx *Context) string {
return sb.String() return sb.String()
} }
// formatKlineDataEN 格式化K线数据(英文) // formatKlineDataEN formats kline data (English)
func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string { func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
var sb strings.Builder 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 { func getOIInterpretationEN(oiChange, priceChange string) string {
if oiChange == "increase" && priceChange == "up" { if oiChange == "increase" && priceChange == "up" {
return OIInterpretation.OIUp_PriceUp.EN return OIInterpretation.OIUp_PriceUp.EN
+24 -24
View File
@@ -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 { type PromptBuilder struct {
lang Language lang Language
} }
// NewPromptBuilder 创建提示词构建器 // NewPromptBuilder creates a new prompt builder for the given language
func NewPromptBuilder(lang Language) *PromptBuilder { func NewPromptBuilder(lang Language) *PromptBuilder {
return &PromptBuilder{lang: lang} return &PromptBuilder{lang: lang}
} }
// BuildSystemPrompt 构建系统提示词 // BuildSystemPrompt builds the system prompt
func (pb *PromptBuilder) BuildSystemPrompt() string { func (pb *PromptBuilder) BuildSystemPrompt() string {
if pb.lang == LangChinese { if pb.lang == LangChinese {
return pb.buildSystemPromptZH() return pb.buildSystemPromptZH()
@@ -29,19 +29,19 @@ func (pb *PromptBuilder) BuildSystemPrompt() string {
return pb.buildSystemPromptEN() return pb.buildSystemPromptEN()
} }
// BuildUserPrompt 构建用户提示词(包含完整的交易上下文) // BuildUserPrompt builds the user prompt with full trading context
func (pb *PromptBuilder) BuildUserPrompt(ctx *Context) string { func (pb *PromptBuilder) BuildUserPrompt(ctx *Context) string {
// 使用Formatter格式化交易上下文 // Use Formatter to format the trading context
formattedData := FormatContextForAI(ctx, pb.lang) formattedData := FormatContextForAI(ctx, pb.lang)
// 添加决策要求 // Append decision requirements
if pb.lang == LangChinese { if pb.lang == LangChinese {
return formattedData + pb.getDecisionRequirementsZH() return formattedData + pb.getDecisionRequirementsZH()
} }
return formattedData + pb.getDecisionRequirementsEN() return formattedData + pb.getDecisionRequirementsEN()
} }
// ========== 中文提示词 ========== // ========== Chinese Prompts ==========
func (pb *PromptBuilder) buildSystemPromptZH() string { func (pb *PromptBuilder) buildSystemPromptZH() string {
return `你是一个专业的量化交易AI助手,负责分析市场数据并做出交易决策。 return `你是一个专业的量化交易AI助手,负责分析市场数据并做出交易决策。
@@ -176,7 +176,7 @@ func (pb *PromptBuilder) getDecisionRequirementsZH() string {
**请立即输出你的决策(JSON格式)**:` **请立即输出你的决策(JSON格式)**:`
} }
// ========== 英文提示词 ========== // ========== English Prompts ==========
func (pb *PromptBuilder) buildSystemPromptEN() string { func (pb *PromptBuilder) buildSystemPromptEN() string {
return `You are a professional quantitative trading AI assistant responsible for analyzing market data and making trading decisions. 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**:` **Please output your decision (JSON format) immediately**:`
} }
// ========== 辅助函数 ========== // ========== Helper Functions ==========
// FormatDecisionExample 格式化决策示例(用于文档) // FormatDecisionExample formats a decision example (for documentation)
func FormatDecisionExample(lang Language) string { func FormatDecisionExample(lang Language) string {
example := Decision{ example := Decision{
Symbol: "BTCUSDT", Symbol: "BTCUSDT",
@@ -323,32 +323,32 @@ func FormatDecisionExample(lang Language) string {
StopLoss: 42000, StopLoss: 42000,
TakeProfit: 48000, TakeProfit: 48000,
Confidence: 85, Confidence: 85,
Reasoning: "详细的推理过程...", Reasoning: "Detailed reasoning process...",
} }
data, _ := json.MarshalIndent([]Decision{example}, "", " ") data, _ := json.MarshalIndent([]Decision{example}, "", " ")
return string(data) return string(data)
} }
// ValidateDecisionFormat 验证决策格式是否正确 // ValidateDecisionFormat validates that the decision format is correct
func ValidateDecisionFormat(decisions []Decision) error { func ValidateDecisionFormat(decisions []Decision) error {
if len(decisions) == 0 { if len(decisions) == 0 {
return fmt.Errorf("决策列表不能为空") return fmt.Errorf("decision list cannot be empty")
} }
for i, d := range decisions { for i, d := range decisions {
// 必需字段检查 // Required field checks
if d.Symbol == "" { if d.Symbol == "" {
return fmt.Errorf("决策#%d: symbol不能为空", i+1) return fmt.Errorf("decision #%d: symbol cannot be empty", i+1)
} }
if d.Action == "" { if d.Action == "" {
return fmt.Errorf("决策#%d: action不能为空", i+1) return fmt.Errorf("decision #%d: action cannot be empty", i+1)
} }
if d.Reasoning == "" { 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{ validActions := map[string]bool{
"HOLD": true, "HOLD": true,
"PARTIAL_CLOSE": true, "PARTIAL_CLOSE": true,
@@ -358,16 +358,16 @@ func ValidateDecisionFormat(decisions []Decision) error {
"WAIT": true, "WAIT": true,
} }
if !validActions[d.Action] { 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.Action == "OPEN_NEW" {
if d.Leverage == 0 { 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 { 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)
} }
} }
} }
+4 -4
View File
@@ -170,8 +170,8 @@ func TestValidateDecisionFormat(t *testing.T) {
t.Error("Empty decisions should return error") t.Error("Empty decisions should return error")
} }
if !strings.Contains(err.Error(), "不能为空") { if !strings.Contains(err.Error(), "cannot be empty") {
t.Errorf("Error message should mention '不能为空', got: %v", err) 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") t.Error("Invalid action should return error")
} }
if !strings.Contains(err.Error(), "无效的action") { if !strings.Contains(err.Error(), "invalid action") {
t.Errorf("Error should mention '无效的action', got: %v", err) t.Errorf("Error should mention 'invalid action', got: %v", err)
} }
}) })
+39 -39
View File
@@ -1,17 +1,17 @@
package kernel package kernel
// ============================================================================ // ============================================================================
// Trading Data Schema - 交易数据字典 // Trading Data Schema
// ============================================================================ // ============================================================================
// 双语数据字典,支持中文和英文 // Bilingual data dictionary supporting Chinese and English.
// 确保AI能够100%理解数据格式,无论使用哪种语言 // Ensures AI can fully understand data formats regardless of language.
// ============================================================================ // ============================================================================
const ( const (
SchemaVersion = "1.0.0" SchemaVersion = "1.0.0"
) )
// Language 语言类型 // Language represents the language type
type Language string type Language string
const ( const (
@@ -19,20 +19,20 @@ const (
LangEnglish Language = "en-US" LangEnglish Language = "en-US"
) )
// ========== 双语字段定义 ========== // ========== Bilingual Field Definitions ==========
// BilingualFieldDef 双语字段定义 // BilingualFieldDef defines a field with bilingual name, formula, and description
type BilingualFieldDef struct { type BilingualFieldDef struct {
NameZH string // 中文名称 NameZH string // Chinese name
NameEN string // English name NameEN string // English name
Unit string // 单位 Unit string // unit of measurement
FormulaZH string // 中文公式 FormulaZH string // Chinese formula
FormulaEN string // English formula FormulaEN string // English formula
DescZH string // 中文描述 DescZH string // Chinese description
DescEN string // English description DescEN string // English description
} }
// GetName 获取字段名称(根据语言) // GetName returns the field name based on language
func (d BilingualFieldDef) GetName(lang Language) string { func (d BilingualFieldDef) GetName(lang Language) string {
if lang == LangChinese { if lang == LangChinese {
return d.NameZH return d.NameZH
@@ -40,7 +40,7 @@ func (d BilingualFieldDef) GetName(lang Language) string {
return d.NameEN return d.NameEN
} }
// GetFormula 获取公式(根据语言) // GetFormula returns the formula based on language
func (d BilingualFieldDef) GetFormula(lang Language) string { func (d BilingualFieldDef) GetFormula(lang Language) string {
if lang == LangChinese { if lang == LangChinese {
return d.FormulaZH return d.FormulaZH
@@ -48,7 +48,7 @@ func (d BilingualFieldDef) GetFormula(lang Language) string {
return d.FormulaEN return d.FormulaEN
} }
// GetDesc 获取描述(根据语言) // GetDesc returns the description based on language
func (d BilingualFieldDef) GetDesc(lang Language) string { func (d BilingualFieldDef) GetDesc(lang Language) string {
if lang == LangChinese { if lang == LangChinese {
return d.DescZH return d.DescZH
@@ -56,9 +56,9 @@ func (d BilingualFieldDef) GetDesc(lang Language) string {
return d.DescEN return d.DescEN
} }
// ========== 数据字典 ========== // ========== Data Dictionary ==========
// DataDictionary 数据字典:定义所有字段的含义 // DataDictionary defines the meaning of all fields
var DataDictionary = map[string]map[string]BilingualFieldDef{ var DataDictionary = map[string]map[string]BilingualFieldDef{
"AccountMetrics": { "AccountMetrics": {
"Equity": { "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 { type BilingualRuleDef struct {
Value interface{} // 规则值 Value interface{} // rule value
DescZH string // 中文描述 DescZH string // Chinese description
DescEN string // English description DescEN string // English description
ReasonZH string // 中文原因 ReasonZH string // Chinese reason
ReasonEN string // English reason ReasonEN string // English reason
} }
// GetDesc 获取描述(根据语言) // GetDesc returns the description based on language
func (d BilingualRuleDef) GetDesc(lang Language) string { func (d BilingualRuleDef) GetDesc(lang Language) string {
if lang == LangChinese { if lang == LangChinese {
return d.DescZH return d.DescZH
@@ -236,7 +236,7 @@ func (d BilingualRuleDef) GetDesc(lang Language) string {
return d.DescEN return d.DescEN
} }
// GetReason 获取原因(根据语言) // GetReason returns the reason based on language
func (d BilingualRuleDef) GetReason(lang Language) string { func (d BilingualRuleDef) GetReason(lang Language) string {
if lang == LangChinese { if lang == LangChinese {
return d.ReasonZH return d.ReasonZH
@@ -244,9 +244,9 @@ func (d BilingualRuleDef) GetReason(lang Language) string {
return d.ReasonEN return d.ReasonEN
} }
// ========== 交易规则 ========== // ========== Trading Rules ==========
// TradingRules 交易规则定义 // TradingRules defines the trading rules
var TradingRules = struct { var TradingRules = struct {
RiskManagement map[string]BilingualRuleDef RiskManagement map[string]BilingualRuleDef
EntrySignals 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 { type OIInterpretationType struct {
OIUp_PriceUp struct { OIUp_PriceUp struct {
ZH string ZH string
@@ -393,9 +393,9 @@ var OIInterpretation = OIInterpretationType{
}, },
} }
// ========== 常见错误 ========== // ========== Common Mistakes ==========
// CommonMistake 常见错误定义 // CommonMistake defines a common mistake with bilingual fields
type CommonMistake struct { type CommonMistake struct {
ErrorZH string ErrorZH string
ErrorEN 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 { func GetSchemaPrompt(lang Language) string {
if lang == LangChinese { if lang == LangChinese {
return getSchemaPromptZH() return getSchemaPromptZH()
@@ -450,36 +450,36 @@ func GetSchemaPrompt(lang Language) string {
return getSchemaPromptEN() return getSchemaPromptEN()
} }
// getSchemaPromptZH 生成中文Prompt // getSchemaPromptZH generates the Chinese prompt
func getSchemaPromptZH() string { func getSchemaPromptZH() string {
prompt := "# 📖 数据字典与交易规则\n\n" prompt := "# 📖 数据字典与交易规则\n\n"
prompt += "## 📊 字段含义说明\n\n" prompt += "## 📊 字段含义说明\n\n"
// 账户指标 // Account metrics
prompt += "### 账户指标\n" prompt += "### 账户指标\n"
for key, field := range DataDictionary["AccountMetrics"] { for key, field := range DataDictionary["AccountMetrics"] {
prompt += formatFieldDefZH(key, field) prompt += formatFieldDefZH(key, field)
} }
// 交易指标 // Trade metrics
prompt += "\n### 交易指标\n" prompt += "\n### 交易指标\n"
for key, field := range DataDictionary["TradeMetrics"] { for key, field := range DataDictionary["TradeMetrics"] {
prompt += formatFieldDefZH(key, field) prompt += formatFieldDefZH(key, field)
} }
// 持仓指标 // Position metrics
prompt += "\n### 持仓指标\n" prompt += "\n### 持仓指标\n"
for key, field := range DataDictionary["PositionMetrics"] { for key, field := range DataDictionary["PositionMetrics"] {
prompt += formatFieldDefZH(key, field) prompt += formatFieldDefZH(key, field)
} }
// 市场数据 // Market data
prompt += "\n### 市场数据\n" prompt += "\n### 市场数据\n"
for key, field := range DataDictionary["MarketData"] { for key, field := range DataDictionary["MarketData"] {
prompt += formatFieldDefZH(key, field) prompt += formatFieldDefZH(key, field)
} }
// OI解读 // OI interpretation
prompt += "\n## 💹 持仓量(OI)变化解读\n\n" prompt += "\n## 💹 持仓量(OI)变化解读\n\n"
prompt += "- **OI增加 + 价格上涨**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n" prompt += "- **OI增加 + 价格上涨**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n"
prompt += "- **OI增加 + 价格下跌**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n" prompt += "- **OI增加 + 价格下跌**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n"
@@ -489,7 +489,7 @@ func getSchemaPromptZH() string {
return prompt return prompt
} }
// getSchemaPromptEN 生成英文Prompt // getSchemaPromptEN generates the English prompt
func getSchemaPromptEN() string { func getSchemaPromptEN() string {
prompt := "# 📖 Data Dictionary & Trading Rules\n\n" prompt := "# 📖 Data Dictionary & Trading Rules\n\n"
prompt += "## 📊 Field Definitions\n\n" prompt += "## 📊 Field Definitions\n\n"
@@ -528,7 +528,7 @@ func getSchemaPromptEN() string {
return prompt return prompt
} }
// formatFieldDefZH 格式化中文字段定义 // formatFieldDefZH formats a field definition in Chinese
func formatFieldDefZH(key string, field BilingualFieldDef) string { func formatFieldDefZH(key string, field BilingualFieldDef) string {
result := "- **" + key + "**" + field.NameZH + ": " + field.DescZH result := "- **" + key + "**" + field.NameZH + ": " + field.DescZH
if field.FormulaZH != "" { if field.FormulaZH != "" {
@@ -541,7 +541,7 @@ func formatFieldDefZH(key string, field BilingualFieldDef) string {
return result return result
} }
// formatFieldDefEN 格式化英文字段定义 // formatFieldDefEN formats a field definition in English
func formatFieldDefEN(key string, field BilingualFieldDef) string { func formatFieldDefEN(key string, field BilingualFieldDef) string {
result := "- **" + key + "** (" + field.NameEN + "): " + field.DescEN result := "- **" + key + "** (" + field.NameEN + "): " + field.DescEN
if field.FormulaEN != "" { if field.FormulaEN != "" {
+5 -5
View File
@@ -210,11 +210,11 @@ type BoxData struct {
type RegimeLevel string type RegimeLevel string
const ( const (
RegimeLevelNarrow RegimeLevel = "narrow" // 窄幅震荡 RegimeLevelNarrow RegimeLevel = "narrow" // narrow range oscillation
RegimeLevelStandard RegimeLevel = "standard" // 标准震荡 RegimeLevelStandard RegimeLevel = "standard" // standard oscillation
RegimeLevelWide RegimeLevel = "wide" // 宽幅震荡 RegimeLevelWide RegimeLevel = "wide" // wide range oscillation
RegimeLevelVolatile RegimeLevel = "volatile" // 剧烈震荡 RegimeLevelVolatile RegimeLevel = "volatile" // extreme volatility
RegimeLevelTrending RegimeLevel = "trending" // 趋势 RegimeLevelTrending RegimeLevel = "trending" // trending
) )
// BreakoutLevel represents which box level has been broken // BreakoutLevel represents which box level has been broken
+1 -1
View File
@@ -73,7 +73,7 @@ func (c *Client) fetchAI500() ([]CoinData, error) {
return nil, fmt.Errorf("API returned failure status") return nil, fmt.Errorf("API returned failure status")
} }
// 空列表是正常情况,不是错误 // Empty list is a normal condition, not an error
if len(response.Data.Coins) == 0 { if len(response.Data.Coins) == 0 {
log.Printf("️ AI500 returned empty coin list (no coins meet criteria currently)") log.Printf("️ AI500 returned empty coin list (no coins meet criteria currently)")
return []CoinData{}, nil return []CoinData{}, nil
+1 -1
View File
@@ -23,7 +23,7 @@ type NetFlowResponse struct {
Netflows []NetFlowPosition `json:"netflows"` Netflows []NetFlowPosition `json:"netflows"`
Count int `json:"count"` Count int `json:"count"`
Type string `json:"type"` // institution or personal Type string `json:"type"` // institution or personal
Trade string `json:"trade"` // 合约 or 现货 Trade string `json:"trade"` // futures or spot
TimeRange string `json:"time_range"` TimeRange string `json:"time_range"`
RankType string `json:"rank_type"` // top or low RankType string `json:"rank_type"` // top or low
Limit int `json:"limit"` Limit int `json:"limit"`
+6 -6
View File
@@ -1,11 +1,11 @@
#!/bin/sh #!/bin/sh
set -e set -e
# Railway 会设置 PORT 环境变量 # Railway sets the PORT environment variable
export PORT=${PORT:-8080} export PORT=${PORT:-8080}
echo "🚀 Starting NOFX on port $PORT..." echo "🚀 Starting NOFX on port $PORT..."
# 生成加密密钥(如果没有设置) # Generate encryption keys (if not already set)
if [ -z "$RSA_PRIVATE_KEY" ]; then if [ -z "$RSA_PRIVATE_KEY" ]; then
export RSA_PRIVATE_KEY=$(openssl genrsa 2048 2>/dev/null) export RSA_PRIVATE_KEY=$(openssl genrsa 2048 2>/dev/null)
fi fi
@@ -13,7 +13,7 @@ if [ -z "$DATA_ENCRYPTION_KEY" ]; then
export DATA_ENCRYPTION_KEY=$(openssl rand -base64 32) export DATA_ENCRYPTION_KEY=$(openssl rand -base64 32)
fi fi
# 生成 nginx 配置 # Generate nginx config
cat > /etc/nginx/http.d/default.conf << NGINX_EOF cat > /etc/nginx/http.d/default.conf << NGINX_EOF
server { server {
listen $PORT; listen $PORT;
@@ -44,14 +44,14 @@ server {
} }
NGINX_EOF NGINX_EOF
# 启动后端(端口 8081 # Start backend (port 8081)
API_SERVER_PORT=8081 /app/nofx & API_SERVER_PORT=8081 /app/nofx &
sleep 2 sleep 2
# 启动 nginx(后台) # Start nginx (background)
nginx nginx
echo "✅ NOFX started successfully" echo "✅ NOFX started successfully"
# 保持容器运行 # Keep the container running
tail -f /dev/null tail -f /dev/null
+2 -2
View File
@@ -111,11 +111,11 @@ type CoinSourceConfig struct {
UseAI500 bool `json:"use_ai500"` UseAI500 bool `json:"use_ai500"`
// AI500 coin pool maximum count // AI500 coin pool maximum count
AI500Limit int `json:"ai500_limit,omitempty"` 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"` UseOITop bool `json:"use_oi_top"`
// OI Top maximum count // OI Top maximum count
OITopLimit int `json:"oi_top_limit,omitempty"` 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"` UseOILow bool `json:"use_oi_low"`
// OI Low maximum count // OI Low maximum count
OILowLimit int `json:"oi_low_limit,omitempty"` OILowLimit int `json:"oi_low_limit,omitempty"`
+1 -1
View File
@@ -273,7 +273,7 @@ func (a *Agent) Run(userMessage string, onChunk func(string)) string {
// Safety: max iterations reached. // Safety: max iterations reached.
logger.Warnf("Agent: max iterations (%d) reached for message: %q", maxIterations, userMessage) 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("user", userMessage)
a.memory.Add("assistant", reply) a.memory.Add("assistant", reply)
return reply return reply
+1 -1
View File
@@ -45,7 +45,7 @@ func (m *Manager) Run(chatID int64, userMessage string, onChunk func(string)) st
case lane <- struct{}{}: case lane <- struct{}{}:
case <-time.After(60 * time.Second): case <-time.After(60 * time.Second):
logger.Warnf("Agent: lane wait timeout for chat %d — previous message still processing", chatID) 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 }() defer func() { <-lane }()
return a.Run(userMessage, onChunk) return a.Run(userMessage, onChunk)
+2 -2
View File
@@ -63,10 +63,10 @@ func (at *AutoTrader) runCycle() error {
// NOTE: Must be called BEFORE candidate coins check to ensure equity is always recorded // NOTE: Must be called BEFORE candidate coins check to ensure equity is always recorded
at.saveEquitySnapshot(ctx) at.saveEquitySnapshot(ctx)
// 如果没有候选币种,记录但不报错 // If no candidate coins available, log but do not error
if len(ctx.CandidateCoins) == 0 { if len(ctx.CandidateCoins) == 0 {
logger.Infof("️ No candidate coins available, skipping this cycle") 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.ExecutionLog = append(record.ExecutionLog, "No candidate coins available, cycle skipped")
record.AccountState = store.AccountSnapshot{ record.AccountState = store.AccountSnapshot{
TotalBalance: ctx.Account.TotalEquity, TotalBalance: ctx.Account.TotalEquity,
+1 -1
View File
@@ -115,7 +115,7 @@ func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[str
resp, err := t.client.Do(req) resp, err := t.client.Do(req)
if err != nil { 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) return nil, fmt.Errorf("failed to query order status: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -2,6 +2,7 @@ import { motion, AnimatePresence } from 'framer-motion'
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react' import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
import { DeepVoidBackground } from '../common/DeepVoidBackground' import { DeepVoidBackground } from '../common/DeepVoidBackground'
import { useLanguage } from '../../contexts/LanguageContext' import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
interface LoginRequiredOverlayProps { interface LoginRequiredOverlayProps {
isOpen: boolean isOpen: boolean
@@ -12,52 +13,19 @@ interface LoginRequiredOverlayProps {
export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequiredOverlayProps) { export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequiredOverlayProps) {
const { language } = useLanguage() const { language } = useLanguage()
const texts = { const tr = (key: string, params?: Record<string, string | number>) =>
zh: { t(`loginRequired.${key}`, language, params)
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 t = texts[language] const subtitle = featureName
? tr('subtitleWithFeature', { featureName })
: tr('subtitleDefault')
const benefits = [
tr('benefit1'),
tr('benefit2'),
tr('benefit3'),
tr('benefit4'),
]
return ( return (
<AnimatePresence> <AnimatePresence>
@@ -108,7 +76,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div> <div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
<div className="bg-nofx-bg border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]"> <div className="bg-nofx-bg border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
<AlertTriangle size={18} className="animate-pulse" /> <AlertTriangle size={18} className="animate-pulse" />
<span className="font-bold tracking-widest text-sm uppercase">{language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'}</span> <span className="font-bold tracking-widest text-sm uppercase">{tr('accessDenied')}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -116,19 +84,19 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
{/* Terminal Text */} {/* Terminal Text */}
<div className="space-y-4 mb-8"> <div className="space-y-4 mb-8">
<div className="text-center"> <div className="text-center">
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{t.title}</h2> <h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{tr('title')}</h2>
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{t.subtitle}</p> <p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{subtitle}</p>
</div> </div>
<div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4"> <div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4">
<p className="text-xs text-nofx-text-muted leading-relaxed font-mono"> <p className="text-xs text-nofx-text-muted leading-relaxed font-mono">
<span className="text-green-500 mr-2">$</span> <span className="text-green-500 mr-2">$</span>
{t.description} {tr('description')}
</p> </p>
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{t.benefits.map((benefit, i) => ( {benefits.map((benefit, i) => (
<div key={i} className="flex items-center gap-2 text-[10px] text-nofx-text-muted uppercase tracking-wide"> <div key={i} className="flex items-center gap-2 text-[10px] text-nofx-text-muted uppercase tracking-wide">
<span className="text-nofx-gold"></span> {benefit} <span className="text-nofx-gold"></span> {benefit}
</div> </div>
@@ -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" 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"
> >
<LogIn size={14} /> <LogIn size={14} />
<span>{t.login}</span> <span>{tr('loginButton')}</span>
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-&gt;</span> <span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-&gt;</span>
</a> </a>
@@ -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" 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"
> >
<UserPlus size={14} /> <UserPlus size={14} />
<span>{t.register}</span> <span>{tr('registerButton')}</span>
</a> </a>
</div> </div>
@@ -161,7 +129,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
onClick={onClose} onClick={onClose}
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30" className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
> >
[ {t.later} ] [ {tr('abort')} ]
</button> </button>
</div> </div>
@@ -29,6 +29,7 @@ import {
CandlestickChart as CandlestickIcon, CandlestickChart as CandlestickIcon,
} from 'lucide-react' } from 'lucide-react'
import { api } from '../../lib/api' import { api } from '../../lib/api'
import { t, type Language } from '../../i18n/translations'
import type { import type {
BacktestEquityPoint, BacktestEquityPoint,
BacktestTradeEvent, BacktestTradeEvent,
@@ -136,7 +137,7 @@ export function EquityChart({ equity, trades }: EquityChartProps) {
interface CandlestickChartProps { interface CandlestickChartProps {
runId: string runId: string
trades: BacktestTradeEvent[] trades: BacktestTradeEvent[]
language: string language: Language
} }
export function CandlestickChartComponent({ runId, trades, language }: CandlestickChartProps) { export function CandlestickChartComponent({ runId, trades, language }: CandlestickChartProps) {
@@ -289,7 +290,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
if (symbols.length === 0) { if (symbols.length === 0) {
return ( return (
<div className="py-12 text-center" style={{ color: '#5E6673' }}> <div className="py-12 text-center" style={{ color: '#5E6673' }}>
{language === 'zh' ? '没有交易记录' : 'No trades to display'} {t('backtestChart.noTrades', language)}
</div> </div>
) )
} }
@@ -300,7 +301,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CandlestickIcon size={16} style={{ color: '#F0B90B' }} /> <CandlestickIcon size={16} style={{ color: '#F0B90B' }} />
<span className="text-sm" style={{ color: '#848E9C' }}> <span className="text-sm" style={{ color: '#848E9C' }}>
{language === 'zh' ? '币种' : 'Symbol'} {t('backtestChart.symbol', language)}
</span> </span>
<select <select
value={selectedSymbol} value={selectedSymbol}
@@ -319,7 +320,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock size={14} style={{ color: '#848E9C' }} /> <Clock size={14} style={{ color: '#848E9C' }} />
<span className="text-sm" style={{ color: '#848E9C' }}> <span className="text-sm" style={{ color: '#848E9C' }}>
{language === 'zh' ? '周期' : 'Interval'} {t('backtestChart.interval', language)}
</span> </span>
<div className="flex rounded overflow-hidden" style={{ border: '1px solid #2B3139' }}> <div className="flex rounded overflow-hidden" style={{ border: '1px solid #2B3139' }}>
{CHART_TIMEFRAMES.map((tf) => ( {CHART_TIMEFRAMES.map((tf) => (
@@ -339,7 +340,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
</div> </div>
<span className="text-xs" style={{ color: '#5E6673' }}> <span className="text-xs" style={{ color: '#5E6673' }}>
({symbolTrades.length} {language === 'zh' ? '笔交易' : 'trades'}) ({symbolTrades.length} {t('backtestChart.trades', language)})
</span> </span>
</div> </div>
@@ -351,7 +352,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center h-[400px]" style={{ color: '#848E9C' }}> <div className="flex items-center justify-center h-[400px]" style={{ color: '#848E9C' }}>
<RefreshCw className="animate-spin mr-2" size={16} /> <RefreshCw className="animate-spin mr-2" size={16} />
{language === 'zh' ? '加载K线数据...' : 'Loading kline data...'} {t('backtestChart.loadingKline', language)}
</div> </div>
)} )}
{error && ( {error && (
@@ -365,14 +366,14 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
<div className="flex items-center gap-4 text-xs" style={{ color: '#848E9C' }}> <div className="flex items-center gap-4 text-xs" style={{ color: '#848E9C' }}>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-full" style={{ background: '#0ECB81' }} /> <div className="w-2.5 h-2.5 rounded-full" style={{ background: '#0ECB81' }} />
<span>{language === 'zh' ? '开仓/盈利' : 'Open/Profit'}</span> <span>{t('backtestChart.openProfit', language)}</span>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-full" style={{ background: '#F6465D' }} /> <div className="w-2.5 h-2.5 rounded-full" style={{ background: '#F6465D' }} />
<span>{language === 'zh' ? '亏损平仓' : 'Loss Close'}</span> <span>{t('backtestChart.lossClose', language)}</span>
</div> </div>
<span style={{ color: '#5E6673' }}>|</span> <span style={{ color: '#5E6673' }}>|</span>
<span> Long · Short · {language === 'zh' ? '平仓' : 'Close'}</span> <span> Long · Short · {t('backtestChart.close', language)}</span>
</div> </div>
</div> </div>
) )
@@ -384,7 +385,7 @@ interface BacktestChartTabProps {
equity: BacktestEquityPoint[] | undefined equity: BacktestEquityPoint[] | undefined
trades: BacktestTradeEvent[] | undefined trades: BacktestTradeEvent[] | undefined
selectedRunId: string selectedRunId: string
language: string language: Language
tr: (key: string) => string tr: (key: string) => string
} }
@@ -405,7 +406,7 @@ export function BacktestChartTab({
> >
<div> <div>
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}> <h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '资金曲线' : 'Equity Curve'} {t('backtestChart.equityCurve', language)}
</h4> </h4>
{equity && equity.length > 0 ? ( {equity && equity.length > 0 ? (
<EquityChart equity={equity} trades={trades ?? []} /> <EquityChart equity={equity} trades={trades ?? []} />
@@ -419,7 +420,7 @@ export function BacktestChartTab({
{selectedRunId && trades && trades.length > 0 && ( {selectedRunId && trades && trades.length > 0 && (
<div> <div>
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}> <h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
{language === 'zh' ? 'K线图 & 交易标记' : 'Candlestick & Trade Markers'} {t('backtestChart.candlestickTradeMarkers', language)}
</h4> </h4>
<CandlestickChartComponent <CandlestickChartComponent
runId={selectedRunId} runId={selectedRunId}
@@ -7,6 +7,8 @@ import {
Zap, Zap,
} from 'lucide-react' } from 'lucide-react'
import type { AIModel, Strategy } from '../../types' import type { AIModel, Strategy } from '../../types'
import { t as globalT } from '../../i18n/translations'
import type { Language } from '../../i18n/translations'
// ============ Types ============ // ============ Types ============
@@ -104,12 +106,12 @@ export function BacktestConfigForm({
} }
}, [selectedStrategy]) }, [selectedStrategy])
const zh = language === 'zh' const lang = language as Language
const quickRanges = [ const quickRanges = [
{ label: zh ? '24小时' : '24h', hours: 24 }, { label: globalT('backtestConfigForm.quickRange24h', lang), hours: 24 },
{ label: zh ? '3天' : '3d', hours: 72 }, { label: globalT('backtestConfigForm.quickRange3d', lang), hours: 72 },
{ label: zh ? '7天' : '7d', hours: 168 }, { label: globalT('backtestConfigForm.quickRange7d', lang), hours: 168 },
{ label: zh ? '30' : '30d', hours: 720 }, { label: globalT('backtestConfigForm.quickRange30d', lang), hours: 720 },
] ]
const applyQuickRange = (hours: number) => { const applyQuickRange = (hours: number) => {
@@ -144,9 +146,9 @@ export function BacktestConfigForm({
</div> </div>
))} ))}
<span className="ml-2 text-xs" style={{ color: '#848E9C' }}> <span className="ml-2 text-xs" style={{ color: '#848E9C' }}>
{wizardStep === 1 ? (zh ? '选择模型' : 'Select Model') {wizardStep === 1 ? globalT('backtestConfigForm.selectModel', lang)
: wizardStep === 2 ? (zh ? '配置参数' : 'Configure') : wizardStep === 2 ? globalT('backtestConfigForm.configure', lang)
: (zh ? '确认启动' : 'Confirm')} : globalT('backtestConfigForm.confirmStart', lang)}
</span> </span>
</div> </div>
@@ -196,7 +198,7 @@ export function BacktestConfigForm({
{/* Strategy Selection (Optional) */} {/* Strategy Selection (Optional) */}
<div> <div>
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}> <label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
{zh ? '策略配置(可选)' : 'Strategy (Optional)'} {globalT('backtestConfigForm.strategyOptional', lang)}
</label> </label>
<select <select
className="w-full p-3 rounded-lg text-sm" className="w-full p-3 rounded-lg text-sm"
@@ -204,7 +206,7 @@ export function BacktestConfigForm({
value={formState.strategyId} value={formState.strategyId}
onChange={(e) => onFormChange('strategyId', e.target.value)} onChange={(e) => onFormChange('strategyId', e.target.value)}
> >
<option value="">{zh ? '不使用保存的策略' : 'No saved strategy'}</option> <option value="">{globalT('backtestConfigForm.noSavedStrategy', lang)}</option>
{strategies?.map((s) => ( {strategies?.map((s) => (
<option key={s.id} value={s.id}> <option key={s.id} value={s.id}>
{s.name} {s.is_active && '✓'} {s.is_default && '⭐'} {s.name} {s.is_active && '✓'} {s.is_default && '⭐'}
@@ -215,7 +217,7 @@ export function BacktestConfigForm({
<div className="mt-2 p-2 rounded" style={{ background: 'rgba(240,185,11,0.1)', border: '1px solid rgba(240,185,11,0.2)' }}> <div className="mt-2 p-2 rounded" style={{ background: 'rgba(240,185,11,0.1)', border: '1px solid rgba(240,185,11,0.2)' }}>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<span style={{ color: '#F0B90B' }}> <span style={{ color: '#F0B90B' }}>
{zh ? '币种来源:' : 'Coin Source:'} {globalT('backtestConfigForm.coinSource', lang)}
</span> </span>
<span className="font-medium" style={{ color: '#EAECEF' }}> <span className="font-medium" style={{ color: '#EAECEF' }}>
{coinSourceDescription.type} {coinSourceDescription.type}
@@ -225,9 +227,7 @@ export function BacktestConfigForm({
</div> </div>
{strategyHasDynamicCoins && ( {strategyHasDynamicCoins && (
<div className="text-xs mt-1" style={{ color: '#F0B90B' }}> <div className="text-xs mt-1" style={{ color: '#F0B90B' }}>
{zh {globalT('backtestConfigForm.clearDynamicCoins', lang)}
? '⚡ 清空下方币种输入框即可使用策略的动态币种'
: '⚡ Clear the symbols field below to use strategy\'s dynamic coins'}
</div> </div>
)} )}
</div> </div>
@@ -239,7 +239,7 @@ export function BacktestConfigForm({
{tr('form.symbolsLabel')} {tr('form.symbolsLabel')}
{strategyHasDynamicCoins && ( {strategyHasDynamicCoins && (
<span className="ml-2" style={{ color: '#5E6673' }}> <span className="ml-2" style={{ color: '#5E6673' }}>
({zh ? '可选 - 策略已配置币种来源' : 'Optional - strategy has coin source'}) ({globalT('backtestConfigForm.optionalCoinSource', lang)})
</span> </span>
)} )}
</label> </label>
@@ -283,7 +283,7 @@ export function BacktestConfigForm({
onChange={(e) => onFormChange('symbols', e.target.value)} onChange={(e) => onFormChange('symbols', e.target.value)}
rows={2} rows={2}
placeholder={strategyHasDynamicCoins placeholder={strategyHasDynamicCoins
? (zh ? '留空将使用策略配置的币种来源' : 'Leave empty to use strategy coin source') ? globalT('backtestConfigForm.leavEmptyForStrategy', lang)
: '' : ''
} }
/> />
@@ -294,7 +294,7 @@ export function BacktestConfigForm({
className="absolute top-2 right-2 px-2 py-1 rounded text-xs" className="absolute top-2 right-2 px-2 py-1 rounded text-xs"
style={{ background: '#F0B90B', color: '#0B0E11' }} style={{ background: '#F0B90B', color: '#0B0E11' }}
> >
{zh ? '清空使用策略币种' : 'Clear to use strategy'} {globalT('backtestConfigForm.clearToUseStrategy', lang)}
</button> </button>
)} )}
</div> </div>
@@ -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" 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' }} style={{ background: '#F0B90B', color: '#0B0E11' }}
> >
{zh ? '下一步' : 'Next'} {globalT('backtestConfigForm.next', lang)}
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</button> </button>
</motion.div> </motion.div>
@@ -359,7 +359,7 @@ export function BacktestConfigForm({
<div> <div>
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}> <label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
{zh ? '时间周期' : 'Timeframes'} {globalT('backtestConfigForm.timeframes', lang)}
</label> </label>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{TIMEFRAME_OPTIONS.map((tf) => { {TIMEFRAME_OPTIONS.map((tf) => {
@@ -428,7 +428,7 @@ export function BacktestConfigForm({
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
> >
<ChevronLeft className="w-4 h-4" /> <ChevronLeft className="w-4 h-4" />
{zh ? '上一步' : 'Back'} {globalT('backtestConfigForm.back', lang)}
</button> </button>
<button <button
type="button" type="button"
@@ -436,7 +436,7 @@ export function BacktestConfigForm({
className="flex-1 py-2 rounded-lg font-medium flex items-center justify-center gap-2" className="flex-1 py-2 rounded-lg font-medium flex items-center justify-center gap-2"
style={{ background: '#F0B90B', color: '#0B0E11' }} style={{ background: '#F0B90B', color: '#0B0E11' }}
> >
{zh ? '下一步' : 'Next'} {globalT('backtestConfigForm.next', lang)}
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</button> </button>
</div> </div>
@@ -520,7 +520,7 @@ export function BacktestConfigForm({
<div> <div>
<label className="block text-xs mb-1" style={{ color: '#848E9C' }}> <label className="block text-xs mb-1" style={{ color: '#848E9C' }}>
{zh ? '策略风格' : 'Strategy Style'} {globalT('backtestConfigForm.strategyStyle', lang)}
</label> </label>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{['baseline', 'aggressive', 'conservative', 'scalping'].map((p) => ( {['baseline', 'aggressive', 'conservative', 'scalping'].map((p) => (
@@ -570,7 +570,7 @@ export function BacktestConfigForm({
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
> >
<ChevronLeft className="w-4 h-4" /> <ChevronLeft className="w-4 h-4" />
{zh ? '上一步' : 'Back'} {globalT('backtestConfigForm.back', lang)}
</button> </button>
<button <button
type="submit" type="submit"
@@ -7,6 +7,7 @@ import {
ArrowDownRight, ArrowDownRight,
} from 'lucide-react' } from 'lucide-react'
import { MetricTooltip } from '../common/MetricTooltip' import { MetricTooltip } from '../common/MetricTooltip'
import { t, type Language } from '../../i18n/translations'
import { EquityChart } from './BacktestChartTab' import { EquityChart } from './BacktestChartTab'
import type { import type {
BacktestEquityPoint, BacktestEquityPoint,
@@ -131,7 +132,7 @@ export function ProgressRing({ progress, size = 120 }: ProgressRingProps) {
interface PositionsDisplayProps { interface PositionsDisplayProps {
positions: BacktestPositionStatus[] positions: BacktestPositionStatus[]
language: string language: Language
} }
export function PositionsDisplay({ positions, language }: PositionsDisplayProps) { export function PositionsDisplay({ positions, language }: PositionsDisplayProps) {
@@ -151,7 +152,7 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Activity className="w-4 h-4" style={{ color: '#F0B90B' }} /> <Activity className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}> <span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '当前持仓' : 'Active Positions'} {t('backtestOverview.activePositions', language)}
</span> </span>
<span <span
className="px-1.5 py-0.5 rounded text-xs" className="px-1.5 py-0.5 rounded text-xs"
@@ -162,13 +163,13 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
</div> </div>
<div className="flex items-center gap-3 text-xs"> <div className="flex items-center gap-3 text-xs">
<span style={{ color: '#848E9C' }}> <span style={{ color: '#848E9C' }}>
{language === 'zh' ? '保证金' : 'Margin'}: ${totalMargin.toFixed(2)} {t('backtestOverview.margin', language)}: ${totalMargin.toFixed(2)}
</span> </span>
<span <span
className="font-medium" className="font-medium"
style={{ color: totalUnrealizedPnL >= 0 ? '#0ECB81' : '#F6465D' }} style={{ color: totalUnrealizedPnL >= 0 ? '#0ECB81' : '#F6465D' }}
> >
{language === 'zh' ? '浮盈' : 'Unrealized'}: {totalUnrealizedPnL >= 0 ? '+' : ''} {t('backtestOverview.unrealized', language)}: {totalUnrealizedPnL >= 0 ? '+' : ''}
${totalUnrealizedPnL.toFixed(2)} ${totalUnrealizedPnL.toFixed(2)}
</span> </span>
</div> </div>
@@ -214,8 +215,8 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
</span> </span>
</div> </div>
<div className="text-[10px]" style={{ color: '#5E6673' }}> <div className="text-[10px]" style={{ color: '#5E6673' }}>
{language === 'zh' ? '数量' : 'Qty'}: {pos.quantity.toFixed(4)} ·{' '} {t('backtestOverview.qty', language)}: {pos.quantity.toFixed(4)} ·{' '}
{language === 'zh' ? '保证金' : 'Margin'}: ${pos.margin_used.toFixed(2)} {t('backtestOverview.margin', language)}: ${pos.margin_used.toFixed(2)}
</div> </div>
</div> </div>
</div> </div>
@@ -223,10 +224,10 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
<div className="text-right"> <div className="text-right">
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<span style={{ color: '#848E9C' }}> <span style={{ color: '#848E9C' }}>
{language === 'zh' ? '开仓' : 'Entry'}: ${pos.entry_price.toFixed(2)} {t('backtestOverview.entry', language)}: ${pos.entry_price.toFixed(2)}
</span> </span>
<span style={{ color: '#EAECEF' }}> <span style={{ color: '#EAECEF' }}>
{language === 'zh' ? '现价' : 'Mark'}: ${pos.mark_price.toFixed(2)} {t('backtestOverview.mark', language)}: ${pos.mark_price.toFixed(2)}
</span> </span>
</div> </div>
<div className="flex items-center justify-end gap-1.5 mt-0.5"> <div className="flex items-center justify-end gap-1.5 mt-0.5">
@@ -255,7 +256,7 @@ interface BacktestOverviewTabProps {
equity: BacktestEquityPoint[] | undefined equity: BacktestEquityPoint[] | undefined
trades: BacktestTradeEvent[] | undefined trades: BacktestTradeEvent[] | undefined
metrics: BacktestMetrics | undefined metrics: BacktestMetrics | undefined
language: string language: Language
tr: (key: string) => string tr: (key: string) => string
} }
@@ -285,7 +286,7 @@ export function BacktestOverviewTab({
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}> <div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}> <div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '胜率' : 'Win Rate'} {t('backtestOverview.winRate', language)}
<MetricTooltip metricKey="win_rate" language={language} size={11} /> <MetricTooltip metricKey="win_rate" language={language} size={11} />
</div> </div>
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}> <div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
@@ -294,7 +295,7 @@ export function BacktestOverviewTab({
</div> </div>
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}> <div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}> <div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '盈亏因子' : 'Profit Factor'} {t('backtestOverview.profitFactor', language)}
<MetricTooltip metricKey="profit_factor" language={language} size={11} /> <MetricTooltip metricKey="profit_factor" language={language} size={11} />
</div> </div>
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}> <div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
@@ -303,7 +304,7 @@ export function BacktestOverviewTab({
</div> </div>
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}> <div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '总交易数' : 'Total Trades'} {t('backtestOverview.totalTrades', language)}
</div> </div>
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}> <div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
{metrics.trades ?? 0} {metrics.trades ?? 0}
@@ -311,7 +312,7 @@ export function BacktestOverviewTab({
</div> </div>
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}> <div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '最佳币种' : 'Best Symbol'} {t('backtestOverview.bestSymbol', language)}
</div> </div>
<div className="text-lg font-bold" style={{ color: '#0ECB81' }}> <div className="text-lg font-bold" style={{ color: '#0ECB81' }}>
{metrics.best_symbol?.replace('USDT', '') || '-'} {metrics.best_symbol?.replace('USDT', '') || '-'}
+12 -20
View File
@@ -244,9 +244,9 @@ export function BacktestPage() {
const handleDelete = async () => { const handleDelete = async () => {
if (!selectedRunId) return if (!selectedRunId) return
const confirmed = await confirmToast(tr('toasts.confirmDelete', { id: selectedRunId }), { const confirmed = await confirmToast(tr('toasts.confirmDelete', { id: selectedRunId }), {
title: language === 'zh' ? '确认删除' : 'Confirm Delete', title: t('backtestPageExtra.confirmDelete', language),
okText: language === 'zh' ? '删除' : 'Delete', okText: t('backtestPageExtra.delete', language),
cancelText: language === 'zh' ? '取消' : 'Cancel', cancelText: t('backtestPageExtra.cancel', language),
}) })
if (!confirmed) return if (!confirmed) return
try { try {
@@ -328,7 +328,7 @@ export function BacktestPage() {
style={{ background: '#F0B90B', color: '#0B0E11' }} style={{ background: '#F0B90B', color: '#0B0E11' }}
> >
<Play className="w-4 h-4" /> <Play className="w-4 h-4" />
{language === 'zh' ? '新建回测' : 'New Backtest'} {t('backtestPageExtra.newBacktest', language)}
</button> </button>
</div> </div>
@@ -474,14 +474,14 @@ export function BacktestPage() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-3"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard <StatCard
icon={Target} icon={Target}
label={language === 'zh' ? '当前净值' : 'Equity'} label={t('backtestPageExtra.equity', language)}
value={(status?.equity ?? 0).toFixed(2)} value={(status?.equity ?? 0).toFixed(2)}
suffix="USDT" suffix="USDT"
language={language} language={language}
/> />
<StatCard <StatCard
icon={TrendingUp} icon={TrendingUp}
label={language === 'zh' ? '总收益率' : 'Return'} label={t('backtestPageExtra.totalReturn', language)}
value={`${(metrics?.total_return_pct ?? 0).toFixed(2)}%`} value={`${(metrics?.total_return_pct ?? 0).toFixed(2)}%`}
trend={(metrics?.total_return_pct ?? 0) >= 0 ? 'up' : 'down'} trend={(metrics?.total_return_pct ?? 0) >= 0 ? 'up' : 'down'}
color={(metrics?.total_return_pct ?? 0) >= 0 ? '#0ECB81' : '#F6465D'} color={(metrics?.total_return_pct ?? 0) >= 0 ? '#0ECB81' : '#F6465D'}
@@ -490,7 +490,7 @@ export function BacktestPage() {
/> />
<StatCard <StatCard
icon={AlertTriangle} icon={AlertTriangle}
label={language === 'zh' ? '最大回撤' : 'Max DD'} label={t('backtestPageExtra.maxDD', language)}
value={`${(metrics?.max_drawdown_pct ?? 0).toFixed(2)}%`} value={`${(metrics?.max_drawdown_pct ?? 0).toFixed(2)}%`}
color="#F6465D" color="#F6465D"
metricKey="max_drawdown" metricKey="max_drawdown"
@@ -498,7 +498,7 @@ export function BacktestPage() {
/> />
<StatCard <StatCard
icon={BarChart3} icon={BarChart3}
label={language === 'zh' ? '夏普比率' : 'Sharpe'} label={t('backtestPageExtra.sharpe', language)}
value={(metrics?.sharpe_ratio ?? 0).toFixed(2)} value={(metrics?.sharpe_ratio ?? 0).toFixed(2)}
metricKey="sharpe_ratio" metricKey="sharpe_ratio"
language={language} language={language}
@@ -516,20 +516,12 @@ export function BacktestPage() {
style={{ color: viewTab === tab ? '#F0B90B' : '#848E9C' }} style={{ color: viewTab === tab ? '#F0B90B' : '#848E9C' }}
> >
{tab === 'overview' {tab === 'overview'
? language === 'zh' ? t('backtestPageExtra.tabOverview', language)
? '概览'
: 'Overview'
: tab === 'chart' : tab === 'chart'
? language === 'zh' ? t('backtestPageExtra.tabChart', language)
? '图表'
: 'Chart'
: tab === 'trades' : tab === 'trades'
? language === 'zh' ? t('backtestPageExtra.tabTrades', language)
? '交易' : t('backtestPageExtra.tabDecisions', language)}
: 'Trades'
: language === 'zh'
? 'AI决策'
: 'Decisions'}
{viewTab === tab && ( {viewTab === tab && (
<motion.div <motion.div
layoutId="tab-indicator" layoutId="tab-indicator"
@@ -7,6 +7,7 @@ import {
Layers, Layers,
Eye, Eye,
} from 'lucide-react' } from 'lucide-react'
import { t, type Language } from '../../i18n/translations'
// ============ Types ============ // ============ Types ============
@@ -61,7 +62,7 @@ interface BacktestRunListProps {
runs: BacktestRunItem[] runs: BacktestRunItem[]
selectedRunId: string | undefined selectedRunId: string | undefined
compareRunIds: string[] compareRunIds: string[]
language: string language: Language
tr: (key: string, params?: Record<string, string | number>) => string tr: (key: string, params?: Record<string, string | number>) => string
onSelectRun: (runId: string) => void onSelectRun: (runId: string) => void
onToggleCompare: (runId: string) => void onToggleCompare: (runId: string) => void
@@ -84,7 +85,7 @@ export function BacktestRunList({
{tr('runList.title')} {tr('runList.title')}
</h3> </h3>
<span className="text-xs" style={{ color: '#848E9C' }}> <span className="text-xs" style={{ color: '#848E9C' }}>
{runs.length} {language === 'zh' ? '条' : 'runs'} {runs.length} {t('backtestPageExtra.runs', language)}
</span> </span>
</div> </div>
@@ -131,7 +132,7 @@ export function BacktestRunList({
? 'rgba(240,185,11,0.2)' ? 'rgba(240,185,11,0.2)'
: 'transparent', : 'transparent',
}} }}
title={language === 'zh' ? '添加到对比' : 'Add to compare'} title={t('backtestPageExtra.addToCompare', language)}
> >
<Eye <Eye
className="w-3 h-3" className="w-3 h-3"
+115 -114
View File
@@ -12,6 +12,7 @@ import {
} from 'lightweight-charts' } from 'lightweight-charts'
import { useLanguage } from '../../contexts/LanguageContext' import { useLanguage } from '../../contexts/LanguageContext'
import { httpClient } from '../../lib/httpClient' import { httpClient } from '../../lib/httpClient'
import { t } from '../../i18n/translations'
import { import {
calculateSMA, calculateSMA,
calculateEMA, calculateEMA,
@@ -20,26 +21,26 @@ import {
} from '../../utils/indicators' } from '../../utils/indicators'
import { Settings, BarChart2 } from 'lucide-react' import { Settings, BarChart2 } from 'lucide-react'
// 订单接口定义 // Order marker interface
interface OrderMarker { interface OrderMarker {
time: number time: number
price: number price: number
side: 'long' | 'short' side: 'long' | 'short'
rawSide: string // 原始 side 字段 (buy/sell from database) rawSide: string // Original side field (buy/sell from database)
action: 'open' | 'close' action: 'open' | 'close'
pnl?: number pnl?: number
symbol: string symbol: string
} }
// 挂单接口定义 (交易所的止盈止损订单) // Open orders interface (exchange TP/SL orders)
interface OpenOrder { interface OpenOrder {
order_id: string order_id: string
symbol: string symbol: string
side: string // BUY/SELL side: string // BUY/SELL
position_side: string // LONG/SHORT position_side: string // LONG/SHORT
type: string // LIMIT/STOP_MARKET/TAKE_PROFIT_MARKET type: string // LIMIT/STOP_MARKET/TAKE_PROFIT_MARKET
price: number // 限价单价格 price: number // Limit order price
stop_price: number // 触发价格 (止损/止盈) stop_price: number // Trigger price (SL/TP)
quantity: number quantity: number
status: string status: string
} }
@@ -49,11 +50,11 @@ interface AdvancedChartProps {
interval?: string interval?: string
traderID?: string traderID?: string
height?: number height?: number
exchange?: string // 交易所类型:binance, bybit, okx, bitget, hyperliquid, aster, lighter exchange?: string // Exchange type: binance, bybit, okx, bitget, hyperliquid, aster, lighter
onSymbolChange?: (symbol: string) => void // 币种切换回调 onSymbolChange?: (symbol: string) => void // Symbol change callback
} }
// 指标配置 // Indicator configuration
interface IndicatorConfig { interface IndicatorConfig {
id: string id: string
name: string name: string
@@ -62,31 +63,31 @@ interface IndicatorConfig {
params?: any params?: any
} }
// 获取成交额货币单位 // Get quote currency unit
const getQuoteUnit = (exchange: string): string => { const getQuoteUnit = (exchange: string): string => {
if (['alpaca'].includes(exchange)) { if (['alpaca'].includes(exchange)) {
return 'USD' return 'USD'
} }
if (['forex', 'metals'].includes(exchange)) { if (['forex', 'metals'].includes(exchange)) {
return '' // 外汇/贵金属没有真实成交量 return '' // Forex/metals have no real volume
} }
return 'USDT' // 加密货币默认 USDT return 'USDT' // Crypto defaults to USDT
} }
// 获取成交量数量单位 // Get base volume unit
const getBaseUnit = (exchange: string, symbol: string): string => { const getBaseUnit = (exchange: string, symbol: string, language: string): string => {
if (['alpaca'].includes(exchange)) { if (['alpaca'].includes(exchange)) {
return '股' return t('advancedChart.shares', language as 'en' | 'zh' | 'id')
} }
if (['forex', 'metals'].includes(exchange)) { if (['forex', 'metals'].includes(exchange)) {
return '' return ''
} }
// 加密货币:从 symbol 提取基础资产 // Crypto: extract base asset from symbol
const base = symbol.replace(/USDT$|USD$|BUSD$/, '') 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 => { const formatVolume = (value: number): string => {
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B' if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B'
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M' if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M'
@@ -99,43 +100,43 @@ export function AdvancedChart({
interval = '5m', interval = '5m',
traderID, traderID,
height = 550, height = 550,
exchange = 'binance', // 默认使用 binance exchange = 'binance', // Default to binance
onSymbolChange: _onSymbolChange, // Available for future use onSymbolChange: _onSymbolChange, // Available for future use
}: AdvancedChartProps) { }: AdvancedChartProps) {
void _onSymbolChange // Prevent unused warning void _onSymbolChange // Prevent unused warning
const { language } = useLanguage() const { language } = useLanguage()
const quoteUnit = getQuoteUnit(exchange) const quoteUnit = getQuoteUnit(exchange)
const baseUnit = getBaseUnit(exchange, symbol) const baseUnit = getBaseUnit(exchange, symbol, language)
const chartContainerRef = useRef<HTMLDivElement>(null) const chartContainerRef = useRef<HTMLDivElement>(null)
const chartRef = useRef<IChartApi | null>(null) const chartRef = useRef<IChartApi | null>(null)
const candlestickSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null) const candlestickSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null)
const volumeSeriesRef = useRef<ISeriesApi<'Histogram'> | null>(null) const volumeSeriesRef = useRef<ISeriesApi<'Histogram'> | null>(null)
const indicatorSeriesRef = useRef<Map<string, ISeriesApi<any>>>(new Map()) const indicatorSeriesRef = useRef<Map<string, ISeriesApi<any>>>(new Map())
const seriesMarkersRef = useRef<any>(null) // Markers primitive for v5 const seriesMarkersRef = useRef<any>(null) // Markers primitive for v5
const currentMarkersDataRef = useRef<any[]>([]) // 存储当前的标记数据 const currentMarkersDataRef = useRef<any[]>([]) // Store current marker data
const klineDataRef = useRef<Map<number, { volume: number; quoteVolume: number }>>(new Map()) // 存储 kline 额外数据 const klineDataRef = useRef<Map<number, { volume: number; quoteVolume: number }>>(new Map()) // Store kline extra data
const priceLinesRef = useRef<any[]>([]) // 存储挂单价格线 const priceLinesRef = useRef<any[]>([]) // Store open order price lines
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [showIndicatorPanel, setShowIndicatorPanel] = useState(false) const [showIndicatorPanel, setShowIndicatorPanel] = useState(false)
const [showOrderMarkers, setShowOrderMarkers] = useState(true) // 订单标记显示开关,默认显示 const [showOrderMarkers, setShowOrderMarkers] = useState(true) // Order marker toggle, default on
const isInitialLoadRef = useRef(true) // 跟踪是否为初始加载 const isInitialLoadRef = useRef(true) // Track if this is initial load
const [tooltipData, setTooltipData] = useState<any>(null) const [tooltipData, setTooltipData] = useState<any>(null)
const tooltipRef = useRef<HTMLDivElement>(null) const tooltipRef = useRef<HTMLDivElement>(null)
// 行情统计数据(当前K线) // Market stats (current candle)
const [marketStats, setMarketStats] = useState<{ const [marketStats, setMarketStats] = useState<{
price: number price: number
priceChange: number priceChange: number
priceChangePercent: number priceChangePercent: number
high: number high: number
low: number low: number
volume: number // 数量(BTC/股数) volume: number // Quantity (BTC/shares)
quoteVolume: number // 成交额(USDT/USD quoteVolume: number // Turnover (USDT/USD)
} | null>(null) } | null>(null)
// 指标配置 // Indicator configuration
const [indicators, setIndicators] = useState<IndicatorConfig[]>([ const [indicators, setIndicators] = useState<IndicatorConfig[]>([
{ id: 'volume', name: 'Volume', enabled: true, color: '#3B82F6' }, { id: 'volume', name: 'Volume', enabled: true, color: '#3B82F6' },
{ id: 'ma5', name: 'MA5', enabled: false, color: '#FF6B6B', params: { period: 5 } }, { 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' }, { id: 'bb', name: 'Bollinger Bands', enabled: false, color: '#9B59B6' },
]) ])
// 从服务获取K线数据 // Fetch kline data from service
const fetchKlineData = async (symbol: string, interval: string) => { const fetchKlineData = async (symbol: string, interval: string) => {
try { try {
const limit = 1500 const limit = 1500
@@ -158,18 +159,18 @@ export function AdvancedChart({
throw new Error('Failed to fetch kline data') throw new Error('Failed to fetch kline data')
} }
// 转换数据格式 // Convert data format
const rawData = result.data.map((candle: any) => ({ const rawData = result.data.map((candle: any) => ({
time: Math.floor(candle.openTime / 1000) as UTCTimestamp, time: Math.floor(candle.openTime / 1000) as UTCTimestamp,
open: candle.open, open: candle.open,
high: candle.high, high: candle.high,
low: candle.low, low: candle.low,
close: candle.close, close: candle.close,
volume: candle.volume, // 数量(BTC/股数) volume: candle.volume, // Quantity (BTC/shares)
quoteVolume: candle.quoteVolume, // 成交额(USDT/USD 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 sortedData = rawData.sort((a: any, b: any) => a.time - b.time)
const dedupedData = sortedData.filter((item: any, index: number, arr: any[]) => const dedupedData = sortedData.filter((item: any, index: number, arr: any[]) =>
index === 0 || item.time !== arr[index - 1].time 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 => { const parseCustomTime = (time: any): number => {
if (!time) { if (!time) {
console.warn('[AdvancedChart] Empty time value') console.warn('[AdvancedChart] Empty time value')
return 0 return 0
} }
// 如果已经是数字(Unix 时间戳) // If already a number (Unix timestamp)
if (typeof time === 'number') { if (typeof time === 'number') {
// 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳) // Determine ms vs seconds: if > 10^12, treat as milliseconds
if (time > 1000000000000) { if (time > 1000000000000) {
const seconds = Math.floor(time / 1000) const seconds = Math.floor(time / 1000)
console.log('[AdvancedChart] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')') console.log('[AdvancedChart] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
@@ -208,7 +209,7 @@ export function AdvancedChart({
const timeStr = String(time) const timeStr = String(time)
console.log('[AdvancedChart] Parsing time string:', timeStr) console.log('[AdvancedChart] Parsing time string:', timeStr)
// 尝试标准ISO格式 // Try standard ISO format
const isoTime = new Date(timeStr).getTime() const isoTime = new Date(timeStr).getTime()
if (!isNaN(isoTime) && isoTime > 0) { if (!isNaN(isoTime) && isoTime > 0) {
const timestamp = Math.floor(isoTime / 1000) const timestamp = Math.floor(isoTime / 1000)
@@ -216,7 +217,7 @@ export function AdvancedChart({
return timestamp 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/) const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
if (match) { if (match) {
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
@@ -237,11 +238,11 @@ export function AdvancedChart({
return 0 return 0
} }
// 获取订单数据 // Fetch order data
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => { const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
try { try {
console.log('[AdvancedChart] Fetching orders for trader:', traderID, 'symbol:', symbol) 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`) const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=200`)
console.log('[AdvancedChart] Orders API response:', result) console.log('[AdvancedChart] Orders API response:', result)
@@ -258,14 +259,14 @@ export function AdvancedChart({
orders.forEach((order: any) => { orders.forEach((order: any) => {
console.log('[AdvancedChart] Processing order:', order) console.log('[AdvancedChart] Processing order:', order)
// 处理字段名:支持PascalCasesnake_case // Handle field names: support PascalCase and snake_case
const filledAt = order.filled_at || order.FilledAt || order.created_at || order.CreatedAt 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 avgPrice = order.avg_fill_price || order.AvgFillPrice || order.price || order.Price
const orderAction = order.order_action || order.OrderAction const orderAction = order.order_action || order.OrderAction
const side = (order.side || order.Side)?.toLowerCase() // BUY/SELL const side = (order.side || order.Side)?.toLowerCase() // BUY/SELL
const symbol = order.symbol || order.Symbol const symbol = order.symbol || order.Symbol
// 跳过没有成交时间或价格的订单 // Skip orders without fill time or price
if (!filledAt || !avgPrice || avgPrice === 0) { if (!filledAt || !avgPrice || avgPrice === 0) {
console.warn('[AdvancedChart] Skipping order - missing data:', { filledAt, avgPrice }) console.warn('[AdvancedChart] Skipping order - missing data:', { filledAt, avgPrice })
return return
@@ -277,7 +278,7 @@ export function AdvancedChart({
return return
} }
// 根据 order_action 判断是开仓还是平仓 // Determine open/close from order_action
let action: 'open' | 'close' = 'open' let action: 'open' | 'close' = 'open'
let positionSide: 'long' | 'short' = 'long' let positionSide: 'long' | 'short' = 'long'
@@ -290,7 +291,7 @@ export function AdvancedChart({
positionSide = orderAction.includes('LONG') ? 'long' : 'short' positionSide = orderAction.includes('LONG') ? 'long' : 'short'
} }
} else { } else {
// 如果没有 order_action,根据 side 判断 // If no order_action, infer from side
positionSide = side === 'buy' ? 'long' : 'short' positionSide = side === 'buy' ? 'long' : 'short'
} }
@@ -307,7 +308,7 @@ export function AdvancedChart({
time: timeSeconds, time: timeSeconds,
price: avgPrice, price: avgPrice,
side: positionSide, side: positionSide,
rawSide: side, // 原始 side 字段 (buy/sell) rawSide: side, // Original side field (buy/sell)
action: action, action: action,
symbol, symbol,
}) })
@@ -321,7 +322,7 @@ export function AdvancedChart({
} }
} }
// 获取交易所挂单 (止盈止损订单) // Fetch exchange open orders (TP/SL)
const fetchOpenOrders = async (traderID: string, symbol: string): Promise<OpenOrder[]> => { const fetchOpenOrders = async (traderID: string, symbol: string): Promise<OpenOrder[]> => {
try { try {
console.log('[AdvancedChart] Fetching open orders for trader:', traderID, 'symbol:', symbol) console.log('[AdvancedChart] Fetching open orders for trader:', traderID, 'symbol:', symbol)
@@ -341,7 +342,7 @@ export function AdvancedChart({
} }
} }
// 初始化图表 // Initialize chart
useEffect(() => { useEffect(() => {
if (!chartContainerRef.current) return if (!chartContainerRef.current) return
@@ -424,7 +425,7 @@ export function AdvancedChart({
chartRef.current = chart chartRef.current = chart
// 创建K线系列 // Create candlestick series
const candlestickSeries = chart.addSeries(CandlestickSeries, { const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#0ECB81', upColor: '#0ECB81',
downColor: '#F6465D', downColor: '#F6465D',
@@ -435,7 +436,7 @@ export function AdvancedChart({
}) })
candlestickSeriesRef.current = candlestickSeries as any candlestickSeriesRef.current = candlestickSeries as any
// 创建成交量系列 // Create volume series
const volumeSeries = chart.addSeries(HistogramSeries, { const volumeSeries = chart.addSeries(HistogramSeries, {
color: '#26a69a', color: '#26a69a',
priceFormat: { priceFormat: {
@@ -447,7 +448,7 @@ export function AdvancedChart({
}) })
volumeSeriesRef.current = volumeSeries as any volumeSeriesRef.current = volumeSeries as any
// 响应式调整 (ResizeObserver) // Responsive resize (ResizeObserver)
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
if (entries.length === 0 || !entries[0].contentRect) return if (entries.length === 0 || !entries[0].contentRect) return
const { width, height } = entries[0].contentRect const { width, height } = entries[0].contentRect
@@ -458,7 +459,7 @@ export function AdvancedChart({
resizeObserver.observe(chartContainerRef.current) resizeObserver.observe(chartContainerRef.current)
} }
// 监听鼠标移动,显示 OHLC 信息 // Listen for crosshair movement to show OHLC info
chart.subscribeCrosshairMove((param) => { chart.subscribeCrosshairMove((param) => {
if (!param.time || !param.point || !candlestickSeriesRef.current) { if (!param.time || !param.point || !candlestickSeriesRef.current) {
setTooltipData(null) setTooltipData(null)
@@ -473,7 +474,7 @@ export function AdvancedChart({
const candleData = data as any 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 } const klineExtra = klineDataRef.current.get(param.time as number) || { volume: 0, quoteVolume: 0 }
setTooltipData({ setTooltipData({
@@ -496,18 +497,18 @@ export function AdvancedChart({
}, []) // Chart is created once, ResizeObserver handles dimension changes }, []) // Chart is created once, ResizeObserver handles dimension changes
// 加载数据和指标 // Load data and indicators
useEffect(() => { useEffect(() => {
// symbolinterval 改变时,重置初始加载标志(以便自动适配新数据) // Reset initial load flag when symbol/interval changes (for auto-fit)
isInitialLoadRef.current = true isInitialLoadRef.current = true
// 清除旧的标记数据,避免旧数据影响新图表 // Clear old marker data to prevent stale data in new chart
currentMarkersDataRef.current = [] currentMarkersDataRef.current = []
if (seriesMarkersRef.current) { if (seriesMarkersRef.current) {
try { try {
seriesMarkersRef.current.setMarkers([]) seriesMarkersRef.current.setMarkers([])
} catch (e) { } catch (e) {
// 忽略错误,稍后会重新创建 // Ignore errors, will be recreated later
} }
seriesMarkersRef.current = null seriesMarkersRef.current = null
} }
@@ -516,30 +517,30 @@ export function AdvancedChart({
if (!candlestickSeriesRef.current) return if (!candlestickSeriesRef.current) return
console.log('[AdvancedChart] Loading data for', symbol, interval, isRefresh ? '(refresh)' : '') console.log('[AdvancedChart] Loading data for', symbol, interval, isRefresh ? '(refresh)' : '')
// 只在首次加载时显示 loading,刷新时不显示避免闪烁 // Only show loading on first load, avoid flicker on refresh
if (!isRefresh) { if (!isRefresh) {
setLoading(true) setLoading(true)
} }
setError(null) setError(null)
try { try {
// 1. 获取K线数据 // 1. Fetch kline data
const klineData = await fetchKlineData(symbol, interval) const klineData = await fetchKlineData(symbol, interval)
console.log('[AdvancedChart] Loaded', klineData.length, 'klines') console.log('[AdvancedChart] Loaded', klineData.length, 'klines')
candlestickSeriesRef.current.setData(klineData) candlestickSeriesRef.current.setData(klineData)
// 存储 volume/quoteVolume 数据供 tooltip 使用 // Store volume/quoteVolume data for tooltip
klineDataRef.current.clear() klineDataRef.current.clear()
klineData.forEach((k: any) => { klineData.forEach((k: any) => {
klineDataRef.current.set(k.time, { volume: k.volume || 0, quoteVolume: k.quoteVolume || 0 }) klineDataRef.current.set(k.time, { volume: k.volume || 0, quoteVolume: k.quoteVolume || 0 })
}) })
// 1.5 计算行情统计数据 // 1.5 Calculate market stats
if (klineData.length > 1) { if (klineData.length > 1) {
const latestKline = klineData[klineData.length - 1] const latestKline = klineData[klineData.length - 1]
const prevKline = klineData[klineData.length - 2] 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 priceChange = latestKline.close - prevKline.close
const priceChangePercent = (priceChange / prevKline.close) * 100 const priceChangePercent = (priceChange / prevKline.close) * 100
@@ -565,7 +566,7 @@ export function AdvancedChart({
}) })
} }
// 2. 显示成交量 // 2. Display volume
if (volumeSeriesRef.current) { if (volumeSeriesRef.current) {
const volumeEnabled = indicators.find(i => i.id === 'volume')?.enabled const volumeEnabled = indicators.find(i => i.id === 'volume')?.enabled
if (volumeEnabled) { if (volumeEnabled) {
@@ -576,15 +577,15 @@ export function AdvancedChart({
})) }))
volumeSeriesRef.current.setData(volumeData) volumeSeriesRef.current.setData(volumeData)
} else { } else {
// 关闭成交量时清空数据 // Clear data when volume is disabled
volumeSeriesRef.current.setData([]) volumeSeriesRef.current.setData([])
} }
} }
// 3. 添加指标 // 3. Add indicators
updateIndicators(klineData) updateIndicators(klineData)
// 4. 获取并显示订单标记 // 4. Fetch and display order markers
if (traderID && candlestickSeriesRef.current) { if (traderID && candlestickSeriesRef.current) {
console.log('[AdvancedChart] Starting to fetch orders...') console.log('[AdvancedChart] Starting to fetch orders...')
const orders = await fetchOrders(traderID, symbol) const orders = await fetchOrders(traderID, symbol)
@@ -593,17 +594,17 @@ export function AdvancedChart({
if (orders.length > 0) { if (orders.length > 0) {
console.log('[AdvancedChart] Creating markers from', orders.length, 'orders') 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 klineTimes = klineData.map((k: any) => k.time as number)
const klineMinTime = klineTimes[0] || 0 const klineMinTime = klineTimes[0] || 0
const klineMaxTime = klineTimes[klineTimes.length - 1] || 0 const klineMaxTime = klineTimes[klineTimes.length - 1] || 0
console.log('[AdvancedChart] Kline time range:', klineMinTime, '-', klineMaxTime, '(', klineTimes.length, 'candles)') console.log('[AdvancedChart] Kline time range:', klineMinTime, '-', klineMaxTime, '(', klineTimes.length, 'candles)')
// 二分查找:找到订单时间所属的 K 线蜡烛 // Binary search: find the kline candle for the order time
// 返回 time <= orderTime 的最大 K 线时间 // Return the largest kline time <= orderTime
const findCandleTime = (orderTime: number): number | null => { const findCandleTime = (orderTime: number): number | null => {
if (orderTime < klineMinTime || orderTime > klineMaxTime) { if (orderTime < klineMinTime || orderTime > klineMaxTime) {
return null // 超出范围 return null // Out of range
} }
let left = 0 let left = 0
@@ -621,11 +622,11 @@ export function AdvancedChart({
return klineTimes[left] return klineTimes[left]
} }
// 按 K 线时间分组统计订单 // Group orders by kline time
const ordersByCandle = new Map<number, { buys: number; sells: number }>() const ordersByCandle = new Map<number, { buys: number; sells: number }>()
orders.forEach(order => { orders.forEach(order => {
// 使用二分查找找到对应的 K 线蜡烛时间 // Use binary search to find matching kline candle time
const candleTime = findCandleTime(order.time) const candleTime = findCandleTime(order.time)
if (candleTime === null) { if (candleTime === null) {
@@ -643,7 +644,7 @@ export function AdvancedChart({
ordersByCandle.set(candleTime, existing) ordersByCandle.set(candleTime, existing)
}) })
// 为每个有订单的 K 线创建标记 // Create markers for each kline with orders
const markers: Array<{ const markers: Array<{
time: Time time: Time
position: 'belowBar' | 'aboveBar' position: 'belowBar' | 'aboveBar'
@@ -654,7 +655,7 @@ export function AdvancedChart({
}> = [] }> = []
ordersByCandle.forEach((counts, candleTime) => { ordersByCandle.forEach((counts, candleTime) => {
// 显示买入标记(绿色,在K线下方) // Show buy markers (green, below bar)
if (counts.buys > 0) { if (counts.buys > 0) {
markers.push({ markers.push({
time: candleTime as Time, time: candleTime as Time,
@@ -665,7 +666,7 @@ export function AdvancedChart({
size: 1, size: 1,
}) })
} }
// 显示卖出标记(红色,在K线上方) // Show sell markers (red, above bar)
if (counts.sells > 0) { if (counts.sells > 0) {
markers.push({ markers.push({
time: candleTime as Time, 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)) markers.sort((a, b) => (a.time as number) - (b.time as number))
console.log('[AdvancedChart] Valid markers:', markers.length, 'out of', orders.length) 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)) console.log('[AdvancedChart] Markers data:', JSON.stringify(markers, null, 2))
try { try {
// 存储标记数据供后续切换使用 // Store marker data for later toggle use
currentMarkersDataRef.current = markers currentMarkersDataRef.current = markers
// 使用 v5 API: createSeriesMarkers // Using v5 API: createSeriesMarkers
const markersToShow = showOrderMarkers ? markers : [] const markersToShow = showOrderMarkers ? markers : []
if (seriesMarkersRef.current) { if (seriesMarkersRef.current) {
// 如果已经存在,更新标记 // If already exists, update markers
seriesMarkersRef.current.setMarkers(markersToShow) seriesMarkersRef.current.setMarkers(markersToShow)
} else { } else {
// 首次创建标记 // First time creating markers
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markersToShow) seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markersToShow)
} }
console.log('[AdvancedChart] ✅ Markers updated! Count:', markersToShow.length, 'Visible:', showOrderMarkers) 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) { if (isInitialLoadRef.current) {
chartRef.current?.timeScale().fitContent() chartRef.current?.timeScale().fitContent()
isInitialLoadRef.current = false 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) const refreshInterval = setInterval(() => loadData(true), 5000)
return () => clearInterval(refreshInterval) return () => clearInterval(refreshInterval)
}, [symbol, interval, traderID, exchange]) }, [symbol, interval, traderID, exchange])
// 单独刷新挂单价格线 (60秒刷新一次,避免频繁调用交易所API) // Refresh open order price lines separately (every 60s, avoid frequent exchange API calls)
useEffect(() => { useEffect(() => {
if (!traderID || !candlestickSeriesRef.current) return if (!traderID || !candlestickSeriesRef.current) return
// 加载挂单并显示价格线 // Load open orders and display price lines
const loadOpenOrders = async () => { const loadOpenOrders = async () => {
try { try {
// 先清除旧的价格线 // Clear old price lines first
priceLinesRef.current.forEach(line => { priceLinesRef.current.forEach(line => {
try { try {
candlestickSeriesRef.current?.removePriceLine(line) candlestickSeriesRef.current?.removePriceLine(line)
} catch (e) { } catch (e) {
// 忽略清除错误 // Ignore clear error
} }
}) })
priceLinesRef.current = [] priceLinesRef.current = []
@@ -763,28 +764,28 @@ export function AdvancedChart({
if (openOrders.length > 0 && candlestickSeriesRef.current) { if (openOrders.length > 0 && candlestickSeriesRef.current) {
openOrders.forEach(order => { 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 const linePrice = order.stop_price > 0 ? order.stop_price : order.price
if (linePrice <= 0) return if (linePrice <= 0) return
// 判断订单类型 // Determine order type
const isStopLoss = order.type.includes('STOP') || order.type.includes('SL') const isStopLoss = order.type.includes('STOP') || order.type.includes('SL')
const isTakeProfit = order.type.includes('TAKE_PROFIT') || order.type.includes('TP') const isTakeProfit = order.type.includes('TAKE_PROFIT') || order.type.includes('TP')
const isLimit = order.type === 'LIMIT' const isLimit = order.type === 'LIMIT'
// 设置价格线样式 // Set price line style
let lineColor = '#F0B90B' // 默认黄色 let lineColor = '#F0B90B' // Default yellow
const lineStyle = 2 // 虚线 const lineStyle = 2 // dashed
let title = '' let title = ''
if (isStopLoss) { if (isStopLoss) {
lineColor = '#F6465D' // 红色 - 止损 lineColor = '#F6465D' // red - stop loss
title = `SL ${order.quantity}` title = `SL ${order.quantity}`
} else if (isTakeProfit) { } else if (isTakeProfit) {
lineColor = '#0ECB81' // 绿色 - 止盈 lineColor = '#0ECB81' // green - take profit
title = `TP ${order.quantity}` title = `TP ${order.quantity}`
} else if (isLimit) { } else if (isLimit) {
lineColor = '#F0B90B' // 黄色 - 限价单 lineColor = '#F0B90B' // yellow - limit order
title = `Limit ${order.side} ${order.quantity}` title = `Limit ${order.side} ${order.quantity}`
} else { } else {
title = `${order.type} ${order.quantity}` 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) const initialTimeout = setTimeout(loadOpenOrders, 1000)
// 60秒刷新一次挂单 // Refresh open orders every 60 seconds
const openOrdersInterval = setInterval(loadOpenOrders, 60000) const openOrdersInterval = setInterval(loadOpenOrders, 60000)
return () => { return () => {
@@ -822,7 +823,7 @@ export function AdvancedChart({
} }
}, [symbol, traderID]) }, [symbol, traderID])
// 单独处理订单标记的显示/隐藏,避免重新加载数据 // Handle order marker show/hide separately to avoid reloading data
useEffect(() => { useEffect(() => {
if (!seriesMarkersRef.current) return if (!seriesMarkersRef.current) return
@@ -835,17 +836,17 @@ export function AdvancedChart({
} }
}, [showOrderMarkers]) }, [showOrderMarkers])
// 更新指标 // Update indicators
const updateIndicators = (klineData: Kline[]) => { const updateIndicators = (klineData: Kline[]) => {
if (!chartRef.current) return if (!chartRef.current) return
// 清除旧指标 // Clear old indicators
indicatorSeriesRef.current.forEach(series => { indicatorSeriesRef.current.forEach(series => {
chartRef.current?.removeSeries(series as any) chartRef.current?.removeSeries(series as any)
}) })
indicatorSeriesRef.current.clear() indicatorSeriesRef.current.clear()
// 添加启用的指标 // Add enabled indicators
indicators.forEach(indicator => { indicators.forEach(indicator => {
if (!indicator.enabled || !chartRef.current) return if (!indicator.enabled || !chartRef.current) return
@@ -864,7 +865,7 @@ export function AdvancedChart({
color: indicator.color, color: indicator.color,
lineWidth: 2, lineWidth: 2,
title: indicator.name, title: indicator.name,
lineStyle: 2, // 虚线 lineStyle: 2, // dashed
}) })
series.setData(emaData as any) series.setData(emaData as any)
indicatorSeriesRef.current.set(indicator.id, series) indicatorSeriesRef.current.set(indicator.id, series)
@@ -900,7 +901,7 @@ export function AdvancedChart({
}) })
} }
// 切换指标 // Toggle indicator
const toggleIndicator = (id: string) => { const toggleIndicator = (id: string) => {
setIndicators(prev => setIndicators(prev =>
prev.map(ind => (ind.id === id ? { ...ind, enabled: !ind.enabled } : ind)) prev.map(ind => (ind.id === id ? { ...ind, enabled: !ind.enabled } : ind))
@@ -980,7 +981,7 @@ export function AdvancedChart({
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{loading && ( {loading && (
<span className="text-[10px] text-yellow-400 animate-pulse mr-2"> <span className="text-[10px] text-yellow-400 animate-pulse mr-2">
{language === 'zh' ? '更新中...' : 'Updating...'} {t('advancedChart.updating', language)}
</span> </span>
)} )}
<button <button
@@ -992,7 +993,7 @@ export function AdvancedChart({
}} }}
> >
<Settings className="w-3 h-3" /> <Settings className="w-3 h-3" />
<span>{language === 'zh' ? '指标' : 'Indicators'}</span> <span>{t('advancedChart.indicators', language)}</span>
</button> </button>
<button <button
@@ -1002,14 +1003,14 @@ export function AdvancedChart({
background: showOrderMarkers ? 'rgba(16, 185, 129, 0.15)' : 'transparent', background: showOrderMarkers ? 'rgba(16, 185, 129, 0.15)' : 'transparent',
color: showOrderMarkers ? '#10B981' : '#6B7280', color: showOrderMarkers ? '#10B981' : '#6B7280',
}} }}
title={language === 'zh' ? '订单标记' : 'Order Markers'} title={t('advancedChart.orderMarkers', language)}
> >
<span>B/S</span> <span>B/S</span>
</button> </button>
</div> </div>
</div> </div>
{/* 指标面板 - 专业化设计 */} {/* Indicator panel - professional design */}
{showIndicatorPanel && ( {showIndicatorPanel && (
<div <div
className="absolute top-16 right-4 z-10 rounded-lg shadow-2xl backdrop-blur-sm" className="absolute top-16 right-4 z-10 rounded-lg shadow-2xl backdrop-blur-sm"
@@ -1021,7 +1022,7 @@ export function AdvancedChart({
overflowY: 'auto', overflowY: 'auto',
}} }}
> >
{/* 标题栏 */} {/* Title bar */}
<div <div
className="flex items-center justify-between px-4 py-3 border-b" className="flex items-center justify-between px-4 py-3 border-b"
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }} style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
@@ -1029,7 +1030,7 @@ export function AdvancedChart({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BarChart2 className="w-4 h-4 text-yellow-400" /> <BarChart2 className="w-4 h-4 text-yellow-400" />
<h4 className="text-sm font-bold text-white"> <h4 className="text-sm font-bold text-white">
{language === 'zh' ? '技术指标' : 'Technical Indicators'} {t('advancedChart.technicalIndicators', language)}
</h4> </h4>
</div> </div>
<button <button
@@ -1040,7 +1041,7 @@ export function AdvancedChart({
</button> </button>
</div> </div>
{/* 指标列表 */} {/* Indicator list */}
<div className="p-3 space-y-1"> <div className="p-3 space-y-1">
{indicators.map(indicator => ( {indicators.map(indicator => (
<label <label
@@ -1069,17 +1070,17 @@ export function AdvancedChart({
))} ))}
</div> </div>
{/* 底部提示 */} {/* Bottom hint */}
<div <div
className="px-4 py-2 text-xs text-gray-500 border-t" className="px-4 py-2 text-xs text-gray-500 border-t"
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }} style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
> >
{language === 'zh' ? '点击选择需要显示的指标' : 'Click to toggle indicators'} {t('advancedChart.clickToToggle', language)}
</div> </div>
</div> </div>
)} )}
{/* 图表容器 */} {/* Chart container */}
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}> <div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
<div ref={chartContainerRef} style={{ height: '100%', width: '100%' }} /> <div ref={chartContainerRef} style={{ height: '100%', width: '100%' }} />
@@ -1151,7 +1152,7 @@ export function AdvancedChart({
</div> </div>
)} )}
{/* NOFX 水印 */} {/* NOFX watermark */}
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -1177,7 +1178,7 @@ export function AdvancedChart({
</div> </div>
</div> </div>
{/* 错误提示 */} {/* Error message */}
{error && ( {error && (
<div <div
className="absolute inset-0 flex items-center justify-center" className="absolute inset-0 flex items-center justify-center"
+25 -24
View File
@@ -3,14 +3,15 @@ import { EquityChart } from './EquityChart'
import { AdvancedChart } from './AdvancedChart' import { AdvancedChart } from './AdvancedChart'
import { useLanguage } from '../../contexts/LanguageContext' import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations' import { t } from '../../i18n/translations'
import { chartTabs, ts } from '../../i18n/strategy-translations'
import { BarChart3, CandlestickChart, ChevronDown, Search } from 'lucide-react' import { BarChart3, CandlestickChart, ChevronDown, Search } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
interface ChartTabsProps { interface ChartTabsProps {
traderId: string traderId: string
selectedSymbol?: string // 从外部选择的币种 selectedSymbol?: string // Externally selected symbol
updateKey?: number // 强制更新的 key updateKey?: number // Force update key
exchangeId?: string // 交易所ID exchangeId?: string // Exchange ID
} }
type ChartTab = 'equity' | 'kline' type ChartTab = 'equity' | 'kline'
@@ -23,13 +24,13 @@ interface SymbolInfo {
category: string category: string
} }
// 市场类型配置 // Market type configuration
const MARKET_CONFIG = { const MARKET_CONFIG = {
hyperliquid: { exchange: 'hyperliquid', defaultSymbol: 'BTC', icon: '🔷', label: { zh: 'HL', en: 'HL' }, color: 'cyan', hasDropdown: true }, hyperliquid: { exchange: 'hyperliquid', defaultSymbol: 'BTC', icon: '🔷', labelKey: 'hyperliquid' as const, color: 'cyan', hasDropdown: true },
crypto: { exchange: 'binance', defaultSymbol: 'BTCUSDT', icon: '₿', label: { zh: '加密', en: 'Crypto' }, color: 'yellow', hasDropdown: false }, crypto: { exchange: 'binance', defaultSymbol: 'BTCUSDT', icon: '₿', labelKey: 'crypto' as const, color: 'yellow', hasDropdown: false },
stocks: { exchange: 'alpaca', defaultSymbol: 'AAPL', icon: '📈', label: { zh: '美股', en: 'Stocks' }, color: 'green', hasDropdown: false }, stocks: { exchange: 'alpaca', defaultSymbol: 'AAPL', icon: '📈', labelKey: 'stocks' as const, color: 'green', hasDropdown: false },
forex: { exchange: 'forex', defaultSymbol: 'EUR/USD', icon: '💱', label: { zh: '外汇', en: 'Forex' }, color: 'blue', hasDropdown: false }, forex: { exchange: 'forex', defaultSymbol: 'EUR/USD', icon: '💱', labelKey: 'forex' as const, color: 'blue', hasDropdown: false },
metals: { exchange: 'metals', defaultSymbol: 'XAU/USD', icon: '🥇', label: { zh: '金属', en: 'Metals' }, color: 'amber', hasDropdown: false }, metals: { exchange: 'metals', defaultSymbol: 'XAU/USD', icon: '🥇', labelKey: 'metals' as const, color: 'amber', hasDropdown: false },
} }
const INTERVALS: { value: Interval; label: string }[] = [ const INTERVALS: { value: Interval; label: string }[] = [
@@ -42,12 +43,12 @@ const INTERVALS: { value: Interval; label: string }[] = [
{ value: '1d', label: '1d' }, { value: '1d', label: '1d' },
] ]
// 根据交易所ID推断市场类型 // Infer market type from exchange ID
function getMarketTypeFromExchange(exchangeId: string | undefined): MarketType { function getMarketTypeFromExchange(exchangeId: string | undefined): MarketType {
if (!exchangeId) return 'hyperliquid' if (!exchangeId) return 'hyperliquid'
const lower = exchangeId.toLowerCase() const lower = exchangeId.toLowerCase()
if (lower.includes('hyperliquid')) return 'hyperliquid' if (lower.includes('hyperliquid')) return 'hyperliquid'
// 其他交易所默认使用 crypto 类型 // Other exchanges default to crypto type
return 'crypto' return 'crypto'
} }
@@ -63,25 +64,25 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
const [searchFilter, setSearchFilter] = useState('') const [searchFilter, setSearchFilter] = useState('')
const dropdownRef = useRef<HTMLDivElement>(null) const dropdownRef = useRef<HTMLDivElement>(null)
// 当交易所ID变化时,自动切换市场类型 // Auto-switch market type when exchange ID changes
useEffect(() => { useEffect(() => {
const newMarketType = getMarketTypeFromExchange(exchangeId) const newMarketType = getMarketTypeFromExchange(exchangeId)
setMarketType(newMarketType) setMarketType(newMarketType)
}, [exchangeId]) }, [exchangeId])
// 根据市场类型确定交易所 // Determine exchange from market type
const marketConfig = MARKET_CONFIG[marketType] const marketConfig = MARKET_CONFIG[marketType]
// 优先使用传入的 exchangeId(非 hyperliquid 时) // Prefer passed-in exchangeId (when not hyperliquid)
const currentExchange = marketType === 'hyperliquid' ? 'hyperliquid' : (exchangeId || marketConfig.exchange) const currentExchange = marketType === 'hyperliquid' ? 'hyperliquid' : (exchangeId || marketConfig.exchange)
// 获取可用币种列表 // Fetch available symbol list
useEffect(() => { useEffect(() => {
if (marketConfig.hasDropdown) { if (marketConfig.hasDropdown) {
fetch(`/api/symbols?exchange=${marketConfig.exchange}`) fetch(`/api/symbols?exchange=${marketConfig.exchange}`)
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (data.symbols) { if (data.symbols) {
// 按类别排序: crypto > stock > forex > commodity > index // Sort by category: crypto > stock > forex > commodity > index
const categoryOrder: Record<string, number> = { crypto: 0, stock: 1, forex: 2, commodity: 3, index: 4 } const categoryOrder: Record<string, number> = { crypto: 0, stock: 1, forex: 2, commodity: 3, index: 4 }
const sorted = [...data.symbols].sort((a: SymbolInfo, b: SymbolInfo) => { const sorted = [...data.symbols].sort((a: SymbolInfo, b: SymbolInfo) => {
const orderA = categoryOrder[a.category] ?? 5 const orderA = categoryOrder[a.category] ?? 5
@@ -96,7 +97,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
} }
}, [marketType, marketConfig.exchange, marketConfig.hasDropdown]) }, [marketType, marketConfig.exchange, marketConfig.hasDropdown])
// 点击外部关闭下拉 // Close dropdown on outside click
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { 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) return () => document.removeEventListener('mousedown', handleClickOutside)
}, []) }, [])
// 切换市场类型时更新默认符号 // Update default symbol when switching market type
const handleMarketTypeChange = (type: MarketType) => { const handleMarketTypeChange = (type: MarketType) => {
setMarketType(type) setMarketType(type)
setChartSymbol(MARKET_CONFIG[type].defaultSymbol) setChartSymbol(MARKET_CONFIG[type].defaultSymbol)
setShowDropdown(false) setShowDropdown(false)
} }
// 过滤后的币种列表 // Filtered symbol list
const filteredSymbols = availableSymbols.filter(s => const filteredSymbols = availableSymbols.filter(s =>
s.symbol.toLowerCase().includes(searchFilter.toLowerCase()) s.symbol.toLowerCase().includes(searchFilter.toLowerCase())
) )
// 当从外部选择币种时,自动切换到K线图 // Auto-switch to kline chart when symbol selected externally
useEffect(() => { useEffect(() => {
if (selectedSymbol) { if (selectedSymbol) {
console.log('[ChartTabs] 收到币种选择:', selectedSymbol, 'updateKey:', updateKey) console.log('[ChartTabs] Symbol selected:', selectedSymbol, 'updateKey:', updateKey)
setChartSymbol(selectedSymbol) setChartSymbol(selectedSymbol)
setActiveTab('kline') setActiveTab('kline')
} }
}, [selectedSymbol, updateKey]) }, [selectedSymbol, updateKey])
// 处理手动输入符号 // Handle manual symbol input
const handleSymbolSubmit = (e: React.FormEvent) => { const handleSymbolSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (symbolInput.trim()) { if (symbolInput.trim()) {
let symbol = symbolInput.trim().toUpperCase() let symbol = symbolInput.trim().toUpperCase()
// 加密货币自动加 USDT 后缀 // Auto-append USDT suffix for crypto
if (marketType === 'crypto' && !symbol.endsWith('USDT')) { if (marketType === 'crypto' && !symbol.endsWith('USDT')) {
symbol = symbol + 'USDT' symbol = symbol + 'USDT'
} }
@@ -198,7 +199,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
}`} }`}
> >
<span className="mr-1 opacity-70">{config.icon}</span> <span className="mr-1 opacity-70">{config.icon}</span>
{language === 'zh' ? config.label.zh : config.label.en} {ts(chartTabs[config.labelKey], language)}
</button> </button>
) )
})} })}
+49 -48
View File
@@ -10,8 +10,9 @@ import {
} from 'lightweight-charts' } from 'lightweight-charts'
import { useLanguage } from '../../contexts/LanguageContext' import { useLanguage } from '../../contexts/LanguageContext'
import { httpClient } from '../../lib/httpClient' import { httpClient } from '../../lib/httpClient'
import { t } from '../../i18n/translations'
// 订单接口定义 // Order marker interface
interface OrderMarker { interface OrderMarker {
time: number // Unix timestamp (seconds) time: number // Unix timestamp (seconds)
price: number price: number
@@ -21,7 +22,7 @@ interface OrderMarker {
symbol: string symbol: string
} }
// K线数据接口 // Kline data interface
interface KlineData { interface KlineData {
time: UTCTimestamp time: UTCTimestamp
open: number open: number
@@ -34,9 +35,9 @@ interface KlineData {
interface ChartWithOrdersProps { interface ChartWithOrdersProps {
symbol: string symbol: string
interval?: string // 1m, 5m, 15m, 1h, 4h, 1d interval?: string // 1m, 5m, 15m, 1h, 4h, 1d
traderID?: string // 用于获取该trader的订单 traderID?: string // Used to fetch orders for this trader
height?: number 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({ export function ChartWithOrders({
@@ -44,7 +45,7 @@ export function ChartWithOrders({
interval = '5m', interval = '5m',
traderID, traderID,
height = 500, height = 500,
exchange = 'binance', // 默认使用 binance exchange = 'binance', // Default to binance
}: ChartWithOrdersProps) { }: ChartWithOrdersProps) {
const { language } = useLanguage() const { language } = useLanguage()
const chartContainerRef = useRef<HTMLDivElement>(null) const chartContainerRef = useRef<HTMLDivElement>(null)
@@ -56,16 +57,16 @@ export function ChartWithOrders({
const [tooltipData, setTooltipData] = useState<any>(null) const [tooltipData, setTooltipData] = useState<any>(null)
const tooltipRef = useRef<HTMLDivElement>(null) const tooltipRef = useRef<HTMLDivElement>(null)
// 解析时间:支持 Unix 时间戳(数字)或字符串格式 // Parse time: supports Unix timestamp (number) or string format
const parseCustomTime = (time: any): number => { const parseCustomTime = (time: any): number => {
if (!time) { if (!time) {
console.warn('[ChartWithOrders] Empty time value') console.warn('[ChartWithOrders] Empty time value')
return 0 return 0
} }
// 如果已经是数字(Unix 时间戳) // If already a number (Unix timestamp)
if (typeof time === 'number') { if (typeof time === 'number') {
// 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳) // Determine ms vs seconds: if > 10^12, treat as milliseconds
if (time > 1000000000000) { if (time > 1000000000000) {
const seconds = Math.floor(time / 1000) const seconds = Math.floor(time / 1000)
console.log('[ChartWithOrders] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')') console.log('[ChartWithOrders] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
@@ -78,7 +79,7 @@ export function ChartWithOrders({
const timeStr = String(time) const timeStr = String(time)
console.log('[ChartWithOrders] Parsing time string:', timeStr) console.log('[ChartWithOrders] Parsing time string:', timeStr)
// 尝试标准ISO格式 // Try standard ISO format
const isoTime = new Date(timeStr).getTime() const isoTime = new Date(timeStr).getTime()
if (!isNaN(isoTime) && isoTime > 0) { if (!isNaN(isoTime) && isoTime > 0) {
const timestamp = Math.floor(isoTime / 1000) const timestamp = Math.floor(isoTime / 1000)
@@ -86,7 +87,7 @@ export function ChartWithOrders({
return timestamp 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/) const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
if (match) { if (match) {
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
@@ -107,10 +108,10 @@ export function ChartWithOrders({
return 0 return 0
} }
// 从我们的服务获取K线数据 // Fetch kline data from our service
const fetchKlineData = async (symbol: string, interval: string): Promise<KlineData[]> => { const fetchKlineData = async (symbol: string, interval: string): Promise<KlineData[]> => {
try { 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 klineUrl = `/api/klines?symbol=${symbol}&interval=${interval}&limit=${limit}&exchange=${exchange}`
const result = await httpClient.get(klineUrl) const result = await httpClient.get(klineUrl)
@@ -121,10 +122,10 @@ export function ChartWithOrders({
const data = result.data const data = result.data
// 转换后端数据格式到 lightweight-charts 格式 // Convert backend data format to lightweight-charts format
// 后端返回的是 market.Kline 格式: {OpenTime, Open, High, Low, Close, Volume, ...} // Backend returns market.Kline format: {OpenTime, Open, High, Low, Close, Volume, ...}
return data.map((candle: any) => ({ 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, open: candle.open,
high: candle.high, high: candle.high,
low: candle.low, low: candle.low,
@@ -137,10 +138,10 @@ export function ChartWithOrders({
} }
} }
// 获取订单数据 // Fetch order data
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => { const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
try { 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`) const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=50`)
if (!result.success || !result.data) { if (!result.success || !result.data) {
@@ -151,7 +152,7 @@ export function ChartWithOrders({
const orders = result.data const orders = result.data
const markers: OrderMarker[] = [] const markers: OrderMarker[] = []
// 转换订单数据为标记格式 // Convert order data to marker format
orders.forEach((order: any) => { orders.forEach((order: any) => {
const createdAt = order.created_at || order.CreatedAt const createdAt = order.created_at || order.CreatedAt
const filledAt = order.filled_at || order.FilledAt const filledAt = order.filled_at || order.FilledAt
@@ -162,14 +163,14 @@ export function ChartWithOrders({
const status = order.status || order.Status const status = order.status || order.Status
const symbol = order.symbol || order.Symbol const symbol = order.symbol || order.Symbol
// 使用成交时间(如果有)或创建时间 // Use fill time (if available) or creation time
const orderTime = filledAt || createdAt const orderTime = filledAt || createdAt
if (!orderTime) return if (!orderTime) return
const timeSeconds = parseCustomTime(orderTime) const timeSeconds = parseCustomTime(orderTime)
if (timeSeconds === 0) return if (timeSeconds === 0) return
// 使用平均成交价(如果有)或订单价格 // Use average fill price (if available) or order price
const orderPrice = avgPrice || price const orderPrice = avgPrice || price
if (!orderPrice || orderPrice === 0) return if (!orderPrice || orderPrice === 0) return
@@ -191,7 +192,7 @@ export function ChartWithOrders({
} }
} }
// 初始化图表 // Initialize chart
useEffect(() => { useEffect(() => {
if (!chartContainerRef.current) { if (!chartContainerRef.current) {
console.error('[ChartWithOrders] Container ref is null') console.error('[ChartWithOrders] Container ref is null')
@@ -201,7 +202,7 @@ export function ChartWithOrders({
console.log('[ChartWithOrders] Initializing chart for', symbol, interval) console.log('[ChartWithOrders] Initializing chart for', symbol, interval)
try { try {
// 创建图表 // Create chart
const chart = createChart(chartContainerRef.current, { const chart = createChart(chartContainerRef.current, {
width: chartContainerRef.current.clientWidth, width: chartContainerRef.current.clientWidth,
height: height, height: height,
@@ -240,7 +241,7 @@ export function ChartWithOrders({
chartRef.current = chart chartRef.current = chart
// 创建K线系列 (使用 v5 API) // Create candlestick series (using v5 API)
const candlestickSeries = chart.addSeries(CandlestickSeries, { const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#0ECB81', upColor: '#0ECB81',
downColor: '#F6465D', downColor: '#F6465D',
@@ -252,7 +253,7 @@ export function ChartWithOrders({
candlestickSeriesRef.current = candlestickSeries as any candlestickSeriesRef.current = candlestickSeries as any
// 响应式调整 // Responsive resize
const handleResize = () => { const handleResize = () => {
if (chartContainerRef.current && chartRef.current) { if (chartContainerRef.current && chartRef.current) {
chartRef.current.applyOptions({ chartRef.current.applyOptions({
@@ -263,7 +264,7 @@ export function ChartWithOrders({
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
// 监听鼠标移动,显示 OHLC 信息 // Listen for crosshair movement to show OHLC info
chart.subscribeCrosshairMove((param) => { chart.subscribeCrosshairMove((param) => {
if (!param.time || !param.point || !candlestickSeriesRef.current) { if (!param.time || !param.point || !candlestickSeriesRef.current) {
setTooltipData(null) setTooltipData(null)
@@ -298,7 +299,7 @@ export function ChartWithOrders({
} }
}, [height]) }, [height])
// 加载数据 // Load data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
if (!candlestickSeriesRef.current) { if (!candlestickSeriesRef.current) {
@@ -311,22 +312,22 @@ export function ChartWithOrders({
setError(null) setError(null)
try { try {
// 1. 获取K线数据 // 1. Fetch kline data
console.log('[ChartWithOrders] Fetching kline data...') console.log('[ChartWithOrders] Fetching kline data...')
const klineData = await fetchKlineData(symbol, interval) const klineData = await fetchKlineData(symbol, interval)
console.log('[ChartWithOrders] Kline data received:', klineData.length, 'candles') console.log('[ChartWithOrders] Kline data received:', klineData.length, 'candles')
candlestickSeriesRef.current.setData(klineData) candlestickSeriesRef.current.setData(klineData)
// 构建 K 线时间集合,用于快速查找 // Build kline time set for quick lookup
const klineTimeSet = new Set(klineData.map(k => k.time as number)) const klineTimeSet = new Set(klineData.map(k => k.time as number))
const klineMinTime = klineData.length > 0 ? klineData[0].time : 0 const klineMinTime = klineData.length > 0 ? klineData[0].time : 0
const klineMaxTime = klineData.length > 0 ? klineData[klineData.length - 1].time : 0 const klineMaxTime = klineData.length > 0 ? klineData[klineData.length - 1].time : 0
console.log('[ChartWithOrders] Kline time range:', klineMinTime, '-', klineMaxTime, 'candles:', klineData.length) console.log('[ChartWithOrders] Kline time range:', klineMinTime, '-', klineMaxTime, 'candles:', klineData.length)
// 计算时间周期的秒数 // Calculate interval in seconds
const getIntervalSeconds = (interval: string): number => { const getIntervalSeconds = (interval: string): number => {
const match = interval.match(/(\d+)([smhd])/) const match = interval.match(/(\d+)([smhd])/)
if (!match) return 60 // 默认1分钟 if (!match) return 60 // Default 1 minute
const [, num, unit] = match const [, num, unit] = match
const n = parseInt(num) const n = parseInt(num)
switch (unit) { switch (unit) {
@@ -340,7 +341,7 @@ export function ChartWithOrders({
const intervalSeconds = getIntervalSeconds(interval) const intervalSeconds = getIntervalSeconds(interval)
console.log('[ChartWithOrders] Interval:', interval, '=', intervalSeconds, 'seconds') console.log('[ChartWithOrders] Interval:', interval, '=', intervalSeconds, 'seconds')
// 2. 获取订单数据并添加标记 // 2. Fetch order data and add markers
if (traderID) { if (traderID) {
console.log('[ChartWithOrders] Fetching orders for trader:', traderID, 'symbol:', symbol) console.log('[ChartWithOrders] Fetching orders for trader:', traderID, 'symbol:', symbol)
const orders = await fetchOrders(traderID, symbol) const orders = await fetchOrders(traderID, symbol)
@@ -350,7 +351,7 @@ export function ChartWithOrders({
console.log('[ChartWithOrders] No orders to display') console.log('[ChartWithOrders] No orders to display')
} }
// 转换订单为图表标记,并对齐到 K 线时间 // Convert orders to chart markers, aligned to kline time
const markers: Array<{ const markers: Array<{
time: Time time: Time
position: 'belowBar' position: 'belowBar'
@@ -362,10 +363,10 @@ export function ChartWithOrders({
}> = [] }> = []
orders.forEach((order) => { orders.forEach((order) => {
// 将订单时间对齐到 K 线周期(向下取整) // Align order time to kline interval (floor)
const alignedTime = Math.floor(order.time / intervalSeconds) * intervalSeconds const alignedTime = Math.floor(order.time / intervalSeconds) * intervalSeconds
// 检查对齐后的时间是否在 K 线数据中存在 // Check if aligned time exists in kline data
if (!klineTimeSet.has(alignedTime)) { if (!klineTimeSet.has(alignedTime)) {
console.warn('[ChartWithOrders] ⚠️ Skipping order - no matching kline:', console.warn('[ChartWithOrders] ⚠️ Skipping order - no matching kline:',
order.time, '→', alignedTime, '(', new Date(order.time * 1000).toISOString(), ')') 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') console.log('[ChartWithOrders] Setting', markers.length, 'markers on chart')
try { try {
// 使用 v5 API: createSeriesMarkers // Using v5 API: createSeriesMarkers
if (seriesMarkersRef.current) { if (seriesMarkersRef.current) {
// 如果已经存在,更新标记 // If already exists, update markers
seriesMarkersRef.current.setMarkers(markers) seriesMarkersRef.current.setMarkers(markers)
} else { } else {
// 首次创建标记 // First time creating markers
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markers) seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markers)
} }
console.log('[ChartWithOrders] ✅ Markers set successfully!') console.log('[ChartWithOrders] ✅ Markers set successfully!')
@@ -403,23 +404,23 @@ export function ChartWithOrders({
} }
} }
// 自动适配视图 // Auto-fit view
chartRef.current?.timeScale().fitContent() chartRef.current?.timeScale().fitContent()
setLoading(false) setLoading(false)
} catch (err) { } catch (err) {
console.error('Error loading chart data:', err) console.error('Error loading chart data:', err)
setError(language === 'zh' ? '加载图表数据失败' : 'Failed to load chart data') setError(t('chartWithOrders.failedToLoad', language))
setLoading(false) setLoading(false)
} }
} }
loadData() loadData()
// 自动刷新 - 每30秒更新一次K线数据 // Auto-refresh - update kline data every 30 seconds
const refreshInterval = setInterval(() => { const refreshInterval = setInterval(() => {
loadData() loadData()
}, 30000) // 30 }, 30000) // 30 seconds
return () => { return () => {
clearInterval(refreshInterval) clearInterval(refreshInterval)
@@ -428,7 +429,7 @@ export function ChartWithOrders({
return ( return (
<div className="relative" style={{ background: '#0B0E11', borderRadius: '8px', overflow: 'hidden' }}> <div className="relative" style={{ background: '#0B0E11', borderRadius: '8px', overflow: 'hidden' }}>
{/* 标题栏 */} {/* Title bar */}
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid #2B3139' }}> <div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid #2B3139' }}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-xl">📈</span> <span className="text-xl">📈</span>
@@ -438,12 +439,12 @@ export function ChartWithOrders({
</div> </div>
{loading && ( {loading && (
<div className="text-sm" style={{ color: '#848E9C' }}> <div className="text-sm" style={{ color: '#848E9C' }}>
{language === 'zh' ? '加载中...' : 'Loading...'} {t('chartWithOrders.loading', language)}
</div> </div>
)} )}
</div> </div>
{/* 图表容器 */} {/* Chart container */}
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<div ref={chartContainerRef} /> <div ref={chartContainerRef} />
@@ -498,7 +499,7 @@ export function ChartWithOrders({
)} )}
</div> </div>
{/* 错误提示 */} {/* Error display */}
{error && ( {error && (
<div <div
className="absolute inset-0 flex items-center justify-center" className="absolute inset-0 flex items-center justify-center"
@@ -511,15 +512,15 @@ export function ChartWithOrders({
</div> </div>
)} )}
{/* 图例说明 */} {/* Legend */}
<div className="flex items-center gap-4 p-4 text-xs" style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}> <div className="flex items-center gap-4 p-4 text-xs" style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-bold" style={{ color: '#0ECB81' }}>B</span> <span className="font-bold" style={{ color: '#0ECB81' }}>B</span>
<span>{language === 'zh' ? 'BUY (买入)' : 'BUY'}</span> <span>{t('chartWithOrders.buy', language)}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-bold" style={{ color: '#F6465D' }}>S</span> <span className="font-bold" style={{ color: '#F6465D' }}>S</span>
<span>{language === 'zh' ? 'SELL (卖出)' : 'SELL'}</span> <span>{t('chartWithOrders.sell', language)}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -21,12 +21,12 @@ import { BarChart3, TrendingUp, TrendingDown, Zap } from 'lucide-react'
// Time period options: 1D, 3D, 7D, 30D, All // Time period options: 1D, 3D, 7D, 30D, All
const TIME_PERIODS = [ const TIME_PERIODS = [
{ key: '1d', hours: 24, label: { en: '1D', zh: '1天' } }, { key: '1d', hours: 24 },
{ key: '3d', hours: 72, label: { en: '3D', zh: '3天' } }, { key: '3d', hours: 72 },
{ key: '7d', hours: 168, label: { en: '7D', zh: '7天' } }, { key: '7d', hours: 168 },
{ key: '30d', hours: 720, label: { en: '30D', zh: '30天' } }, { key: '30d', hours: 720 },
{ key: 'all', hours: 0, label: { en: 'All', zh: '全部' } }, { key: 'all', hours: 0 },
] ] as const
interface ComparisonChartProps { interface ComparisonChartProps {
traders: CompetitionTraderData[] traders: CompetitionTraderData[]
@@ -352,7 +352,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`, 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)}
</button> </button>
))} ))}
</div> </div>
+5 -5
View File
@@ -58,8 +58,8 @@ export function ConfirmDialogProvider({
const [state, setState] = useState<ConfirmState>({ const [state, setState] = useState<ConfirmState>({
isOpen: false, isOpen: false,
message: '', message: '',
okText: '确认', okText: 'Confirm',
cancelText: '取消', cancelText: 'Cancel',
}) })
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => { const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
@@ -68,14 +68,14 @@ export function ConfirmDialogProvider({
isOpen: true, isOpen: true,
title: options.title, title: options.title,
message: options.message, message: options.message,
okText: options.okText || '确认', okText: options.okText || 'Confirm',
cancelText: options.cancelText || '取消', cancelText: options.cancelText || 'Cancel',
resolve, resolve,
}) })
}) })
}, []) }, [])
// 注册全局 confirm 函数 // Register global confirm function
useEffect(() => { useEffect(() => {
setGlobalConfirm(confirm) setGlobalConfirm(confirm)
}, [confirm]) }, [confirm])
+3 -1
View File
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'
import { HelpCircle } from 'lucide-react' import { HelpCircle } from 'lucide-react'
import katex from 'katex' import katex from 'katex'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import { t } from '../../i18n/translations'
export interface MetricDefinition { export interface MetricDefinition {
key: string key: string
@@ -241,6 +242,7 @@ export function MetricTooltip({
const name = language === 'zh' ? metric.nameZh : metric.nameEn const name = language === 'zh' ? metric.nameZh : metric.nameEn
const description = language === 'zh' ? metric.descriptionZh : metric.descriptionEn const description = language === 'zh' ? metric.descriptionZh : metric.descriptionEn
const formulaLabel = t('metricTooltip.formula', language as 'en' | 'zh' | 'id')
const tooltipContent = ( const tooltipContent = (
<div <div
@@ -292,7 +294,7 @@ export function MetricTooltip({
marginBottom: '12px' marginBottom: '12px'
}}> }}>
<div style={{ fontSize: '12px', color: '#848E9C', marginBottom: '8px' }}> <div style={{ fontSize: '12px', color: '#848E9C', marginBottom: '8px' }}>
{language === 'zh' ? '计算公式' : 'Formula'} {formulaLabel}
</div> </div>
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -1,6 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap, Shuffle } from 'lucide-react' import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap, Shuffle } from 'lucide-react'
import type { CoinSourceConfig } from '../../types' import type { CoinSourceConfig } from '../../types'
import { coinSource, ts } from '../../i18n/strategy-translations'
interface CoinSourceEditorProps { interface CoinSourceEditorProps {
config: CoinSourceConfig config: CoinSourceConfig
@@ -18,52 +19,6 @@ export function CoinSourceEditor({
const [newCoin, setNewCoin] = useState('') const [newCoin, setNewCoin] = useState('')
const [newExcludedCoin, setNewExcludedCoin] = useState('') const [newExcludedCoin, setNewExcludedCoin] = useState('')
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
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 = [ const sourceTypes = [
{ value: 'static', icon: List, color: '#848E9C' }, { value: 'static', icon: List, color: '#848E9C' },
{ value: 'ai500', icon: Database, color: '#F0B90B' }, { value: 'ai500', icon: Database, color: '#F0B90B' },
@@ -82,15 +37,15 @@ export function CoinSourceEditor({
totalLimit += config.ai500_limit || 10 totalLimit += config.ai500_limit || 10
} }
if (config.use_oi_top) { 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 totalLimit += config.oi_top_limit || 10
} }
if (config.use_oi_low) { 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 totalLimit += config.oi_low_limit || 10
} }
if ((config.static_coins || []).length > 0) { 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 totalLimit += config.static_coins?.length || 0
} }
@@ -191,7 +146,7 @@ export function CoinSourceEditor({
{/* Source Type Selector */} {/* Source Type Selector */}
<div> <div>
<label className="block text-sm font-medium mb-3 text-nofx-text"> <label className="block text-sm font-medium mb-3 text-nofx-text">
{t('sourceType')} {ts(coinSource.sourceType, language)}
</label> </label>
<div className="grid grid-cols-5 gap-2"> <div className="grid grid-cols-5 gap-2">
{sourceTypes.map(({ value, icon: Icon, color }) => ( {sourceTypes.map(({ value, icon: Icon, color }) => (
@@ -209,10 +164,10 @@ export function CoinSourceEditor({
> >
<Icon className="w-6 h-6 mx-auto mb-2" style={{ color }} /> <Icon className="w-6 h-6 mx-auto mb-2" style={{ color }} />
<div className="text-sm font-medium text-nofx-text"> <div className="text-sm font-medium text-nofx-text">
{t(value)} {ts(coinSource[value as keyof typeof coinSource], language)}
</div> </div>
<div className="text-xs mt-1 text-nofx-text-muted"> <div className="text-xs mt-1 text-nofx-text-muted">
{t(`${value}Desc`)} {ts(coinSource[`${value}Desc` as keyof typeof coinSource], language)}
</div> </div>
</button> </button>
))} ))}
@@ -223,7 +178,7 @@ export function CoinSourceEditor({
{config.source_type === 'static' && ( {config.source_type === 'static' && (
<div> <div>
<label className="block text-sm font-medium mb-3 text-nofx-text"> <label className="block text-sm font-medium mb-3 text-nofx-text">
{t('staticCoins')} {ts(coinSource.staticCoins, language)}
</label> </label>
<div className="flex flex-wrap gap-2 mb-3"> <div className="flex flex-wrap gap-2 mb-3">
{(config.static_coins || []).map((coin) => ( {(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" className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors bg-nofx-gold text-black hover:bg-yellow-500"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
{t('addCoin')} {ts(coinSource.addCoin, language)}
</button> </button>
</div> </div>
)} )}
@@ -270,11 +225,11 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<Ban className="w-4 h-4 text-nofx-danger" /> <Ban className="w-4 h-4 text-nofx-danger" />
<label className="text-sm font-medium text-nofx-text"> <label className="text-sm font-medium text-nofx-text">
{t('excludedCoins')} {ts(coinSource.excludedCoins, language)}
</label> </label>
</div> </div>
<p className="text-xs mb-3 text-nofx-text-muted"> <p className="text-xs mb-3 text-nofx-text-muted">
{t('excludedCoinsDesc')} {ts(coinSource.excludedCoinsDesc, language)}
</p> </p>
<div className="flex flex-wrap gap-2 mb-3"> <div className="flex flex-wrap gap-2 mb-3">
{(config.excluded_coins || []).map((coin) => ( {(config.excluded_coins || []).map((coin) => (
@@ -295,7 +250,7 @@ export function CoinSourceEditor({
))} ))}
{(config.excluded_coins || []).length === 0 && ( {(config.excluded_coins || []).length === 0 && (
<span className="text-xs italic text-nofx-text-muted"> <span className="text-xs italic text-nofx-text-muted">
{language === 'zh' ? '无' : 'None'} {ts(coinSource.excludedNone, language)}
</span> </span>
)} )}
</div> </div>
@@ -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" 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"
> >
<Ban className="w-4 h-4" /> <Ban className="w-4 h-4" />
{t('addExcludedCoin')} {ts(coinSource.addExcludedCoin, language)}
</button> </button>
</div> </div>
)} )}
@@ -329,7 +284,7 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-nofx-gold" /> <Zap className="w-4 h-4 text-nofx-gold" />
<span className="text-sm font-medium text-nofx-text"> <span className="text-sm font-medium text-nofx-text">
AI500 {t('dataSourceConfig')} AI500 {ts(coinSource.dataSourceConfig, language)}
</span> </span>
<NofxOSBadge /> <NofxOSBadge />
</div> </div>
@@ -346,13 +301,13 @@ export function CoinSourceEditor({
disabled={disabled} disabled={disabled}
className="w-5 h-5 rounded accent-nofx-gold" className="w-5 h-5 rounded accent-nofx-gold"
/> />
<span className="text-nofx-text">{t('useAI500')}</span> <span className="text-nofx-text">{ts(coinSource.useAI500, language)}</span>
</label> </label>
{config.use_ai500 && ( {config.use_ai500 && (
<div className="flex items-center gap-3 pl-8"> <div className="flex items-center gap-3 pl-8">
<span className="text-sm text-nofx-text-muted"> <span className="text-sm text-nofx-text-muted">
{t('ai500Limit')}: {ts(coinSource.ai500Limit, language)}:
</span> </span>
<select <select
value={config.ai500_limit || 10} value={config.ai500_limit || 10}
@@ -371,7 +326,7 @@ export function CoinSourceEditor({
)} )}
<p className="text-xs pl-8 text-nofx-text-muted"> <p className="text-xs pl-8 text-nofx-text-muted">
{t('nofxosNote')} {ts(coinSource.nofxosNote, language)}
</p> </p>
</div> </div>
</div> </div>
@@ -386,7 +341,7 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-nofx-success" /> <TrendingUp className="w-4 h-4 text-nofx-success" />
<span className="text-sm font-medium text-nofx-text"> <span className="text-sm font-medium text-nofx-text">
OI {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig')} {ts(coinSource.oiIncreaseTitle, language)} {ts(coinSource.dataSourceConfig, language)}
</span> </span>
<NofxOSBadge /> <NofxOSBadge />
</div> </div>
@@ -403,13 +358,13 @@ export function CoinSourceEditor({
disabled={disabled} disabled={disabled}
className="w-5 h-5 rounded accent-nofx-success" className="w-5 h-5 rounded accent-nofx-success"
/> />
<span className="text-nofx-text">{t('useOITop')}</span> <span className="text-nofx-text">{ts(coinSource.useOITop, language)}</span>
</label> </label>
{config.use_oi_top && ( {config.use_oi_top && (
<div className="flex items-center gap-3 pl-8"> <div className="flex items-center gap-3 pl-8">
<span className="text-sm text-nofx-text-muted"> <span className="text-sm text-nofx-text-muted">
{t('oiTopLimit')}: {ts(coinSource.oiTopLimit, language)}:
</span> </span>
<select <select
value={config.oi_top_limit || 10} value={config.oi_top_limit || 10}
@@ -428,7 +383,7 @@ export function CoinSourceEditor({
)} )}
<p className="text-xs pl-8 text-nofx-text-muted"> <p className="text-xs pl-8 text-nofx-text-muted">
{t('nofxosNote')} {ts(coinSource.nofxosNote, language)}
</p> </p>
</div> </div>
</div> </div>
@@ -443,7 +398,7 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-nofx-danger" /> <TrendingDown className="w-4 h-4 text-nofx-danger" />
<span className="text-sm font-medium text-nofx-text"> <span className="text-sm font-medium text-nofx-text">
OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig')} {ts(coinSource.oiDecreaseTitle, language)} {ts(coinSource.dataSourceConfig, language)}
</span> </span>
<NofxOSBadge /> <NofxOSBadge />
</div> </div>
@@ -460,13 +415,13 @@ export function CoinSourceEditor({
disabled={disabled} disabled={disabled}
className="w-5 h-5 rounded accent-red-500" className="w-5 h-5 rounded accent-red-500"
/> />
<span className="text-nofx-text">{t('useOILow')}</span> <span className="text-nofx-text">{ts(coinSource.useOILow, language)}</span>
</label> </label>
{config.use_oi_low && ( {config.use_oi_low && (
<div className="flex items-center gap-3 pl-8"> <div className="flex items-center gap-3 pl-8">
<span className="text-sm text-nofx-text-muted"> <span className="text-sm text-nofx-text-muted">
{t('oiLowLimit')}: {ts(coinSource.oiLowLimit, language)}:
</span> </span>
<select <select
value={config.oi_low_limit || 10} value={config.oi_low_limit || 10}
@@ -485,7 +440,7 @@ export function CoinSourceEditor({
)} )}
<p className="text-xs pl-8 text-nofx-text-muted"> <p className="text-xs pl-8 text-nofx-text-muted">
{t('nofxosNote')} {ts(coinSource.nofxosNote, language)}
</p> </p>
</div> </div>
</div> </div>
@@ -497,7 +452,7 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Shuffle className="w-4 h-4 text-blue-400" /> <Shuffle className="w-4 h-4 text-blue-400" />
<span className="text-sm font-medium text-nofx-text"> <span className="text-sm font-medium text-nofx-text">
{t('mixedConfig')} {ts(coinSource.mixedConfig, language)}
</span> </span>
</div> </div>
@@ -566,11 +521,11 @@ export function CoinSourceEditor({
/> />
<TrendingUp className="w-4 h-4 text-nofx-success" /> <TrendingUp className="w-4 h-4 text-nofx-success" />
<span className="text-sm font-medium text-nofx-text"> <span className="text-sm font-medium text-nofx-text">
{language === 'zh' ? 'OI 增加' : 'OI Increase'} {ts(coinSource.oiIncreaseLabel, language)}
</span> </span>
</div> </div>
<p className="text-xs text-nofx-text-muted pl-6 mb-1"> <p className="text-xs text-nofx-text-muted pl-6 mb-1">
{language === 'zh' ? '适合做多' : 'For long'} {ts(coinSource.forLong, language)}
</p> </p>
{config.use_oi_top && ( {config.use_oi_top && (
<div className="flex items-center gap-2 mt-2 pl-6"> <div className="flex items-center gap-2 mt-2 pl-6">
@@ -613,11 +568,11 @@ export function CoinSourceEditor({
/> />
<TrendingDown className="w-4 h-4 text-nofx-danger" /> <TrendingDown className="w-4 h-4 text-nofx-danger" />
<span className="text-sm font-medium text-nofx-text"> <span className="text-sm font-medium text-nofx-text">
{language === 'zh' ? 'OI 减少' : 'OI Decrease'} {ts(coinSource.oiDecreaseLabel, language)}
</span> </span>
</div> </div>
<p className="text-xs text-nofx-text-muted pl-6 mb-1"> <p className="text-xs text-nofx-text-muted pl-6 mb-1">
{language === 'zh' ? '适合做空' : 'For short'} {ts(coinSource.forShort, language)}
</p> </p>
{config.use_oi_low && ( {config.use_oi_low && (
<div className="flex items-center gap-2 mt-2 pl-6"> <div className="flex items-center gap-2 mt-2 pl-6">
@@ -651,7 +606,7 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<List className="w-4 h-4 text-gray-400" /> <List className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium text-nofx-text"> <span className="text-sm font-medium text-nofx-text">
{language === 'zh' ? '自定义' : 'Custom'} {ts(coinSource.custom, language)}
</span> </span>
{(config.static_coins || []).length > 0 && ( {(config.static_coins || []).length > 0 && (
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400"> <span className="text-xs px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">
@@ -720,13 +675,13 @@ export function CoinSourceEditor({
return ( return (
<div className="p-2 rounded bg-nofx-bg border border-nofx-border"> <div className="p-2 rounded bg-nofx-bg border border-nofx-border">
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<span className="text-nofx-text-muted">{t('mixedSummary')}:</span> <span className="text-nofx-text-muted">{ts(coinSource.mixedSummary, language)}:</span>
<span className="text-nofx-text font-medium"> <span className="text-nofx-text font-medium">
{sources.join(' + ')} {sources.join(' + ')}
</span> </span>
</div> </div>
<div className="text-xs text-nofx-text-muted mt-1"> <div className="text-xs text-nofx-text-muted mt-1">
{t('maxCoins')} {totalLimit} {t('coins')} {ts(coinSource.maxCoins, language)} {totalLimit} {ts(coinSource.coins, language)}
</div> </div>
</div> </div>
) )
+52 -116
View File
@@ -1,5 +1,6 @@
import { Grid, DollarSign, TrendingUp, Shield, Compass } from 'lucide-react' import { Grid, DollarSign, TrendingUp, Shield, Compass } from 'lucide-react'
import type { GridStrategyConfig } from '../../types' import type { GridStrategyConfig } from '../../types'
import { gridConfig, ts } from '../../i18n/strategy-translations'
interface GridConfigEditorProps { interface GridConfigEditorProps {
config: GridStrategyConfig config: GridStrategyConfig
@@ -8,7 +9,7 @@ interface GridConfigEditorProps {
language: string language: string
} }
// Default grid config // Default grid configuration
export const defaultGridConfig: GridStrategyConfig = { export const defaultGridConfig: GridStrategyConfig = {
symbol: 'BTCUSDT', symbol: 'BTCUSDT',
grid_count: 10, grid_count: 10,
@@ -33,71 +34,6 @@ export function GridConfigEditor({
disabled, disabled,
language, language,
}: GridConfigEditorProps) { }: GridConfigEditorProps) {
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
// Section titles
tradingPair: { zh: '交易设置', en: 'Trading Setup' },
gridParameters: { zh: '网格参数', en: 'Grid Parameters' },
priceBounds: { zh: '价格边界', en: 'Price Bounds' },
riskControl: { zh: '风险控制', en: 'Risk Control' },
// Trading pair
symbol: { zh: '交易对', en: 'Trading Pair' },
symbolDesc: { zh: '选择要进行网格交易的交易对', en: 'Select trading pair for grid trading' },
// Investment
totalInvestment: { zh: '投资金额 (USDT)', en: 'Investment (USDT)' },
totalInvestmentDesc: { zh: '网格策略的总投资金额', en: 'Total investment for grid strategy' },
leverage: { zh: '杠杆倍数', en: 'Leverage' },
leverageDesc: { zh: '交易使用的杠杆倍数 (1-5)', en: 'Leverage for trading (1-5)' },
// Grid parameters
gridCount: { zh: '网格数量', en: 'Grid Count' },
gridCountDesc: { zh: '网格层级数量 (5-50)', en: 'Number of grid levels (5-50)' },
distribution: { zh: '资金分配方式', en: 'Distribution' },
distributionDesc: { zh: '网格层级的资金分配方式', en: 'Fund allocation across grid levels' },
uniform: { zh: '均匀分配', en: 'Uniform' },
gaussian: { zh: '高斯分配 (推荐)', en: 'Gaussian (Recommended)' },
pyramid: { zh: '金字塔分配', en: 'Pyramid' },
// Price bounds
useAtrBounds: { zh: '自动计算边界 (ATR)', en: 'Auto-calculate Bounds (ATR)' },
useAtrBoundsDesc: { zh: '基于 ATR 自动计算网格上下边界', en: 'Auto-calculate bounds based on ATR' },
atrMultiplier: { zh: 'ATR 倍数', en: 'ATR Multiplier' },
atrMultiplierDesc: { zh: '边界距离当前价格的 ATR 倍数', en: 'ATR multiplier for bounds distance' },
upperPrice: { zh: '上边界价格', en: 'Upper Price' },
upperPriceDesc: { zh: '网格上边界价格 (0=自动计算)', en: 'Grid upper bound (0=auto)' },
lowerPrice: { zh: '下边界价格', en: 'Lower Price' },
lowerPriceDesc: { zh: '网格下边界价格 (0=自动计算)', en: 'Grid lower bound (0=auto)' },
// Risk control
maxDrawdown: { zh: '最大回撤 (%)', en: 'Max Drawdown (%)' },
maxDrawdownDesc: { zh: '触发紧急退出的最大回撤百分比', en: 'Max drawdown before emergency exit' },
stopLoss: { zh: '止损 (%)', en: 'Stop Loss (%)' },
stopLossDesc: { zh: '单仓位止损百分比', en: 'Stop loss per position' },
dailyLossLimit: { zh: '日损失限制 (%)', en: 'Daily Loss Limit (%)' },
dailyLossLimitDesc: { zh: '每日最大亏损百分比', en: 'Maximum daily loss percentage' },
useMakerOnly: { zh: '仅使用 Maker 订单', en: 'Maker Only Orders' },
useMakerOnlyDesc: { zh: '使用限价单以降低手续费', en: 'Use limit orders for lower fees' },
// Direction adjustment
directionAdjust: { zh: '方向自动调整', en: 'Direction Auto-Adjust' },
enableDirectionAdjust: { zh: '启用方向调整', en: 'Enable Direction Adjust' },
enableDirectionAdjustDesc: { zh: '根据箱体突破自动调整网格方向', en: 'Auto-adjust grid direction based on box breakouts' },
directionBiasRatio: { zh: '偏向强度', en: 'Bias Strength' },
directionBiasRatioDesc: { zh: '偏多/偏空模式的强度', en: 'Strength for long_bias/short_bias modes' },
directionBiasExplain: { zh: '偏多模式:X%买 + (100-X)%卖 | 偏空模式:(100-X)%买 + X%卖', en: 'Long bias: X% buy + (100-X)% sell | Short bias: (100-X)% buy + X% sell' },
directionExplain: { zh: '短期箱体突破 → 偏向,中期箱体突破 → 全仓,价格回归 → 逐步恢复中性', en: 'Short box breakout → bias, Mid box breakout → full, Price return → gradually recover to neutral' },
directionModes: { zh: '方向模式说明', en: 'Direction Modes' },
modeNeutral: { zh: '中性:50%买 + 50%卖(默认)', en: 'Neutral: 50% buy + 50% sell (default)' },
modeLongBias: { zh: '偏多:X%买 + (100-X)%卖', en: 'Long Bias: X% buy + (100-X)% sell' },
modeLong: { zh: '全多:100%买 + 0%卖', en: 'Long: 100% buy + 0% sell' },
modeShortBias: { zh: '偏空:(100-X)%买 + X%卖', en: 'Short Bias: (100-X)% buy + X% sell' },
modeShort: { zh: '全空:0%买 + 100%卖', en: 'Short: 0% buy + 100% sell' },
}
return translations[key]?.[language] || key
}
const updateField = <K extends keyof GridStrategyConfig>( const updateField = <K extends keyof GridStrategyConfig>(
key: K, key: K,
value: GridStrategyConfig[K] value: GridStrategyConfig[K]
@@ -125,7 +61,7 @@ export function GridConfigEditor({
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<DollarSign className="w-5 h-5" style={{ color: '#F0B90B' }} /> <DollarSign className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}> <h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('tradingPair')} {ts(gridConfig.tradingPair, language)}
</h3> </h3>
</div> </div>
@@ -133,10 +69,10 @@ export function GridConfigEditor({
{/* Symbol */} {/* Symbol */}
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('symbol')} {ts(gridConfig.symbol, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('symbolDesc')} {ts(gridConfig.symbolDesc, language)}
</p> </p>
<select <select
value={config.symbol} value={config.symbol}
@@ -157,10 +93,10 @@ export function GridConfigEditor({
{/* Investment */} {/* Investment */}
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('totalInvestment')} {ts(gridConfig.totalInvestment, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('totalInvestmentDesc')} {ts(gridConfig.totalInvestmentDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -177,10 +113,10 @@ export function GridConfigEditor({
{/* Leverage */} {/* Leverage */}
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('leverage')} {ts(gridConfig.leverage, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('leverageDesc')} {ts(gridConfig.leverageDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -201,7 +137,7 @@ export function GridConfigEditor({
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Grid className="w-5 h-5" style={{ color: '#F0B90B' }} /> <Grid className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}> <h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('gridParameters')} {ts(gridConfig.gridParameters, language)}
</h3> </h3>
</div> </div>
@@ -209,10 +145,10 @@ export function GridConfigEditor({
{/* Grid Count */} {/* Grid Count */}
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('gridCount')} {ts(gridConfig.gridCount, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('gridCountDesc')} {ts(gridConfig.gridCountDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -229,10 +165,10 @@ export function GridConfigEditor({
{/* Distribution */} {/* Distribution */}
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('distribution')} {ts(gridConfig.distribution, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('distributionDesc')} {ts(gridConfig.distributionDesc, language)}
</p> </p>
<select <select
value={config.distribution} value={config.distribution}
@@ -241,9 +177,9 @@ export function GridConfigEditor({
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={inputStyle} style={inputStyle}
> >
<option value="uniform">{t('uniform')}</option> <option value="uniform">{ts(gridConfig.uniform, language)}</option>
<option value="gaussian">{t('gaussian')}</option> <option value="gaussian">{ts(gridConfig.gaussian, language)}</option>
<option value="pyramid">{t('pyramid')}</option> <option value="pyramid">{ts(gridConfig.pyramid, language)}</option>
</select> </select>
</div> </div>
</div> </div>
@@ -254,7 +190,7 @@ export function GridConfigEditor({
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<TrendingUp className="w-5 h-5" style={{ color: '#F0B90B' }} /> <TrendingUp className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}> <h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('priceBounds')} {ts(gridConfig.priceBounds, language)}
</h3> </h3>
</div> </div>
@@ -263,10 +199,10 @@ export function GridConfigEditor({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<label className="block text-sm" style={{ color: '#EAECEF' }}> <label className="block text-sm" style={{ color: '#EAECEF' }}>
{t('useAtrBounds')} {ts(gridConfig.useAtrBounds, language)}
</label> </label>
<p className="text-xs" style={{ color: '#848E9C' }}> <p className="text-xs" style={{ color: '#848E9C' }}>
{t('useAtrBoundsDesc')} {ts(gridConfig.useAtrBoundsDesc, language)}
</p> </p>
</div> </div>
<label className="relative inline-flex items-center cursor-pointer"> <label className="relative inline-flex items-center cursor-pointer">
@@ -285,10 +221,10 @@ export function GridConfigEditor({
{config.use_atr_bounds ? ( {config.use_atr_bounds ? (
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('atrMultiplier')} {ts(gridConfig.atrMultiplier, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('atrMultiplierDesc')} {ts(gridConfig.atrMultiplierDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -306,10 +242,10 @@ export function GridConfigEditor({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('upperPrice')} {ts(gridConfig.upperPrice, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('upperPriceDesc')} {ts(gridConfig.upperPriceDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -324,10 +260,10 @@ export function GridConfigEditor({
</div> </div>
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('lowerPrice')} {ts(gridConfig.lowerPrice, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('lowerPriceDesc')} {ts(gridConfig.lowerPriceDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -349,17 +285,17 @@ export function GridConfigEditor({
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} /> <Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}> <h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('riskControl')} {ts(gridConfig.riskControl, language)}
</h3> </h3>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxDrawdown')} {ts(gridConfig.maxDrawdown, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxDrawdownDesc')} {ts(gridConfig.maxDrawdownDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -375,10 +311,10 @@ export function GridConfigEditor({
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('stopLoss')} {ts(gridConfig.stopLoss, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('stopLossDesc')} {ts(gridConfig.stopLossDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -394,10 +330,10 @@ export function GridConfigEditor({
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('dailyLossLimit')} {ts(gridConfig.dailyLossLimit, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('dailyLossLimitDesc')} {ts(gridConfig.dailyLossLimitDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -417,10 +353,10 @@ export function GridConfigEditor({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<label className="block text-sm" style={{ color: '#EAECEF' }}> <label className="block text-sm" style={{ color: '#EAECEF' }}>
{t('useMakerOnly')} {ts(gridConfig.useMakerOnly, language)}
</label> </label>
<p className="text-xs" style={{ color: '#848E9C' }}> <p className="text-xs" style={{ color: '#848E9C' }}>
{t('useMakerOnlyDesc')} {ts(gridConfig.useMakerOnlyDesc, language)}
</p> </p>
</div> </div>
<label className="relative inline-flex items-center cursor-pointer"> <label className="relative inline-flex items-center cursor-pointer">
@@ -442,7 +378,7 @@ export function GridConfigEditor({
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Compass className="w-5 h-5" style={{ color: '#F0B90B' }} /> <Compass className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}> <h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('directionAdjust')} {ts(gridConfig.directionAdjust, language)}
</h3> </h3>
</div> </div>
@@ -451,10 +387,10 @@ export function GridConfigEditor({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<label className="block text-sm" style={{ color: '#EAECEF' }}> <label className="block text-sm" style={{ color: '#EAECEF' }}>
{t('enableDirectionAdjust')} {ts(gridConfig.enableDirectionAdjust, language)}
</label> </label>
<p className="text-xs" style={{ color: '#848E9C' }}> <p className="text-xs" style={{ color: '#848E9C' }}>
{t('enableDirectionAdjustDesc')} {ts(gridConfig.enableDirectionAdjustDesc, language)}
</p> </p>
</div> </div>
<label className="relative inline-flex items-center cursor-pointer"> <label className="relative inline-flex items-center cursor-pointer">
@@ -475,30 +411,30 @@ export function GridConfigEditor({
{/* Direction Modes Explanation */} {/* Direction Modes Explanation */}
<div className="p-4 rounded-lg mb-4" style={{ background: '#1E2329', border: '1px solid #F0B90B33' }}> <div className="p-4 rounded-lg mb-4" style={{ background: '#1E2329', border: '1px solid #F0B90B33' }}>
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}> <p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
📊 {t('directionModes')} 📊 {ts(gridConfig.directionModes, language)}
</p> </p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs" style={{ color: '#848E9C' }}> <div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs" style={{ color: '#848E9C' }}>
<div> {t('modeNeutral')}</div> <div> {ts(gridConfig.modeNeutral, language)}</div>
<div> <span style={{ color: '#0ECB81' }}>{t('modeLongBias')}</span></div> <div> <span style={{ color: '#0ECB81' }}>{ts(gridConfig.modeLongBias, language)}</span></div>
<div> <span style={{ color: '#0ECB81' }}>{t('modeLong')}</span></div> <div> <span style={{ color: '#0ECB81' }}>{ts(gridConfig.modeLong, language)}</span></div>
<div> <span style={{ color: '#F6465D' }}>{t('modeShortBias')}</span></div> <div> <span style={{ color: '#F6465D' }}>{ts(gridConfig.modeShortBias, language)}</span></div>
<div> <span style={{ color: '#F6465D' }}>{t('modeShort')}</span></div> <div> <span style={{ color: '#F6465D' }}>{ts(gridConfig.modeShort, language)}</span></div>
</div> </div>
<p className="text-xs mt-3 pt-2 border-t border-zinc-700" style={{ color: '#848E9C' }}> <p className="text-xs mt-3 pt-2 border-t border-zinc-700" style={{ color: '#848E9C' }}>
💡 {t('directionExplain')} 💡 {ts(gridConfig.directionExplain, language)}
</p> </p>
</div> </div>
{/* Bias Strength */} {/* Bias Strength */}
<div className="p-4 rounded-lg" style={sectionStyle}> <div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('directionBiasRatio')} (X) {ts(gridConfig.directionBiasRatio, language)} (X)
</label> </label>
<p className="text-xs mb-1" style={{ color: '#848E9C' }}> <p className="text-xs mb-1" style={{ color: '#848E9C' }}>
{t('directionBiasRatioDesc')} {ts(gridConfig.directionBiasRatioDesc, language)}
</p> </p>
<p className="text-xs mb-3" style={{ color: '#F0B90B' }}> <p className="text-xs mb-3" style={{ color: '#F0B90B' }}>
{t('directionBiasExplain')} {ts(gridConfig.directionBiasExplain, language)}
</p> </p>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<input <input
@@ -518,12 +454,12 @@ export function GridConfigEditor({
</div> </div>
<div className="mt-2 grid grid-cols-2 gap-2 text-xs"> <div className="mt-2 grid grid-cols-2 gap-2 text-xs">
<div className="p-2 rounded" style={{ background: '#0ECB8115', border: '1px solid #0ECB8130' }}> <div className="p-2 rounded" style={{ background: '#0ECB8115', border: '1px solid #0ECB8130' }}>
<span style={{ color: '#0ECB81' }}>/Long Bias: </span> <span style={{ color: '#0ECB81' }}>Long Bias: </span>
<span style={{ color: '#EAECEF' }}>{Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% + {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% </span> <span style={{ color: '#EAECEF' }}>{Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% {ts(gridConfig.buy, language)} + {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% {ts(gridConfig.sell, language)}</span>
</div> </div>
<div className="p-2 rounded" style={{ background: '#F6465D15', border: '1px solid #F6465D30' }}> <div className="p-2 rounded" style={{ background: '#F6465D15', border: '1px solid #F6465D30' }}>
<span style={{ color: '#F6465D' }}>/Short Bias: </span> <span style={{ color: '#F6465D' }}>Short Bias: </span>
<span style={{ color: '#EAECEF' }}>{Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% + {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% </span> <span style={{ color: '#EAECEF' }}>{Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% {ts(gridConfig.buy, language)} + {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% {ts(gridConfig.sell, language)}</span>
</div> </div>
</div> </div>
</div> </div>
+29 -88
View File
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Shield, TrendingUp, AlertTriangle, Activity, Box, ChevronDown, ChevronUp } from 'lucide-react' import { Shield, TrendingUp, AlertTriangle, Activity, Box, ChevronDown, ChevronUp } from 'lucide-react'
import type { GridRiskInfo } from '../../types' import type { GridRiskInfo } from '../../types'
import { gridRisk, ts } from '../../i18n/strategy-translations'
interface GridRiskPanelProps { interface GridRiskPanelProps {
traderId: string traderId: string
@@ -18,66 +19,6 @@ export function GridRiskPanel({
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
// Section titles
gridRisk: { zh: '网格风控', en: 'Grid Risk' },
leverageInfo: { zh: '杠杆', en: 'Leverage' },
positionInfo: { zh: '仓位', en: 'Position' },
liquidationInfo: { zh: '清算', en: 'Liquidation' },
marketState: { zh: '市场', en: 'Market' },
boxState: { zh: '箱体', en: 'Box' },
// Leverage
currentLeverage: { zh: '当前', en: 'Current' },
effectiveLeverage: { zh: '有效', en: 'Effective' },
recommendedLeverage: { zh: '建议', en: 'Recommend' },
// Position
currentPosition: { zh: '当前', en: 'Current' },
maxPosition: { zh: '最大', en: 'Max' },
positionPercent: { zh: '占比', en: 'Usage' },
// Liquidation
liquidationPrice: { zh: '清算价', en: 'Liq Price' },
liquidationDistance: { zh: '距离', en: 'Distance' },
// Market
regimeLevel: { zh: '波动', en: 'Regime' },
currentPrice: { zh: '价格', en: 'Price' },
breakoutLevel: { zh: '突破', en: 'Breakout' },
breakoutDirection: { zh: '方向', en: 'Direction' },
// Box
shortBox: { zh: '短期', en: 'Short' },
midBox: { zh: '中期', en: 'Mid' },
longBox: { zh: '长期', en: 'Long' },
// Regime levels
narrow: { zh: '窄幅', en: 'Narrow' },
standard: { zh: '标准', en: 'Standard' },
wide: { zh: '宽幅', en: 'Wide' },
volatile: { zh: '剧烈', en: 'Volatile' },
trending: { zh: '趋势', en: 'Trending' },
// Breakout levels
none: { zh: '无', en: 'None' },
short: { zh: '短期', en: 'Short' },
mid: { zh: '中期', en: 'Mid' },
long: { zh: '长期', en: 'Long' },
// Directions
up: { zh: '↑', en: '↑' },
down: { zh: '↓', en: '↓' },
// Status
loading: { zh: '加载中...', en: 'Loading...' },
error: { zh: '加载失败', en: 'Load Failed' },
noData: { zh: '暂无数据', en: 'No Data' },
}
return translations[key]?.[language] || key
}
const fetchRiskInfo = useCallback(async () => { const fetchRiskInfo = useCallback(async () => {
try { try {
const token = localStorage.getItem('auth_token') const token = localStorage.getItem('auth_token')
@@ -153,7 +94,7 @@ export function GridRiskPanel({
if (loading) { if (loading) {
return ( return (
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}> <div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
{t('loading')} {ts(gridRisk.loading, language)}
</div> </div>
) )
} }
@@ -161,7 +102,7 @@ export function GridRiskPanel({
if (error) { if (error) {
return ( return (
<div className="p-3 text-center text-xs" style={{ color: '#F6465D' }}> <div className="p-3 text-center text-xs" style={{ color: '#F6465D' }}>
{t('error')}: {error} {ts(gridRisk.error, language)}: {error}
</div> </div>
) )
} }
@@ -169,7 +110,7 @@ export function GridRiskPanel({
if (!riskInfo) { if (!riskInfo) {
return ( return (
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}> <div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
{t('noData')} {ts(gridRisk.noData, language)}
</div> </div>
) )
} }
@@ -184,7 +125,7 @@ export function GridRiskPanel({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Shield className="w-4 h-4" style={{ color: '#F0B90B' }} /> <Shield className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}> <span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
{t('gridRisk')} {ts(gridRisk.gridRisk, language)}
</span> </span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -194,7 +135,7 @@ export function GridRiskPanel({
className="px-2 py-0.5 rounded" className="px-2 py-0.5 rounded"
style={{ background: getRegimeColor(riskInfo.regime_level) + '20', color: getRegimeColor(riskInfo.regime_level) }} style={{ background: getRegimeColor(riskInfo.regime_level) + '20', color: getRegimeColor(riskInfo.regime_level) }}
> >
{t(riskInfo.regime_level || 'standard')} {ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
</span> </span>
<span className="font-mono" style={{ color: '#EAECEF' }}> <span className="font-mono" style={{ color: '#EAECEF' }}>
{riskInfo.effective_leverage.toFixed(1)}x {riskInfo.effective_leverage.toFixed(1)}x
@@ -223,19 +164,19 @@ export function GridRiskPanel({
<div className="p-2 rounded" style={{ background: '#1E2329' }}> <div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 mb-2"> <div className="flex items-center gap-1 mb-2">
<TrendingUp className="w-3 h-3" style={{ color: '#F0B90B' }} /> <TrendingUp className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('leverageInfo')}</span> <span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.leverageInfo, language)}</span>
</div> </div>
<div className="grid grid-cols-3 gap-1 text-xs"> <div className="grid grid-cols-3 gap-1 text-xs">
<div> <div>
<div style={{ color: '#5E6673' }}>{t('currentLeverage')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.currentLeverage, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{riskInfo.current_leverage}x</div> <div className="font-mono" style={{ color: '#EAECEF' }}>{riskInfo.current_leverage}x</div>
</div> </div>
<div> <div>
<div style={{ color: '#5E6673' }}>{t('effectiveLeverage')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.effectiveLeverage, language)}</div>
<div className="font-mono" style={{ color: '#F0B90B' }}>{riskInfo.effective_leverage.toFixed(2)}x</div> <div className="font-mono" style={{ color: '#F0B90B' }}>{riskInfo.effective_leverage.toFixed(2)}x</div>
</div> </div>
<div> <div>
<div style={{ color: '#5E6673' }}>{t('recommendedLeverage')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.recommendedLeverage, language)}</div>
<div <div
className="font-mono" className="font-mono"
style={{ color: riskInfo.current_leverage > riskInfo.recommended_leverage ? '#F6465D' : '#0ECB81' }} style={{ color: riskInfo.current_leverage > riskInfo.recommended_leverage ? '#F6465D' : '#0ECB81' }}
@@ -250,19 +191,19 @@ export function GridRiskPanel({
<div className="p-2 rounded" style={{ background: '#1E2329' }}> <div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 mb-2"> <div className="flex items-center gap-1 mb-2">
<Activity className="w-3 h-3" style={{ color: '#F0B90B' }} /> <Activity className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('positionInfo')}</span> <span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.positionInfo, language)}</span>
</div> </div>
<div className="grid grid-cols-3 gap-1 text-xs"> <div className="grid grid-cols-3 gap-1 text-xs">
<div> <div>
<div style={{ color: '#5E6673' }}>{t('currentPosition')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPosition, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.current_position)}</div> <div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.current_position)}</div>
</div> </div>
<div> <div>
<div style={{ color: '#5E6673' }}>{t('maxPosition')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.maxPosition, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.max_position)}</div> <div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.max_position)}</div>
</div> </div>
<div> <div>
<div style={{ color: '#5E6673' }}>{t('positionPercent')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.positionPercent, language)}</div>
<div className="font-mono" style={{ color: getPositionColor(riskInfo.position_percent) }}> <div className="font-mono" style={{ color: getPositionColor(riskInfo.position_percent) }}>
{riskInfo.position_percent.toFixed(1)}% {riskInfo.position_percent.toFixed(1)}%
</div> </div>
@@ -284,32 +225,32 @@ export function GridRiskPanel({
<div className="p-2 rounded" style={{ background: '#1E2329' }}> <div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 mb-2"> <div className="flex items-center gap-1 mb-2">
<Shield className="w-3 h-3" style={{ color: '#F0B90B' }} /> <Shield className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('marketState')}</span> <span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.marketState, language)}</span>
</div> </div>
<div className="grid grid-cols-2 gap-2 text-xs"> <div className="grid grid-cols-2 gap-2 text-xs">
<div> <div>
<div style={{ color: '#5E6673' }}>{t('regimeLevel')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.regimeLevel, language)}</div>
<div className="font-medium" style={{ color: getRegimeColor(riskInfo.regime_level) }}> <div className="font-medium" style={{ color: getRegimeColor(riskInfo.regime_level) }}>
{t(riskInfo.regime_level || 'standard')} {ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
</div> </div>
</div> </div>
<div> <div>
<div style={{ color: '#5E6673' }}>{t('currentPrice')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPrice, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatPrice(riskInfo.current_price)}</div> <div className="font-mono" style={{ color: '#EAECEF' }}>{formatPrice(riskInfo.current_price)}</div>
</div> </div>
<div> <div>
<div style={{ color: '#5E6673' }}>{t('breakoutLevel')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutLevel, language)}</div>
<div className="font-medium" style={{ color: getBreakoutColor(riskInfo.breakout_level) }}> <div className="font-medium" style={{ color: getBreakoutColor(riskInfo.breakout_level) }}>
{t(riskInfo.breakout_level || 'none')} {ts(gridRisk[(riskInfo.breakout_level || 'none') as keyof typeof gridRisk], language)}
</div> </div>
</div> </div>
<div> <div>
<div style={{ color: '#5E6673' }}>{t('breakoutDirection')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutDirection, language)}</div>
<div <div
className="font-medium" className="font-medium"
style={{ color: riskInfo.breakout_direction === 'up' ? '#0ECB81' : riskInfo.breakout_direction === 'down' ? '#F6465D' : '#848E9C' }} style={{ color: riskInfo.breakout_direction === 'up' ? '#0ECB81' : riskInfo.breakout_direction === 'down' ? '#F6465D' : '#848E9C' }}
> >
{riskInfo.breakout_direction ? t(riskInfo.breakout_direction) : '-'} {riskInfo.breakout_direction ? ts(gridRisk[riskInfo.breakout_direction as keyof typeof gridRisk], language) : '-'}
</div> </div>
</div> </div>
</div> </div>
@@ -319,17 +260,17 @@ export function GridRiskPanel({
<div className="p-2 rounded" style={{ background: '#1E2329' }}> <div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 mb-2"> <div className="flex items-center gap-1 mb-2">
<AlertTriangle className="w-3 h-3" style={{ color: '#F6465D' }} /> <AlertTriangle className="w-3 h-3" style={{ color: '#F6465D' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('liquidationInfo')}</span> <span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.liquidationInfo, language)}</span>
</div> </div>
<div className="grid grid-cols-2 gap-2 text-xs"> <div className="grid grid-cols-2 gap-2 text-xs">
<div> <div>
<div style={{ color: '#5E6673' }}>{t('liquidationPrice')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationPrice, language)}</div>
<div className="font-mono" style={{ color: '#F6465D' }}> <div className="font-mono" style={{ color: '#F6465D' }}>
{riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'} {riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'}
</div> </div>
</div> </div>
<div> <div>
<div style={{ color: '#5E6673' }}>{t('liquidationDistance')}</div> <div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationDistance, language)}</div>
<div className="font-mono" style={{ color: '#F6465D' }}> <div className="font-mono" style={{ color: '#F6465D' }}>
{riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'} {riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'}
</div> </div>
@@ -342,23 +283,23 @@ export function GridRiskPanel({
<div className="p-2 rounded" style={{ background: '#1E2329' }}> <div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 mb-2"> <div className="flex items-center gap-1 mb-2">
<Box className="w-3 h-3" style={{ color: '#F0B90B' }} /> <Box className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('boxState')}</span> <span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.boxState, language)}</span>
</div> </div>
<div className="grid grid-cols-3 gap-2 text-xs"> <div className="grid grid-cols-3 gap-2 text-xs">
<div className="flex justify-between"> <div className="flex justify-between">
<span style={{ color: '#5E6673' }}>{t('shortBox')}</span> <span style={{ color: '#5E6673' }}>{ts(gridRisk.shortBox, language)}</span>
<span className="font-mono" style={{ color: '#EAECEF' }}> <span className="font-mono" style={{ color: '#EAECEF' }}>
{formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)} {formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)}
</span> </span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span style={{ color: '#5E6673' }}>{t('midBox')}</span> <span style={{ color: '#5E6673' }}>{ts(gridRisk.midBox, language)}</span>
<span className="font-mono" style={{ color: '#EAECEF' }}> <span className="font-mono" style={{ color: '#EAECEF' }}>
{formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)} {formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)}
</span> </span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span style={{ color: '#5E6673' }}>{t('longBox')}</span> <span style={{ color: '#5E6673' }}>{ts(gridRisk.longBox, language)}</span>
<span className="font-mono" style={{ color: '#EAECEF' }}> <span className="font-mono" style={{ color: '#EAECEF' }}>
{formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)} {formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)}
</span> </span>
+41 -122
View File
@@ -1,5 +1,6 @@
import { Clock, Activity, TrendingUp, BarChart2, Info, Lock, ExternalLink, Zap, Check, AlertCircle, Key } from 'lucide-react' import { Clock, Activity, TrendingUp, BarChart2, Info, Lock, ExternalLink, Zap, Check, AlertCircle, Key } from 'lucide-react'
import type { IndicatorConfig } from '../../types' import type { IndicatorConfig } from '../../types'
import { indicator, ts } from '../../i18n/strategy-translations'
// Default NofxOS API Key // Default NofxOS API Key
const DEFAULT_NOFXOS_API_KEY = 'cm_568c67eae410d912c54c' const DEFAULT_NOFXOS_API_KEY = 'cm_568c67eae410d912c54c'
@@ -11,7 +12,7 @@ interface IndicatorEditorProps {
language: string language: string
} }
// 所有可用时间周期 // All available timeframes
const allTimeframes = [ const allTimeframes = [
{ value: '1m', label: '1m', category: 'scalp' }, { value: '1m', label: '1m', category: 'scalp' },
{ value: '3m', label: '3m', category: 'scalp' }, { value: '3m', label: '3m', category: 'scalp' },
@@ -35,92 +36,10 @@ export function IndicatorEditor({
disabled, disabled,
language, language,
}: IndicatorEditorProps) { }: IndicatorEditorProps) {
const t = (key: string) => { // Get currently selected timeframes
const translations: Record<string, Record<string, string>> = {
// Section titles
marketData: { zh: '市场数据', en: 'Market Data' },
marketDataDesc: { zh: 'AI 分析所需的核心价格数据', en: 'Core price data for AI analysis' },
technicalIndicators: { zh: '技术指标', en: 'Technical Indicators' },
technicalIndicatorsDesc: { zh: '可选的技术分析指标,AI 可自行计算', en: 'Optional indicators, AI can calculate them' },
marketSentiment: { zh: '市场情绪', en: 'Market Sentiment' },
marketSentimentDesc: { zh: '持仓量、资金费率等市场情绪数据', en: 'OI, funding rate and market sentiment data' },
quantData: { zh: '量化数据', en: 'Quant Data' },
quantDataDesc: { zh: '资金流向、大户动向', en: 'Netflow, whale movements' },
// Timeframes
timeframes: { zh: '时间周期', en: 'Timeframes' },
timeframesDesc: { zh: '选择 K 线分析周期,★ 为主周期(双击设置)', en: 'Select K-line timeframes, ★ = primary (double-click)' },
klineCount: { zh: 'K 线数量', en: 'K-line Count' },
scalp: { zh: '超短', en: 'Scalp' },
intraday: { zh: '日内', en: 'Intraday' },
swing: { zh: '波段', en: 'Swing' },
position: { zh: '趋势', en: 'Position' },
// Data types
rawKlines: { zh: 'OHLCV 原始 K 线', en: 'Raw OHLCV K-lines' },
rawKlinesDesc: { zh: '必须 - 开高低收量原始数据,AI 核心分析依据', en: 'Required - Open/High/Low/Close/Volume data for AI' },
required: { zh: '必须', en: 'Required' },
// Indicators
ema: { zh: 'EMA 均线', en: 'EMA' },
emaDesc: { zh: '指数移动平均线', en: 'Exponential Moving Average' },
macd: { zh: 'MACD', en: 'MACD' },
macdDesc: { zh: '异同移动平均线', en: 'Moving Average Convergence Divergence' },
rsi: { zh: 'RSI', en: 'RSI' },
rsiDesc: { zh: '相对强弱指标', en: 'Relative Strength Index' },
atr: { zh: 'ATR', en: 'ATR' },
atrDesc: { zh: '真实波幅均值', en: 'Average True Range' },
boll: { zh: 'BOLL 布林带', en: 'Bollinger Bands' },
bollDesc: { zh: '布林带指标(上中下轨)', en: 'Upper/Middle/Lower Bands' },
volume: { zh: '成交量', en: 'Volume' },
volumeDesc: { zh: '交易量分析', en: 'Trading volume analysis' },
oi: { zh: '持仓量', en: 'Open Interest' },
oiDesc: { zh: '合约未平仓量', en: 'Futures open interest' },
fundingRate: { zh: '资金费率', en: 'Funding Rate' },
fundingRateDesc: { zh: '永续合约资金费率', en: 'Perpetual funding rate' },
// OI Ranking
oiRanking: { zh: 'OI 排行', en: 'OI Ranking' },
oiRankingDesc: { zh: '持仓量增减排行', en: 'OI change ranking' },
oiRankingNote: { zh: '显示持仓量增加/减少的币种排行,帮助发现资金流向', en: 'Shows coins with OI increase/decrease, helps identify capital flow' },
// NetFlow Ranking
netflowRanking: { zh: '资金流向', en: 'NetFlow' },
netflowRankingDesc: { zh: '机构/散户资金流向', en: 'Institution/retail fund flow' },
netflowRankingNote: { zh: '显示机构资金流入/流出排行,散户动向对比,发现聪明钱信号', en: 'Shows institution inflow/outflow ranking, retail flow comparison, Smart Money signals' },
// Price Ranking
priceRanking: { zh: '涨跌幅排行', en: 'Price Ranking' },
priceRankingDesc: { zh: '涨跌幅排行榜', en: 'Gainers/losers ranking' },
priceRankingNote: { zh: '显示涨幅/跌幅排行,结合资金流和持仓变化分析趋势强度', en: 'Shows top gainers/losers, combined with fund flow and OI for trend analysis' },
priceRankingMulti: { zh: '多周期', en: 'Multi-period' },
// Common settings
duration: { zh: '周期', en: 'Duration' },
limit: { zh: '数量', en: 'Limit' },
// Tips
aiCanCalculate: { zh: '💡 提示:AI 可自行计算这些指标,开启可减少 AI 计算量', en: '💡 Tip: AI can calculate these, enabling reduces AI workload' },
// NofxOS Data Provider
nofxosTitle: { zh: 'NofxOS 量化数据源', en: 'NofxOS Data Provider' },
nofxosDesc: { zh: '专业加密货币量化数据服务', en: 'Professional crypto quant data service' },
nofxosFeatures: { zh: 'AI500 · OI排行 · 资金流向 · 涨跌榜', en: 'AI500 · OI Ranking · Fund Flow · Price Ranking' },
viewApiDocs: { zh: 'API 文档', en: 'API Docs' },
apiKey: { zh: 'API Key', en: 'API Key' },
apiKeyPlaceholder: { zh: '输入 NofxOS API Key', en: 'Enter NofxOS API Key' },
fillDefault: { zh: '填入默认', en: 'Fill Default' },
connected: { zh: '已配置', en: 'Configured' },
notConfigured: { zh: '未配置', en: 'Not Configured' },
nofxosDataSources: { zh: 'NofxOS 数据源', en: 'NofxOS Data Sources' },
}
return translations[key]?.[language] || key
}
// 获取当前选中的时间周期
const selectedTimeframes = config.klines.selected_timeframes || [config.klines.primary_timeframe] const selectedTimeframes = config.klines.selected_timeframes || [config.klines.primary_timeframe]
// 切换时间周期选择 // Toggle timeframe selection
const toggleTimeframe = (tf: string) => { const toggleTimeframe = (tf: string) => {
if (disabled) return if (disabled) return
const current = [...selectedTimeframes] const current = [...selectedTimeframes]
@@ -153,7 +72,7 @@ export function IndicatorEditor({
} }
} }
// 设置主时间周期 // Set primary timeframe
const setPrimaryTimeframe = (tf: string) => { const setPrimaryTimeframe = (tf: string) => {
if (disabled) return if (disabled) return
onChange({ onChange({
@@ -218,10 +137,10 @@ export function IndicatorEditor({
</div> </div>
<div> <div>
<h3 className="text-sm font-semibold" style={{ color: '#EAECEF' }}> <h3 className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{t('nofxosTitle')} {ts(indicator.nofxosTitle, language)}
</h3> </h3>
<span className="text-[10px]" style={{ color: '#848E9C' }}> <span className="text-[10px]" style={{ color: '#848E9C' }}>
{t('nofxosFeatures')} {ts(indicator.nofxosFeatures, language)}
</span> </span>
</div> </div>
</div> </div>
@@ -231,12 +150,12 @@ export function IndicatorEditor({
{hasApiKey ? ( {hasApiKey ? (
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}> <span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
<Check className="w-3 h-3" /> <Check className="w-3 h-3" />
{t('connected')} {ts(indicator.connected, language)}
</span> </span>
) : ( ) : (
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}> <span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}>
<AlertCircle className="w-3 h-3" /> <AlertCircle className="w-3 h-3" />
{t('notConfigured')} {ts(indicator.notConfigured, language)}
</span> </span>
)} )}
<a <a
@@ -250,7 +169,7 @@ export function IndicatorEditor({
}} }}
> >
<ExternalLink className="w-3 h-3" /> <ExternalLink className="w-3 h-3" />
{t('viewApiDocs')} {ts(indicator.viewApiDocs, language)}
</a> </a>
</div> </div>
</div> </div>
@@ -264,7 +183,7 @@ export function IndicatorEditor({
value={config.nofxos_api_key || ''} value={config.nofxos_api_key || ''}
onChange={(e) => !disabled && onChange({ ...config, nofxos_api_key: e.target.value })} onChange={(e) => !disabled && onChange({ ...config, nofxos_api_key: e.target.value })}
disabled={disabled} disabled={disabled}
placeholder={t('apiKeyPlaceholder')} placeholder={ts(indicator.apiKeyPlaceholder, language)}
className="w-full pl-9 pr-3 py-2 rounded-lg text-sm font-mono" className="w-full pl-9 pr-3 py-2 rounded-lg text-sm font-mono"
style={{ style={{
background: 'rgba(30, 35, 41, 0.8)', background: 'rgba(30, 35, 41, 0.8)',
@@ -283,7 +202,7 @@ export function IndicatorEditor({
color: '#fff', color: '#fff',
}} }}
> >
{t('fillDefault')} {ts(indicator.fillDefault, language)}
</button> </button>
)} )}
</div> </div>
@@ -291,7 +210,7 @@ export function IndicatorEditor({
{/* NofxOS Data Sources Grid */} {/* NofxOS Data Sources Grid */}
<div className="mt-4"> <div className="mt-4">
<div className="text-[10px] font-medium mb-2" style={{ color: '#848E9C' }}> <div className="text-[10px] font-medium mb-2" style={{ color: '#848E9C' }}>
{t('nofxosDataSources')} {ts(indicator.nofxosDataSources, language)}
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{/* Quant Data */} {/* Quant Data */}
@@ -307,7 +226,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ background: '#60a5fa' }} /> <div className="w-2 h-2 rounded-full" style={{ background: '#60a5fa' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span> <span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.quantData, language)}</span>
</div> </div>
<input <input
type="checkbox" type="checkbox"
@@ -317,7 +236,7 @@ export function IndicatorEditor({
className="w-3.5 h-3.5 rounded accent-blue-500" className="w-3.5 h-3.5 rounded accent-blue-500"
/> />
</div> </div>
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('quantDataDesc')}</p> <p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.quantDataDesc, language)}</p>
{config.enable_quant_data && ( {config.enable_quant_data && (
<div className="flex gap-3 mt-2"> <div className="flex gap-3 mt-2">
<label className="flex items-center gap-1.5 cursor-pointer"> <label className="flex items-center gap-1.5 cursor-pointer">
@@ -362,7 +281,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} /> <div className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('oiRanking')}</span> <span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.oiRanking, language)}</span>
</div> </div>
<input <input
type="checkbox" type="checkbox"
@@ -377,7 +296,7 @@ export function IndicatorEditor({
className="w-3.5 h-3.5 rounded accent-green-500" className="w-3.5 h-3.5 rounded accent-green-500"
/> />
</div> </div>
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('oiRankingDesc')}</p> <p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.oiRankingDesc, language)}</p>
{config.enable_oi_ranking && ( {config.enable_oi_ranking && (
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}> <div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
<select <select
@@ -422,7 +341,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ background: '#f59e0b' }} /> <div className="w-2 h-2 rounded-full" style={{ background: '#f59e0b' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('netflowRanking')}</span> <span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.netflowRanking, language)}</span>
</div> </div>
<input <input
type="checkbox" type="checkbox"
@@ -437,7 +356,7 @@ export function IndicatorEditor({
className="w-3.5 h-3.5 rounded accent-amber-500" className="w-3.5 h-3.5 rounded accent-amber-500"
/> />
</div> </div>
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('netflowRankingDesc')}</p> <p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.netflowRankingDesc, language)}</p>
{config.enable_netflow_ranking && ( {config.enable_netflow_ranking && (
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}> <div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
<select <select
@@ -482,7 +401,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ background: '#ec4899' }} /> <div className="w-2 h-2 rounded-full" style={{ background: '#ec4899' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('priceRanking')}</span> <span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.priceRanking, language)}</span>
</div> </div>
<input <input
type="checkbox" type="checkbox"
@@ -497,7 +416,7 @@ export function IndicatorEditor({
className="w-3.5 h-3.5 rounded accent-pink-500" className="w-3.5 h-3.5 rounded accent-pink-500"
/> />
</div> </div>
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('priceRankingDesc')}</p> <p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.priceRankingDesc, language)}</p>
{config.enable_price_ranking && ( {config.enable_price_ranking && (
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}> <div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
<select <select
@@ -510,7 +429,7 @@ export function IndicatorEditor({
<option value="1h">1h</option> <option value="1h">1h</option>
<option value="4h">4h</option> <option value="4h">4h</option>
<option value="24h">24h</option> <option value="24h">24h</option>
<option value="1h,4h,24h">{t('priceRankingMulti')}</option> <option value="1h,4h,24h">{ts(indicator.priceRankingMulti, language)}</option>
</select> </select>
<select <select
value={config.price_ranking_limit || 10} value={config.price_ranking_limit || 10}
@@ -531,7 +450,7 @@ export function IndicatorEditor({
<div className="flex items-center gap-2 mt-3 p-2 rounded-lg" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.2)' }}> <div className="flex items-center gap-2 mt-3 p-2 rounded-lg" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.2)' }}>
<AlertCircle className="w-4 h-4 flex-shrink-0" style={{ color: '#F6465D' }} /> <AlertCircle className="w-4 h-4 flex-shrink-0" style={{ color: '#F6465D' }} />
<span className="text-[10px]" style={{ color: '#F6465D' }}> <span className="text-[10px]" style={{ color: '#F6465D' }}>
{language === 'zh' ? '请配置 API Key 以启用 NofxOS 数据源' : 'Please configure API Key to enable NofxOS data sources'} {ts(indicator.configureApiKey, language)}
</span> </span>
</div> </div>
)} )}
@@ -545,8 +464,8 @@ export function IndicatorEditor({
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}> <div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}> <div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
<BarChart2 className="w-4 h-4" style={{ color: '#F0B90B' }} /> <BarChart2 className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('marketData')}</span> <span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.marketData, language)}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('marketDataDesc')}</span> <span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.marketDataDesc, language)}</span>
</div> </div>
<div className="p-3 space-y-4"> <div className="p-3 space-y-4">
@@ -558,13 +477,13 @@ export function IndicatorEditor({
</div> </div>
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('rawKlines')}</span> <span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.rawKlines, language)}</span>
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium flex items-center gap-1" style={{ background: 'rgba(240, 185, 11, 0.2)', color: '#F0B90B' }}> <span className="px-1.5 py-0.5 rounded text-[10px] font-medium flex items-center gap-1" style={{ background: 'rgba(240, 185, 11, 0.2)', color: '#F0B90B' }}>
<Lock className="w-2.5 h-2.5" /> <Lock className="w-2.5 h-2.5" />
{t('required')} {ts(indicator.required, language)}
</span> </span>
</div> </div>
<p className="text-xs mt-0.5" style={{ color: '#848E9C' }}>{t('rawKlinesDesc')}</p> <p className="text-xs mt-0.5" style={{ color: '#848E9C' }}>{ts(indicator.rawKlinesDesc, language)}</p>
</div> </div>
</div> </div>
<input <input
@@ -580,10 +499,10 @@ export function IndicatorEditor({
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className="w-3.5 h-3.5" style={{ color: '#848E9C' }} /> <Clock className="w-3.5 h-3.5" style={{ color: '#848E9C' }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('timeframes')}</span> <span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.timeframes, language)}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[10px]" style={{ color: '#848E9C' }}>{t('klineCount')}:</span> <span className="text-[10px]" style={{ color: '#848E9C' }}>{ts(indicator.klineCount, language)}:</span>
<input <input
type="number" type="number"
value={config.klines.primary_count} value={config.klines.primary_count}
@@ -602,7 +521,7 @@ export function IndicatorEditor({
/> />
</div> </div>
</div> </div>
<p className="text-[10px] mb-2" style={{ color: '#5E6673' }}>{t('timeframesDesc')}</p> <p className="text-[10px] mb-2" style={{ color: '#5E6673' }}>{ts(indicator.timeframesDesc, language)}</p>
{/* Timeframe Grid */} {/* Timeframe Grid */}
<div className="space-y-1.5"> <div className="space-y-1.5">
@@ -611,7 +530,7 @@ export function IndicatorEditor({
return ( return (
<div key={category} className="flex items-center gap-2"> <div key={category} className="flex items-center gap-2">
<span className="text-[10px] w-10 flex-shrink-0" style={{ color: categoryColors[category] }}> <span className="text-[10px] w-10 flex-shrink-0" style={{ color: categoryColors[category] }}>
{t(category)} {ts(indicator[category], language)}
</span> </span>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{categoryTfs.map((tf) => { {categoryTfs.map((tf) => {
@@ -654,15 +573,15 @@ export function IndicatorEditor({
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}> <div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}> <div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} /> <Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('technicalIndicators')}</span> <span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.technicalIndicators, language)}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('technicalIndicatorsDesc')}</span> <span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.technicalIndicatorsDesc, language)}</span>
</div> </div>
<div className="p-3"> <div className="p-3">
{/* Tip */} {/* Tip */}
<div className="flex items-start gap-2 mb-3 p-2 rounded" style={{ background: 'rgba(14, 203, 129, 0.05)' }}> <div className="flex items-start gap-2 mb-3 p-2 rounded" style={{ background: 'rgba(14, 203, 129, 0.05)' }}>
<Info className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" style={{ color: '#0ECB81' }} /> <Info className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" style={{ color: '#0ECB81' }} />
<p className="text-[10px]" style={{ color: '#848E9C' }}>{t('aiCanCalculate')}</p> <p className="text-[10px]" style={{ color: '#848E9C' }}>{ts(indicator.aiCanCalculate, language)}</p>
</div> </div>
{/* Indicator Grid */} {/* Indicator Grid */}
@@ -685,7 +604,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ background: color }} /> <div className="w-2 h-2 rounded-full" style={{ background: color }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t(label)}</span> <span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator[label as keyof typeof indicator], language)}</span>
</div> </div>
<input <input
type="checkbox" type="checkbox"
@@ -695,7 +614,7 @@ export function IndicatorEditor({
className="w-4 h-4 rounded accent-yellow-500" className="w-4 h-4 rounded accent-yellow-500"
/> />
</div> </div>
<p className="text-[10px] mb-1.5" style={{ color: '#5E6673' }}>{t(desc)}</p> <p className="text-[10px] mb-1.5" style={{ color: '#5E6673' }}>{ts(indicator[desc as keyof typeof indicator], language)}</p>
{periodKey && config[key as keyof IndicatorConfig] && ( {periodKey && config[key as keyof IndicatorConfig] && (
<input <input
type="text" type="text"
@@ -726,8 +645,8 @@ export function IndicatorEditor({
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}> <div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}> <div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
<TrendingUp className="w-4 h-4" style={{ color: '#22c55e' }} /> <TrendingUp className="w-4 h-4" style={{ color: '#22c55e' }} />
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('marketSentiment')}</span> <span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.marketSentiment, language)}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('marketSentimentDesc')}</span> <span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.marketSentimentDesc, language)}</span>
</div> </div>
<div className="p-3"> <div className="p-3">
@@ -748,7 +667,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ background: color }} /> <div className="w-2 h-2 rounded-full" style={{ background: color }} />
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t(label)}</span> <span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator[label as keyof typeof indicator], language)}</span>
</div> </div>
<input <input
type="checkbox" type="checkbox"
@@ -758,7 +677,7 @@ export function IndicatorEditor({
className="w-4 h-4 rounded accent-yellow-500" className="w-4 h-4 rounded accent-yellow-500"
/> />
</div> </div>
<p className="text-[10px]" style={{ color: '#5E6673' }}>{t(desc)}</p> <p className="text-[10px]" style={{ color: '#5E6673' }}>{ts(indicator[desc as keyof typeof indicator], language)}</p>
</div> </div>
))} ))}
</div> </div>
@@ -1,6 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import { ChevronDown, ChevronRight, RotateCcw, FileText } from 'lucide-react' import { ChevronDown, ChevronRight, RotateCcw, FileText } from 'lucide-react'
import type { PromptSectionsConfig } from '../../types' import type { PromptSectionsConfig } from '../../types'
import { promptSections as promptSectionsI18n, ts } from '../../i18n/strategy-translations'
interface PromptSectionsEditorProps { interface PromptSectionsEditorProps {
config: PromptSectionsConfig | undefined config: PromptSectionsConfig | undefined
@@ -54,29 +55,11 @@ export function PromptSectionsEditor({
decision_process: false, decision_process: false,
}) })
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
promptSections: { zh: 'System Prompt 自定义', en: 'System Prompt Customization' },
promptSectionsDesc: { zh: '自定义 AI 行为和决策逻辑(输出格式和风控规则不可修改)', en: 'Customize AI behavior and decision logic (output format and risk rules are fixed)' },
roleDefinition: { zh: '角色定义', en: 'Role Definition' },
roleDefinitionDesc: { zh: '定义 AI 的身份和核心目标', en: 'Define AI identity and core objectives' },
tradingFrequency: { zh: '交易频率', en: 'Trading Frequency' },
tradingFrequencyDesc: { zh: '设定交易频率预期和过度交易警告', en: 'Set trading frequency expectations and overtrading warnings' },
entryStandards: { zh: '开仓标准', en: 'Entry Standards' },
entryStandardsDesc: { zh: '定义开仓信号条件和避免事项', en: 'Define entry signal conditions and avoidances' },
decisionProcess: { zh: '决策流程', en: 'Decision Process' },
decisionProcessDesc: { zh: '设定决策步骤和思考流程', en: 'Set decision steps and thinking process' },
resetToDefault: { zh: '重置为默认', en: 'Reset to Default' },
chars: { zh: '字符', en: 'chars' },
}
return translations[key]?.[language] || key
}
const sections = [ const sections = [
{ key: 'role_definition', label: t('roleDefinition'), desc: t('roleDefinitionDesc') }, { key: 'role_definition', label: ts(promptSectionsI18n.roleDefinition, language), desc: ts(promptSectionsI18n.roleDefinitionDesc, language) },
{ key: 'trading_frequency', label: t('tradingFrequency'), desc: t('tradingFrequencyDesc') }, { key: 'trading_frequency', label: ts(promptSectionsI18n.tradingFrequency, language), desc: ts(promptSectionsI18n.tradingFrequencyDesc, language) },
{ key: 'entry_standards', label: t('entryStandards'), desc: t('entryStandardsDesc') }, { key: 'entry_standards', label: ts(promptSectionsI18n.entryStandards, language), desc: ts(promptSectionsI18n.entryStandardsDesc, language) },
{ key: 'decision_process', label: t('decisionProcess'), desc: t('decisionProcessDesc') }, { key: 'decision_process', label: ts(promptSectionsI18n.decisionProcess, language), desc: ts(promptSectionsI18n.decisionProcessDesc, language) },
] ]
const currentConfig = config || {} const currentConfig = config || {}
@@ -107,10 +90,10 @@ export function PromptSectionsEditor({
<FileText className="w-5 h-5 mt-0.5" style={{ color: '#a855f7' }} /> <FileText className="w-5 h-5 mt-0.5" style={{ color: '#a855f7' }} />
<div> <div>
<h3 className="font-medium" style={{ color: '#EAECEF' }}> <h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('promptSections')} {ts(promptSectionsI18n.promptSections, language)}
</h3> </h3>
<p className="text-xs mt-1" style={{ color: '#848E9C' }}> <p className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('promptSectionsDesc')} {ts(promptSectionsI18n.promptSectionsDesc, language)}
</p> </p>
</div> </div>
</div> </div>
@@ -146,12 +129,12 @@ export function PromptSectionsEditor({
className="px-1.5 py-0.5 text-[10px] rounded" className="px-1.5 py-0.5 text-[10px] rounded"
style={{ background: 'rgba(168, 85, 247, 0.15)', color: '#a855f7' }} style={{ background: 'rgba(168, 85, 247, 0.15)', color: '#a855f7' }}
> >
{language === 'zh' ? '已修改' : 'Modified'} {ts(promptSectionsI18n.modified, language)}
</span> </span>
)} )}
</div> </div>
<span className="text-[10px]" style={{ color: '#848E9C' }}> <span className="text-[10px]" style={{ color: '#848E9C' }}>
{value.length} {t('chars')} {value.length} {ts(promptSectionsI18n.chars, language)}
</span> </span>
</button> </button>
@@ -181,7 +164,7 @@ export function PromptSectionsEditor({
style={{ color: '#848E9C' }} style={{ color: '#848E9C' }}
> >
<RotateCcw className="w-3 h-3" /> <RotateCcw className="w-3 h-3" />
{t('resetToDefault')} {ts(promptSectionsI18n.resetToDefault, language)}
</button> </button>
</div> </div>
</div> </div>
@@ -1,4 +1,5 @@
import { Globe, Lock, Eye, EyeOff } from 'lucide-react' import { Globe, Lock, Eye, EyeOff } from 'lucide-react'
import { publishSettings, ts } from '../../i18n/strategy-translations'
interface PublishSettingsEditorProps { interface PublishSettingsEditorProps {
isPublic: boolean isPublic: boolean
@@ -17,23 +18,9 @@ export function PublishSettingsEditor({
disabled = false, disabled = false,
language, language,
}: PublishSettingsEditorProps) { }: PublishSettingsEditorProps) {
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
publishToMarket: { zh: '发布到策略市场', en: 'Publish to Market' },
publishDesc: { zh: '策略将在市场公开展示,其他用户可发现并使用', en: 'Strategy will be publicly visible in the marketplace' },
showConfig: { zh: '公开配置参数', en: 'Show Config' },
showConfigDesc: { zh: '允许他人查看和复制详细配置', en: 'Allow others to view and clone config details' },
private: { zh: '私有', en: 'PRIVATE' },
public: { zh: '公开', en: 'PUBLIC' },
hidden: { zh: '隐藏', en: 'HIDDEN' },
visible: { zh: '可见', en: 'VISIBLE' },
}
return translations[key]?.[language] || key
}
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{/* 发布开关 */} {/* Publish toggle */}
<div <div
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`} className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
style={{ style={{
@@ -73,10 +60,10 @@ export function PublishSettingsEditor({
</div> </div>
<div> <div>
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}> <div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
{t('publishToMarket')} {ts(publishSettings.publishToMarket, language)}
</div> </div>
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}> <div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
{t('publishDesc')} {ts(publishSettings.publishDesc, language)}
</div> </div>
</div> </div>
</div> </div>
@@ -87,7 +74,7 @@ export function PublishSettingsEditor({
className="text-[10px] font-mono font-bold tracking-wider" className="text-[10px] font-mono font-bold tracking-wider"
style={{ color: isPublic ? '#0ECB81' : '#848E9C' }} style={{ color: isPublic ? '#0ECB81' : '#848E9C' }}
> >
{isPublic ? t('public') : t('private')} {isPublic ? ts(publishSettings.public, language) : ts(publishSettings.private, language)}
</span> </span>
<div <div
className="relative w-12 h-6 rounded-full transition-all duration-300" className="relative w-12 h-6 rounded-full transition-all duration-300"
@@ -111,7 +98,7 @@ export function PublishSettingsEditor({
</div> </div>
</div> </div>
{/* 配置可见性开关 - 仅在公开时显示 */} {/* Config visibility toggle - only shown when public */}
{isPublic && ( {isPublic && (
<div <div
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`} className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
@@ -152,10 +139,10 @@ export function PublishSettingsEditor({
</div> </div>
<div> <div>
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}> <div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
{t('showConfig')} {ts(publishSettings.showConfig, language)}
</div> </div>
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}> <div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
{t('showConfigDesc')} {ts(publishSettings.showConfigDesc, language)}
</div> </div>
</div> </div>
</div> </div>
@@ -166,7 +153,7 @@ export function PublishSettingsEditor({
className="text-[10px] font-mono font-bold tracking-wider" className="text-[10px] font-mono font-bold tracking-wider"
style={{ color: configVisible ? '#a855f7' : '#848E9C' }} style={{ color: configVisible ? '#a855f7' : '#848E9C' }}
> >
{configVisible ? t('visible') : t('hidden')} {configVisible ? ts(publishSettings.visible, language) : ts(publishSettings.hidden, language)}
</span> </span>
<div <div
className="relative w-12 h-6 rounded-full transition-all duration-300" className="relative w-12 h-6 rounded-full transition-all duration-300"
@@ -1,5 +1,6 @@
import { Shield, AlertTriangle } from 'lucide-react' import { Shield, AlertTriangle } from 'lucide-react'
import type { RiskControlConfig } from '../../types' import type { RiskControlConfig } from '../../types'
import { riskControl, ts } from '../../i18n/strategy-translations'
interface RiskControlEditorProps { interface RiskControlEditorProps {
config: RiskControlConfig config: RiskControlConfig
@@ -14,38 +15,6 @@ export function RiskControlEditor({
disabled, disabled,
language, language,
}: RiskControlEditorProps) { }: RiskControlEditorProps) {
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
positionLimits: { zh: '仓位限制', en: 'Position Limits' },
maxPositions: { zh: '最大持仓数量', en: 'Max Positions' },
maxPositionsDesc: { zh: '同时持有的最大币种数量', en: 'Maximum coins held simultaneously' },
// Trading leverage (exchange leverage)
tradingLeverage: { zh: '交易杠杆(交易所杠杆)', en: 'Trading Leverage (Exchange)' },
btcEthLeverage: { zh: 'BTC/ETH 交易杠杆', en: 'BTC/ETH Trading Leverage' },
btcEthLeverageDesc: { zh: '交易所开仓使用的杠杆倍数', en: 'Exchange leverage for opening positions' },
altcoinLeverage: { zh: '山寨币交易杠杆', en: 'Altcoin Trading Leverage' },
altcoinLeverageDesc: { zh: '交易所开仓使用的杠杆倍数', en: 'Exchange leverage for opening positions' },
// Position value ratio (risk control) - CODE ENFORCED
positionValueRatio: { zh: '仓位价值比例(代码强制)', en: 'Position Value Ratio (CODE ENFORCED)' },
positionValueRatioDesc: { zh: '单仓位名义价值 / 账户净值,由代码强制执行', en: 'Position notional value / equity, enforced by code' },
btcEthPositionValueRatio: { zh: 'BTC/ETH 仓位价值比例', en: 'BTC/ETH Position Value Ratio' },
btcEthPositionValueRatioDesc: { zh: '单仓最大名义价值 = 净值 × 此值(代码强制)', en: 'Max position value = equity × this ratio (CODE ENFORCED)' },
altcoinPositionValueRatio: { zh: '山寨币仓位价值比例', en: 'Altcoin Position Value Ratio' },
altcoinPositionValueRatioDesc: { zh: '单仓最大名义价值 = 净值 × 此值(代码强制)', en: 'Max position value = equity × this ratio (CODE ENFORCED)' },
riskParameters: { zh: '风险参数', en: 'Risk Parameters' },
minRiskReward: { zh: '最小风险回报比', en: 'Min Risk/Reward Ratio' },
minRiskRewardDesc: { zh: '开仓要求的最低盈亏比', en: 'Minimum profit ratio for opening' },
maxMarginUsage: { zh: '最大保证金使用率(代码强制)', en: 'Max Margin Usage (CODE ENFORCED)' },
maxMarginUsageDesc: { zh: '保证金使用率上限,由代码强制执行', en: 'Maximum margin utilization, enforced by code' },
entryRequirements: { zh: '开仓要求', en: 'Entry Requirements' },
minPositionSize: { zh: '最小开仓金额', en: 'Min Position Size' },
minPositionSizeDesc: { zh: 'USDT 最小名义价值', en: 'Minimum notional value in USDT' },
minConfidence: { zh: '最小信心度', en: 'Min Confidence' },
minConfidenceDesc: { zh: 'AI 开仓信心度阈值', en: 'AI confidence threshold for entry' },
}
return translations[key]?.[language] || key
}
const updateField = <K extends keyof RiskControlConfig>( const updateField = <K extends keyof RiskControlConfig>(
key: K, key: K,
value: RiskControlConfig[K] value: RiskControlConfig[K]
@@ -62,7 +31,7 @@ export function RiskControlEditor({
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} /> <Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}> <h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('positionLimits')} {ts(riskControl.positionLimits, language)}
</h3> </h3>
</div> </div>
@@ -72,10 +41,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }} style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
> >
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxPositions')} {ts(riskControl.maxPositions, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxPositionsDesc')} {ts(riskControl.maxPositionsDesc, language)}
</p> </p>
<input <input
type="number" type="number"
@@ -99,7 +68,7 @@ export function RiskControlEditor({
{/* Trading Leverage (Exchange) */} {/* Trading Leverage (Exchange) */}
<div className="mb-2"> <div className="mb-2">
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}> <p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
{t('tradingLeverage')} {ts(riskControl.tradingLeverage, language)}
</p> </p>
</div> </div>
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-2 gap-4 mb-4">
@@ -108,10 +77,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }} style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
> >
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('btcEthLeverage')} {ts(riskControl.btcEthLeverage, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('btcEthLeverageDesc')} {ts(riskControl.btcEthLeverageDesc, language)}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -139,10 +108,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }} style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
> >
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('altcoinLeverage')} {ts(riskControl.altcoinLeverage, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('altcoinLeverageDesc')} {ts(riskControl.altcoinLeverageDesc, language)}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -169,10 +138,10 @@ export function RiskControlEditor({
{/* Position Value Ratio (Risk Control - CODE ENFORCED) */} {/* Position Value Ratio (Risk Control - CODE ENFORCED) */}
<div className="mb-2"> <div className="mb-2">
<p className="text-xs font-medium" style={{ color: '#0ECB81' }}> <p className="text-xs font-medium" style={{ color: '#0ECB81' }}>
{t('positionValueRatio')} {ts(riskControl.positionValueRatio, language)}
</p> </p>
<p className="text-xs mt-1" style={{ color: '#848E9C' }}> <p className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('positionValueRatioDesc')} {ts(riskControl.positionValueRatioDesc, language)}
</p> </p>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -181,10 +150,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }} style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
> >
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('btcEthPositionValueRatio')} {ts(riskControl.btcEthPositionValueRatio, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('btcEthPositionValueRatioDesc')} {ts(riskControl.btcEthPositionValueRatioDesc, language)}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -213,10 +182,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }} style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
> >
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('altcoinPositionValueRatio')} {ts(riskControl.altcoinPositionValueRatio, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('altcoinPositionValueRatioDesc')} {ts(riskControl.altcoinPositionValueRatioDesc, language)}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -247,7 +216,7 @@ export function RiskControlEditor({
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<AlertTriangle className="w-5 h-5" style={{ color: '#F6465D' }} /> <AlertTriangle className="w-5 h-5" style={{ color: '#F6465D' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}> <h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('riskParameters')} {ts(riskControl.riskParameters, language)}
</h3> </h3>
</div> </div>
@@ -257,10 +226,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }} style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
> >
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minRiskReward')} {ts(riskControl.minRiskReward, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minRiskRewardDesc')} {ts(riskControl.minRiskRewardDesc, language)}
</p> </p>
<div className="flex items-center"> <div className="flex items-center">
<span style={{ color: '#848E9C' }}>1:</span> <span style={{ color: '#848E9C' }}>1:</span>
@@ -289,10 +258,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }} style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
> >
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxMarginUsage')} {ts(riskControl.maxMarginUsage, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxMarginUsageDesc')} {ts(riskControl.maxMarginUsageDesc, language)}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -319,7 +288,7 @@ export function RiskControlEditor({
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Shield className="w-5 h-5" style={{ color: '#0ECB81' }} /> <Shield className="w-5 h-5" style={{ color: '#0ECB81' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}> <h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('entryRequirements')} {ts(riskControl.entryRequirements, language)}
</h3> </h3>
</div> </div>
@@ -329,10 +298,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }} style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
> >
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minPositionSize')} {ts(riskControl.minPositionSize, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minPositionSizeDesc')} {ts(riskControl.minPositionSizeDesc, language)}
</p> </p>
<div className="flex items-center"> <div className="flex items-center">
<input <input
@@ -362,10 +331,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }} style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
> >
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}> <label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minConfidence')} {ts(riskControl.minConfidence, language)}
</label> </label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minConfidenceDesc')} {ts(riskControl.minConfidenceDesc, language)}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
+33 -33
View File
@@ -212,9 +212,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
await toast.promise(api.createTrader(data), { await toast.promise(api.createTrader(data), {
loading: '正在创建…', loading: t('aiTradersToast.creating', language),
success: '创建成功', success: t('aiTradersToast.created', language),
error: '创建失败', error: t('aiTradersToast.createFailed', language),
}) })
setShowCreateModal(false) setShowCreateModal(false)
await mutateTraders() await mutateTraders()
@@ -269,9 +269,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
console.log('🔥 handleSaveEditTrader - request:', request) console.log('🔥 handleSaveEditTrader - request:', request)
await toast.promise(api.updateTrader(editingTrader.trader_id, request), { await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
loading: '正在保存…', loading: t('aiTradersToast.saving', language),
success: '保存成功', success: t('aiTradersToast.saved', language),
error: '保存失败', error: t('aiTradersToast.saveFailed', language),
}) })
setShowEditModal(false) setShowEditModal(false)
setEditingTrader(null) setEditingTrader(null)
@@ -290,9 +290,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
try { try {
await toast.promise(api.deleteTrader(traderId), { await toast.promise(api.deleteTrader(traderId), {
loading: '正在删除…', loading: t('aiTradersToast.deleting', language),
success: '删除成功', success: t('aiTradersToast.deleted', language),
error: '删除失败', error: t('aiTradersToast.deleteFailed', language),
}) })
await mutateTraders() await mutateTraders()
@@ -306,15 +306,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
try { try {
if (running) { if (running) {
await toast.promise(api.stopTrader(traderId), { await toast.promise(api.stopTrader(traderId), {
loading: '正在停止…', loading: t('aiTradersToast.stopping', language),
success: '已停止', success: t('aiTradersToast.stopped', language),
error: '停止失败', error: t('aiTradersToast.stopFailed', language),
}) })
} else { } else {
await toast.promise(api.startTrader(traderId), { await toast.promise(api.startTrader(traderId), {
loading: '正在启动…', loading: t('aiTradersToast.starting', language),
success: '已启动', success: t('aiTradersToast.started', language),
error: '启动失败', error: t('aiTradersToast.startFailed', language),
}) })
} }
@@ -329,9 +329,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
try { try {
const newValue = !currentShowInCompetition const newValue = !currentShowInCompetition
await toast.promise(api.toggleCompetition(traderId, newValue), { await toast.promise(api.toggleCompetition(traderId, newValue), {
loading: '正在更新…', loading: t('aiTradersToast.updating', language),
success: newValue ? '已在竞技场显示' : '已在竞技场隐藏', success: newValue ? t('aiTradersToast.showInCompetition', language) : t('aiTradersToast.hideInCompetition', language),
error: '更新失败', error: t('aiTradersToast.updateFailed', language),
}) })
await mutateTraders() await mutateTraders()
@@ -393,9 +393,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const request = config.buildRequest(updatedItems) const request = config.buildRequest(updatedItems)
await toast.promise(config.updateApi(request), { await toast.promise(config.updateApi(request), {
loading: '正在更新配置…', loading: t('aiTradersToast.updatingConfig', language),
success: '配置已更新', success: t('aiTradersToast.configUpdated', language),
error: '更新配置失败', error: t('aiTradersToast.configUpdateFailed', language),
}) })
const refreshedItems = await config.refreshApi() const refreshedItems = await config.refreshApi()
@@ -506,9 +506,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
await toast.promise(api.updateModelConfigs(request), { await toast.promise(api.updateModelConfigs(request), {
loading: '正在更新模型配置…', loading: t('aiTradersToast.updatingModelConfig', language),
success: '模型配置已更新', success: t('aiTradersToast.modelConfigUpdated', language),
error: '更新模型配置失败', error: t('aiTradersToast.modelConfigUpdateFailed', language),
}) })
const refreshedModels = await api.getModelConfigs() const refreshedModels = await api.getModelConfigs()
@@ -536,9 +536,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
try { try {
await toast.promise(api.deleteExchange(exchangeId), { await toast.promise(api.deleteExchange(exchangeId), {
loading: language === 'zh' ? '正在删除交易所账户…' : 'Deleting exchange account...', loading: t('aiTradersToast.deletingExchange', language),
success: language === 'zh' ? '交易所账户已删除' : 'Exchange account deleted', success: t('aiTradersToast.exchangeDeleted', language),
error: language === 'zh' ? '删除交易所账户失败' : 'Failed to delete exchange account', error: t('aiTradersToast.exchangeDeleteFailed', language),
}) })
const refreshedExchanges = await api.getExchangeConfigs() const refreshedExchanges = await api.getExchangeConfigs()
@@ -598,9 +598,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
await toast.promise(api.updateExchangeConfigsEncrypted(request), { await toast.promise(api.updateExchangeConfigsEncrypted(request), {
loading: language === 'zh' ? '正在更新交易所配置…' : 'Updating exchange config...', loading: t('aiTradersToast.updatingExchangeConfig', language),
success: language === 'zh' ? '交易所配置已更新' : 'Exchange config updated', success: t('aiTradersToast.exchangeConfigUpdated', language),
error: language === 'zh' ? '更新交易所配置失败' : 'Failed to update exchange config', error: t('aiTradersToast.exchangeConfigUpdateFailed', language),
}) })
} else { } else {
const createRequest = { const createRequest = {
@@ -622,9 +622,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
await toast.promise(api.createExchangeEncrypted(createRequest), { await toast.promise(api.createExchangeEncrypted(createRequest), {
loading: language === 'zh' ? '正在创建交易所账户…' : 'Creating exchange account...', loading: t('aiTradersToast.creatingExchange', language),
success: language === 'zh' ? '交易所账户已创建' : 'Exchange account created', success: t('aiTradersToast.exchangeCreated', language),
error: language === 'zh' ? '创建交易所账户失败' : 'Failed to create exchange account', error: t('aiTradersToast.exchangeCreateFailed', language),
}) })
} }
@@ -258,7 +258,7 @@ export function ExchangeConfigModal({
toast.success(t('ipCopied', language)) toast.success(t('ipCopied', language))
} }
} catch { } catch {
toast.error(t('copyIPFailed', language) || `复制失败: ${ip}`) toast.error(t('copyIPFailed', language))
} }
} }
@@ -305,7 +305,7 @@ export function ExchangeConfigModal({
const trimmedAccountName = accountName.trim() const trimmedAccountName = accountName.trim()
if (!trimmedAccountName) { if (!trimmedAccountName) {
toast.error(language === 'zh' ? '请输入账户名称' : 'Please enter account name') toast.error(t('exchangeConfig.pleaseEnterAccountName', language))
return return
} }
@@ -338,7 +338,7 @@ export function ExchangeConfigModal({
} }
} }
const stepLabels = language === 'zh' ? ['选择交易所', '配置账户'] : ['Select Exchange', 'Configure'] const stepLabels = [t('exchangeConfig.selectExchange', language), t('exchangeConfig.configure', language)]
const cexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'cex') const cexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'cex')
const dexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'dex') const dexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'dex')
@@ -412,13 +412,13 @@ export function ExchangeConfigModal({
{/* Exchange Grid */} {/* Exchange Grid */}
<div className="space-y-4"> <div className="space-y-4">
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}> <div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '选择您的交易所' : 'Choose Your Exchange'} {t('exchangeConfig.chooseExchange', language)}
</div> </div>
{/* CEX */} {/* CEX */}
<div className="space-y-3"> <div className="space-y-3">
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#F0B90B' }}> <div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#F0B90B' }}>
{language === 'zh' ? '中心化交易所 (CEX)' : 'Centralized Exchanges'} {t('exchangeConfig.centralizedExchanges', language)}
</div> </div>
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3"> <div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
{cexExchanges.map((template) => ( {cexExchanges.map((template) => (
@@ -436,7 +436,7 @@ export function ExchangeConfigModal({
{/* DEX */} {/* DEX */}
<div className="space-y-3"> <div className="space-y-3">
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#A78BFA' }}> <div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#A78BFA' }}>
{language === 'zh' ? '去中心化交易所 (DEX)' : 'Decentralized Exchanges'} {t('exchangeConfig.decentralizedExchanges', language)}
</div> </div>
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3"> <div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
{dexExchanges.map((template) => ( {dexExchanges.map((template) => (
@@ -477,11 +477,11 @@ export function ExchangeConfigModal({
> >
<UserPlus className="w-4 h-4" style={{ color: '#F0B90B' }} /> <UserPlus className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium" style={{ color: '#F0B90B' }}> <span className="text-sm font-medium" style={{ color: '#F0B90B' }}>
{language === 'zh' ? '注册' : 'Register'} {t('exchangeConfig.register', language)}
</span> </span>
{exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && ( {exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && (
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(14, 203, 129, 0.2)', color: '#0ECB81' }}> <span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(14, 203, 129, 0.2)', color: '#0ECB81' }}>
{language === 'zh' ? '优惠' : 'Bonus'} {t('exchangeConfig.bonus', language)}
</span> </span>
)} )}
</a> </a>
@@ -491,13 +491,13 @@ export function ExchangeConfigModal({
<div className="space-y-2"> <div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}> <label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<Key className="w-4 h-4" style={{ color: '#F0B90B' }} /> <Key className="w-4 h-4" style={{ color: '#F0B90B' }} />
{language === 'zh' ? '账户名称' : 'Account Name'} * {t('exchangeConfig.accountName', language)} *
</label> </label>
<input <input
type="text" type="text"
value={accountName} value={accountName}
onChange={(e) => setAccountName(e.target.value)} onChange={(e) => setAccountName(e.target.value)}
placeholder={language === 'zh' ? '例如:主账户、套利账户' : 'e.g., Main Account'} placeholder={t('exchangeConfig.accountNamePlaceholder', language)}
className="w-full px-4 py-3 rounded-xl text-base" className="w-full px-4 py-3 rounded-xl text-base"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required required
@@ -517,7 +517,7 @@ export function ExchangeConfigModal({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span style={{ color: '#58a6ff' }}></span> <span style={{ color: '#58a6ff' }}></span>
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}> <span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '币安用户必读:使用「现货与合约交易」API' : 'Use "Spot & Futures Trading" API'} {t('exchangeConfig.useBinanceFuturesApi', language)}
</span> </span>
</div> </div>
<span style={{ color: '#8b949e' }}>{showBinanceGuide ? '▲' : '▼'}</span> <span style={{ color: '#8b949e' }}>{showBinanceGuide ? '▲' : '▼'}</span>
@@ -532,7 +532,7 @@ export function ExchangeConfigModal({
style={{ color: '#58a6ff' }} style={{ color: '#58a6ff' }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{language === 'zh' ? '查看官方教程' : 'View Tutorial'} <ExternalLink className="w-3 h-3" /> {t('exchangeConfig.viewTutorial', language)} <ExternalLink className="w-3 h-3" />
</a> </a>
</div> </div>
)} )}
@@ -696,10 +696,10 @@ export function ExchangeConfigModal({
<span style={{ fontSize: '16px' }}>🔐</span> <span style={{ fontSize: '16px' }}>🔐</span>
<div> <div>
<div className="text-sm font-semibold mb-1" style={{ color: '#3B82F6' }}> <div className="text-sm font-semibold mb-1" style={{ color: '#3B82F6' }}>
{language === 'zh' ? 'Lighter API Key 配置' : 'Lighter API Key Setup'} {t('exchangeConfig.lighterApiKeySetup', language)}
</div> </div>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '请在 Lighter 网站生成 API Key' : 'Generate an API Key on Lighter website'} {t('exchangeConfig.lighterApiKeyDesc', language)}
</div> </div>
</div> </div>
</div> </div>
@@ -717,8 +717,8 @@ export function ExchangeConfigModal({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}> <label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
{language === 'zh' ? 'API Key 索引' : 'API Key Index'} {t('exchangeConfig.apiKeyIndex', language)}
<Tooltip content={language === 'zh' ? 'API Key 索引从0开始' : 'API Key index starts from 0'}> <Tooltip content={t('exchangeConfig.apiKeyIndexTooltip', language)}>
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#3B82F6' }} /> <HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#3B82F6' }} />
</Tooltip> </Tooltip>
</label> </label>
@@ -730,7 +730,7 @@ export function ExchangeConfigModal({
{/* Buttons */} {/* Buttons */}
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button type="button" onClick={handleBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}> <button type="button" onClick={handleBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
{editingExchangeId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')} {editingExchangeId ? t('cancel', language) : t('exchangeConfig.back', language)}
</button> </button>
<button <button
type="submit" type="submit"
@@ -738,7 +738,7 @@ export function ExchangeConfigModal({
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed" className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
style={{ background: '#F0B90B', color: '#000' }} style={{ background: '#F0B90B', color: '#000' }}
> >
{isSaving ? (t('saving', language) || '保存中...') : ( {isSaving ? t('saving', language) : (
<>{t('saveConfig', language)} <ArrowRight className="w-4 h-4" /></> <>{t('saveConfig', language)} <ArrowRight className="w-4 h-4" /></>
)} )}
</button> </button>
+26 -40
View File
@@ -79,7 +79,7 @@ export function ModelConfigModal({
const availableModels = allModels || [] const availableModels = allModels || []
const configuredIds = new Set(configuredModels?.map(m => m.id) || []) const configuredIds = new Set(configuredModels?.map(m => m.id) || [])
const stepLabels = language === 'zh' ? ['选择模型', '配置 API'] : ['Select Model', 'Configure API'] const stepLabels = [t('modelConfig.selectModel', language), t('modelConfig.configureApi', language)]
return ( return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm"> <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
@@ -192,7 +192,7 @@ function ModelSelectionStep({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}> <div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '选择 AI 模型提供商' : 'Choose Your AI Provider'} {t('modelConfig.chooseProvider', language)}
</div> </div>
{/* Claw402 Featured Card */} {/* Claw402 Featured Card */}
@@ -217,9 +217,7 @@ function ModelSelectionStep({
<a href="https://claw402.ai" target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="ml-1.5 text-[10px] font-normal px-1.5 py-0.5 rounded" style={{ color: '#60A5FA', background: 'rgba(96, 165, 250, 0.1)' }}> claw402.ai</a> <a href="https://claw402.ai" target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="ml-1.5 text-[10px] font-normal px-1.5 py-0.5 rounded" style={{ color: '#60A5FA', background: 'rgba(96, 165, 250, 0.1)' }}> claw402.ai</a>
</div> </div>
<div className="text-xs mt-0.5" style={{ color: '#A0AEC0' }}> <div className="text-xs mt-0.5" style={{ color: '#A0AEC0' }}>
{language === 'zh' {t('modelConfig.payPerCall', language)}
? 'USDC 按次付费 · 支持全部 AI 模型 · 无需 API Key'
: 'Pay-per-call USDC · All AI Models · No API Key'}
</div> </div>
</div> </div>
</div> </div>
@@ -228,7 +226,7 @@ function ModelSelectionStep({
<div className="w-2 h-2 rounded-full" style={{ background: '#00E096' }} /> <div className="w-2 h-2 rounded-full" style={{ background: '#00E096' }} />
)} )}
<div className="px-3 py-1.5 rounded-full text-xs font-bold" style={{ background: 'linear-gradient(135deg, #2563EB, #7C3AED)', color: '#fff' }}> <div className="px-3 py-1.5 rounded-full text-xs font-bold" style={{ background: 'linear-gradient(135deg, #2563EB, #7C3AED)', color: '#fff' }}>
{language === 'zh' ? '🔥 推荐' : '🔥 Best'} {'🔥 ' + t('modelConfig.recommended', language)}
</div> </div>
</div> </div>
</div> </div>
@@ -256,7 +254,7 @@ function ModelSelectionStep({
<div className="flex items-center gap-3 pt-2"> <div className="flex items-center gap-3 pt-2">
<div className="flex-1 h-px" style={{ background: '#2B3139' }} /> <div className="flex-1 h-px" style={{ background: '#2B3139' }} />
<span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}> <span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}>
{language === 'zh' ? '通过钱包支付' : 'Via BlockRun Wallet'} {t('modelConfig.viaBlockrunWallet', language)}
</span> </span>
<div className="flex-1 h-px" style={{ background: '#2B3139' }} /> <div className="flex-1 h-px" style={{ background: '#2B3139' }} />
</div> </div>
@@ -274,7 +272,7 @@ function ModelSelectionStep({
</> </>
)} )}
<div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}> <div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}>
{language === 'zh' ? '带金色标记的模型已配置' : 'Models with gold badge are already configured'} {t('modelConfig.modelsConfigured', language)}
</div> </div>
</div> </div>
) )
@@ -310,9 +308,7 @@ function Claw402ConfigForm({
Claw402 <span className="text-xs font-normal" style={{ color: '#60A5FA' }}></span> Claw402 <span className="text-xs font-normal" style={{ color: '#60A5FA' }}></span>
</a> </a>
<div className="text-sm mt-1" style={{ color: '#A0AEC0' }}> <div className="text-sm mt-1" style={{ color: '#A0AEC0' }}>
{language === 'zh' {t('modelConfig.allModelsClaw', language)}
? '用 USDC 按次付费,支持所有主流 AI 模型'
: 'Pay-per-call with USDC — supports all major AI models'}
</div> </div>
<div className="flex items-center justify-center gap-3 mt-3 flex-wrap"> <div className="flex items-center justify-center gap-3 mt-3 flex-wrap">
{['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => ( {['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => (
@@ -327,12 +323,10 @@ function Claw402ConfigForm({
<div className="space-y-3"> <div className="space-y-3">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}> <label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<Brain className="w-4 h-4" style={{ color: '#2563EB' }} /> <Brain className="w-4 h-4" style={{ color: '#2563EB' }} />
{language === 'zh' ? '① 选择 AI 模型' : '① Choose AI Model'} {t('modelConfig.selectAiModel', language)}
</label> </label>
<div className="text-xs mb-2" style={{ color: '#848E9C' }}> <div className="text-xs mb-2" style={{ color: '#848E9C' }}>
{language === 'zh' {t('modelConfig.allModelsUnified', language)}
? '所有模型通过 Claw402 统一调用,创建后可随时切换'
: 'All models unified via Claw402. Switch anytime after setup.'}
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{CLAW402_MODELS.map((m) => { {CLAW402_MODELS.map((m) => {
@@ -372,34 +366,28 @@ function Claw402ConfigForm({
<svg className="w-4 h-4" style={{ color: '#2563EB' }} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" style={{ color: '#2563EB' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg> </svg>
{language === 'zh' ? '② 设置钱包' : '② Setup Wallet'} {t('modelConfig.setupWallet', language)}
</label> </label>
<div className="p-3 rounded-xl" style={{ background: 'rgba(37, 99, 235, 0.06)', border: '1px solid rgba(37, 99, 235, 0.15)' }}> <div className="p-3 rounded-xl" style={{ background: 'rgba(37, 99, 235, 0.06)', border: '1px solid rgba(37, 99, 235, 0.15)' }}>
<div className="text-xs mb-2" style={{ color: '#A0AEC0' }}> <div className="text-xs mb-2" style={{ color: '#A0AEC0' }}>
{language === 'zh' {t('modelConfig.walletInfo', language)}
? '💡 Claw402 使用 Base 链上的 USDC 付费,你需要一个 EVM 钱包'
: '💡 Claw402 uses USDC on Base chain. You need an EVM wallet.'}
</div> </div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}> <div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span style={{ color: '#00E096' }}></span> <span style={{ color: '#00E096' }}></span>
{language === 'zh' {t('modelConfig.exportKey', language)}
? '可以用 MetaMask、Rabby 等钱包导出私钥'
: 'Export private key from MetaMask, Rabby, etc.'}
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span style={{ color: '#00E096' }}></span> <span style={{ color: '#00E096' }}></span>
{language === 'zh' {t('modelConfig.dedicatedWallet', language)}
? '建议新建一个专用钱包,充入少量 USDC 即可'
: 'Recommended: create a dedicated wallet with a small USDC balance'}
</div> </div>
</div> </div>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="text-xs font-medium" style={{ color: '#A0AEC0' }}> <div className="text-xs font-medium" style={{ color: '#A0AEC0' }}>
{language === 'zh' ? '钱包私钥(Base 链 EVM' : 'Wallet Private Key (Base Chain EVM)'} {t('modelConfig.walletPrivateKey', language)}
</div> </div>
<input <input
type="password" type="password"
@@ -413,9 +401,7 @@ function Claw402ConfigForm({
<div className="flex items-start gap-1.5 text-[11px]" style={{ color: '#848E9C' }}> <div className="flex items-start gap-1.5 text-[11px]" style={{ color: '#848E9C' }}>
<span className="mt-px">🔒</span> <span className="mt-px">🔒</span>
<span> <span>
{language === 'zh' {t('modelConfig.privateKeyNote', language)}
? '私钥仅在本地签名使用,不会上传或发送交易。无需 ETH,无 Gas 费用。'
: 'Private key is only used locally for signing. Never uploaded. No ETH or gas needed.'}
</span> </span>
</div> </div>
</div> </div>
@@ -424,20 +410,20 @@ function Claw402ConfigForm({
{/* USDC Recharge Guide */} {/* USDC Recharge Guide */}
<div className="p-4 rounded-xl" style={{ background: 'rgba(0, 224, 150, 0.05)', border: '1px solid rgba(0, 224, 150, 0.15)' }}> <div className="p-4 rounded-xl" style={{ background: 'rgba(0, 224, 150, 0.05)', border: '1px solid rgba(0, 224, 150, 0.15)' }}>
<div className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: '#00E096' }}> <div className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: '#00E096' }}>
💰 {language === 'zh' ? '如何充值 USDC' : 'How to Fund USDC'} {'💰 ' + t('modelConfig.howToFundUsdc', language)}
</div> </div>
<div className="text-xs space-y-1.5" style={{ color: '#848E9C' }}> <div className="text-xs space-y-1.5" style={{ color: '#848E9C' }}>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span className="font-bold" style={{ color: '#A0AEC0' }}>1.</span> <span className="font-bold" style={{ color: '#A0AEC0' }}>1.</span>
<span>{language === 'zh' ? '从交易所(Binance / OKX / Coinbase)提 USDC 到你的钱包地址' : 'Withdraw USDC from exchange (Binance/OKX/Coinbase) to your wallet'}</span> <span>{t('modelConfig.fundStep1', language)}</span>
</div> </div>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span className="font-bold" style={{ color: '#A0AEC0' }}>2.</span> <span className="font-bold" style={{ color: '#A0AEC0' }}>2.</span>
<span>{language === 'zh' ? '选择 Base 网络(手续费极低)' : 'Select Base network (very low fees)'}</span> <span>{t('modelConfig.fundStep2', language)}</span>
</div> </div>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span className="font-bold" style={{ color: '#A0AEC0' }}>3.</span> <span className="font-bold" style={{ color: '#A0AEC0' }}>3.</span>
<span>{language === 'zh' ? '充入 $5-10 USDC 即可使用很长时间(约 $0.003/次调用)' : '$5-10 USDC lasts a long time (~$0.003/call)'}</span> <span>{t('modelConfig.fundStep3', language)}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -445,7 +431,7 @@ function Claw402ConfigForm({
{/* Buttons */} {/* Buttons */}
<div className="flex gap-3 pt-2"> <div className="flex gap-3 pt-2">
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}> <button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')} {editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
</button> </button>
<button <button
type="submit" type="submit"
@@ -453,7 +439,7 @@ function Claw402ConfigForm({
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed" className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
style={{ background: apiKey.trim() ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }} style={{ background: apiKey.trim() ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
> >
{language === 'zh' ? '🚀 开始交易' : '🚀 Start Trading'} {'🚀 ' + t('modelConfig.startTrading', language)}
</button> </button>
</div> </div>
</form> </form>
@@ -513,8 +499,8 @@ function StandardProviderConfigForm({
<ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} /> <ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} />
<span className="text-sm font-medium" style={{ color: '#A78BFA' }}> <span className="text-sm font-medium" style={{ color: '#A78BFA' }}>
{selectedModel.provider?.startsWith('blockrun') {selectedModel.provider?.startsWith('blockrun')
? (language === 'zh' ? '开始使用' : 'Get Started') ? t('modelConfig.getStarted', language)
: (language === 'zh' ? '获取 API Key' : 'Get API Key')} : t('modelConfig.getApiKey', language)}
</span> </span>
</a> </a>
)} )}
@@ -539,7 +525,7 @@ function StandardProviderConfigForm({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg> </svg>
{selectedModel.provider?.startsWith('blockrun') {selectedModel.provider?.startsWith('blockrun')
? (language === 'zh' ? '钱包私钥 *' : 'Wallet Private Key *') ? t('modelConfig.walletPrivateKeyLabel', language)
: 'API Key *'} : 'API Key *'}
</label> </label>
<input <input
@@ -612,7 +598,7 @@ function StandardProviderConfigForm({
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg> </svg>
{language === 'zh' ? '选择模型' : 'Select Model'} {t('modelConfig.selectModelLabel', language)}
</label> </label>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{BLOCKRUN_MODELS.map((m) => { {BLOCKRUN_MODELS.map((m) => {
@@ -655,7 +641,7 @@ function StandardProviderConfigForm({
{/* Buttons */} {/* Buttons */}
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}> <button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')} {editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
</button> </button>
<button <button
type="submit" type="submit"
@@ -3,7 +3,7 @@ import { Check, ChevronLeft, ExternalLink, MessageCircle, Unlink, ArrowRight } f
import { toast } from 'sonner' import { toast } from 'sonner'
import { api } from '../../lib/api' import { api } from '../../lib/api'
import type { TelegramConfig, AIModel } from '../../types' import type { TelegramConfig, AIModel } from '../../types'
import type { Language } from '../../i18n/translations' import { t, type Language } from '../../i18n/translations'
// Step indicator (reused pattern from ExchangeConfigModal) // Step indicator (reused pattern from ExchangeConfigModal)
function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) { function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) {
@@ -55,8 +55,6 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isUnbinding, setIsUnbinding] = useState(false) const [isUnbinding, setIsUnbinding] = useState(false)
const zh = language === 'zh'
// Load current config and available models // Load current config and available models
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
@@ -84,20 +82,20 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
// Basic format validation: looks like "123456789:ABCdef..." // Basic format validation: looks like "123456789:ABCdef..."
if (!/^\d+:[A-Za-z0-9_-]{35,}$/.test(token.trim())) { if (!/^\d+:[A-Za-z0-9_-]{35,}$/.test(token.trim())) {
toast.error(zh ? 'Bot Token 格式不正确,应为 "数字:字母数字串"' : 'Invalid Bot Token format. Expected "numbers:alphanumeric"') toast.error(t('telegram.invalidTokenFormat', language))
return return
} }
setIsSaving(true) setIsSaving(true)
try { try {
await api.updateTelegramConfig(token.trim(), selectedModelId || undefined) await api.updateTelegramConfig(token.trim(), selectedModelId || undefined)
toast.success(zh ? 'Bot Token 已保存,等待绑定' : 'Bot Token saved, waiting for binding') toast.success(t('telegram.tokenSaved', language))
const updated = await api.getTelegramConfig() const updated = await api.getTelegramConfig()
setConfig(updated) setConfig(updated)
setToken('') setToken('')
setStep(1) setStep(1)
} catch (err) { } catch (err) {
toast.error(zh ? '保存失败,请检查 Token 是否正确' : 'Save failed, please verify the token') toast.error(t('telegram.saveFailed', language))
} finally { } finally {
setIsSaving(false) setIsSaving(false)
} }
@@ -108,33 +106,31 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
setIsUnbinding(true) setIsUnbinding(true)
try { try {
await api.unbindTelegram() await api.unbindTelegram()
toast.success(zh ? '已解绑 Telegram 账号' : 'Telegram account unbound') toast.success(t('telegram.unbound', language))
const updated = await api.getTelegramConfig() const updated = await api.getTelegramConfig()
setConfig(updated) setConfig(updated)
setStep(updated.token_masked ? 1 : 0) setStep(updated.token_masked ? 1 : 0)
} catch { } catch {
toast.error(zh ? '解绑失败' : 'Unbind failed') toast.error(t('telegram.unbindFailed', language))
} finally { } finally {
setIsUnbinding(false) setIsUnbinding(false)
} }
} }
const stepLabels = zh const stepLabels = [t('telegram.createBot', language), t('telegram.bindAccount', language), t('telegram.done', language)]
? ['创建 Bot', '绑定账号', '完成']
: ['Create Bot', 'Bind Account', 'Done']
// Model selector shared between steps // Model selector shared between steps
const ModelSelector = () => ( const ModelSelector = () => (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}> <label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{zh ? '选择 AI 模型(可选)' : 'Select AI Model (optional)'} {t('telegram.selectAiModel', language)}
</label> </label>
{models.length === 0 ? ( {models.length === 0 ? (
<div <div
className="px-4 py-3 rounded-xl text-xs" className="px-4 py-3 rounded-xl text-xs"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#848E9C' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#848E9C' }}
> >
{zh ? '暂无启用的模型,请先在「AI 模型」中配置' : 'No enabled models. Configure one in AI Models first.'} {t('telegram.noEnabledModels', language)}
</div> </div>
) : ( ) : (
<select <select
@@ -147,7 +143,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
color: selectedModelId ? '#EAECEF' : '#848E9C', color: selectedModelId ? '#EAECEF' : '#848E9C',
}} }}
> >
<option value="">{zh ? '— 自动选择(推荐)' : '— Auto-select (recommended)'}</option> <option value="">{t('telegram.autoSelect', language)}</option>
{models.map((m) => ( {models.map((m) => (
<option key={m.id} value={m.id}> <option key={m.id} value={m.id}>
{m.name} ({m.provider}{m.customModelName ? ` · ${m.customModelName}` : ''}) {m.name} ({m.provider}{m.customModelName ? ` · ${m.customModelName}` : ''})
@@ -156,9 +152,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
</select> </select>
)} )}
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{zh {t('telegram.autoUseEnabled', language)}
? '不选则自动使用已启用的模型'
: 'Leave blank to auto-use any enabled model'}
</div> </div>
</div> </div>
) )
@@ -184,7 +178,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MessageCircle className="w-6 h-6" style={{ color: '#2AABEE' }} /> <MessageCircle className="w-6 h-6" style={{ color: '#2AABEE' }} />
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}> <h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{zh ? 'Telegram Bot 配置' : 'Telegram Bot Setup'} {t('telegram.botSetup', language)}
</h3> </h3>
</div> </div>
</div> </div>
@@ -207,7 +201,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="px-6 pb-6 space-y-5"> <div className="px-6 pb-6 space-y-5">
{isLoading ? ( {isLoading ? (
<div className="text-center py-8 text-zinc-500 text-sm font-mono"> <div className="text-center py-8 text-zinc-500 text-sm font-mono">
{zh ? '加载中...' : 'Loading...'} {t('telegram.loading', language)}
</div> </div>
) : ( ) : (
<> <>
@@ -222,13 +216,13 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<span className="text-2xl">🤖</span> <span className="text-2xl">🤖</span>
<div> <div>
<div className="font-semibold mb-1" style={{ color: '#2AABEE' }}> <div className="font-semibold mb-1" style={{ color: '#2AABEE' }}>
{zh ? '第一步:在 Telegram 创建你的 Bot' : 'Step 1: Create your Bot in Telegram'} {t('telegram.step1Title', language)}
</div> </div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}> <div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div>1. {zh ? '打开 Telegram,搜索' : 'Open Telegram, search for'} <code className="text-blue-400">@BotFather</code></div> <div>1. {t('telegram.step1Desc1', language)} <code className="text-blue-400">@BotFather</code></div>
<div>2. {zh ? '发送' : 'Send'} <code className="text-blue-400">/newbot</code> {zh ? '命令' : 'command'}</div> <div>2. {t('telegram.step1Desc2', language)} <code className="text-blue-400">/newbot</code> {t('telegram.step1Desc2Suffix', language)}</div>
<div>3. {zh ? '按提示输入 Bot 名称和用户名' : 'Follow prompts to set bot name and username'}</div> <div>3. {t('telegram.step1Desc3', language)}</div>
<div>4. {zh ? 'BotFather 会返回一个 Token,复制它' : 'BotFather will return a Token, copy it'}</div> <div>4. {t('telegram.step1Desc4', language)}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -242,12 +236,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: '#2AABEE', color: '#000' }} style={{ background: '#2AABEE', color: '#000' }}
> >
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" />
{zh ? '打开 @BotFather' : 'Open @BotFather'} {t('telegram.openBotFather', language)}
</a> </a>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}> <label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{zh ? '粘贴 Bot Token' : 'Paste Bot Token'} {t('telegram.pasteToken', language)}
</label> </label>
<input <input
type="password" type="password"
@@ -258,7 +252,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/> />
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{zh ? 'Token 格式:数字:字母数字串,如 123456789:ABCdef...' : 'Format: numbers:alphanumeric, e.g. 123456789:ABCdef...'} {t('telegram.tokenFormat', language)}
</div> </div>
</div> </div>
@@ -271,8 +265,8 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: '#2AABEE', color: '#000' }} style={{ background: '#2AABEE', color: '#000' }}
> >
{isSaving {isSaving
? (zh ? '保存中...' : 'Saving...') ? t('telegram.savingToken', language)
: (<>{zh ? '保存并继续' : 'Save & Continue'} <ArrowRight className="w-4 h-4" /></>) : (<>{t('telegram.saveAndContinue', language)} <ArrowRight className="w-4 h-4" /></>)
} }
</button> </button>
</div> </div>
@@ -289,12 +283,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<span className="text-2xl">📱</span> <span className="text-2xl">📱</span>
<div> <div>
<div className="font-semibold mb-1" style={{ color: '#0ECB81' }}> <div className="font-semibold mb-1" style={{ color: '#0ECB81' }}>
{zh ? '第二步:向你的 Bot 发送 /start' : 'Step 2: Send /start to your Bot'} {t('telegram.step2Title', language)}
</div> </div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}> <div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div>1. {zh ? '在 Telegram 中搜索你刚创建的 Bot' : 'Search for your newly created Bot in Telegram'}</div> <div>1. {t('telegram.step2Desc1', language)}</div>
<div>2. {zh ? '点击 Start 或发送' : 'Click Start or send'} <code className="text-green-400">/start</code></div> <div>2. {t('telegram.step2Desc2', language)} <code className="text-green-400">/start</code></div>
<div>3. {zh ? 'Bot 会自动绑定到你的账号' : 'Bot will automatically bind to your account'}</div> <div>3. {t('telegram.step2Desc3', language)}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -308,7 +302,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse flex-shrink-0" /> <div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse flex-shrink-0" />
<div> <div>
<div className="text-xs font-mono" style={{ color: '#848E9C' }}> <div className="text-xs font-mono" style={{ color: '#848E9C' }}>
{zh ? '当前 Token' : 'Current Token'} {t('telegram.currentToken', language)}
</div> </div>
<div className="text-sm font-mono" style={{ color: '#EAECEF' }}> <div className="text-sm font-mono" style={{ color: '#EAECEF' }}>
{config.token_masked} {config.token_masked}
@@ -322,9 +316,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: 'rgba(240, 185, 11, 0.08)', border: '1px solid rgba(240, 185, 11, 0.2)' }} style={{ background: 'rgba(240, 185, 11, 0.08)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
> >
<div className="text-xs" style={{ color: '#F0B90B' }}> <div className="text-xs" style={{ color: '#F0B90B' }}>
{zh {t('telegram.waitingForStart', language)}
? '⏳ 等待你发送 /start... 发送后刷新页面查看状态'
: '⏳ Waiting for you to send /start... Refresh page after sending'}
</div> </div>
</div> </div>
@@ -334,7 +326,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5"
style={{ background: '#2B3139', color: '#848E9C' }} style={{ background: '#2B3139', color: '#848E9C' }}
> >
{zh ? '重新配置 Token' : 'Reconfigure Token'} {t('telegram.reconfigureToken', language)}
</button> </button>
<button <button
onClick={async () => { onClick={async () => {
@@ -343,19 +335,19 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
setConfig(updated) setConfig(updated)
if (updated.is_bound) { if (updated.is_bound) {
setStep(2) setStep(2)
toast.success(zh ? '绑定成功!' : 'Bound successfully!') toast.success(t('telegram.bindSuccess', language))
} else { } else {
toast.info(zh ? '尚未收到 /start,请先向 Bot 发送 /start' : 'No /start received yet. Please send /start to your Bot first') toast.info(t('telegram.noStartReceived', language))
} }
} catch { } catch {
toast.error(zh ? '检查失败' : 'Check failed') toast.error(t('telegram.checkFailed', language))
} }
}} }}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]" className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
style={{ background: '#0ECB81', color: '#000' }} style={{ background: '#0ECB81', color: '#000' }}
> >
<Check className="w-4 h-4" /> <Check className="w-4 h-4" />
{zh ? '检查绑定状态' : 'Check Status'} {t('telegram.checkStatus', language)}
</button> </button>
</div> </div>
</div> </div>
@@ -370,12 +362,10 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
> >
<div className="text-4xl">🎉</div> <div className="text-4xl">🎉</div>
<div className="font-bold text-lg" style={{ color: '#0ECB81' }}> <div className="font-bold text-lg" style={{ color: '#0ECB81' }}>
{zh ? 'Telegram Bot 已绑定!' : 'Telegram Bot is Active!'} {t('telegram.botActive', language)}
</div> </div>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{zh {t('telegram.botActiveDesc', language)}
? '你现在可以通过 Telegram 用自然语言控制交易系统'
: 'You can now control the trading system via natural language in Telegram'}
</div> </div>
</div> </div>
@@ -387,7 +377,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" /> <div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
<div className="min-w-0"> <div className="min-w-0">
<div className="text-xs font-mono" style={{ color: '#848E9C' }}> <div className="text-xs font-mono" style={{ color: '#848E9C' }}>
{zh ? 'Bot Token' : 'Bot Token'} Bot Token
</div> </div>
<div className="text-sm font-mono truncate" style={{ color: '#EAECEF' }}> <div className="text-sm font-mono truncate" style={{ color: '#EAECEF' }}>
{config.token_masked} {config.token_masked}
@@ -398,7 +388,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
{/* AI Model selector — works on active bot */} {/* AI Model selector — works on active bot */}
<BoundModelSelector <BoundModelSelector
zh={zh} language={language}
models={models} models={models}
currentModelId={config?.model_id ?? ''} currentModelId={config?.model_id ?? ''}
onSaved={(modelId) => { onSaved={(modelId) => {
@@ -412,14 +402,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: '#0B0E11', border: '1px solid #2B3139' }} style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
> >
<div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#848E9C' }}> <div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#848E9C' }}>
{zh ? '支持的命令' : 'Supported Commands'} {t('telegram.supportedCommands', language)}
</div> </div>
{[ {[
{ cmd: '/help', desc: zh ? '查看所有命令' : 'Show all commands' }, { cmd: '/help', desc: t('telegram.cmdHelp', language) },
{ cmd: zh ? '查看交易员状态' : 'Show trader status', desc: zh ? '自然语言查询' : 'Natural language' }, { cmd: t('telegram.cmdStatus', language), desc: t('telegram.cmdNaturalLang', language) },
{ cmd: zh ? '启动/停止交易员' : 'Start/stop trader', desc: zh ? '自然语言控制' : 'Natural language control' }, { cmd: t('telegram.cmdStartStop', language), desc: t('telegram.cmdControl', language) },
{ cmd: zh ? '查看持仓' : 'View positions', desc: zh ? '实时持仓查询' : 'Real-time position query' }, { cmd: t('telegram.cmdPositions', language), desc: t('telegram.cmdPositionsDesc', language) },
{ cmd: zh ? '配置策略' : 'Configure strategy', desc: zh ? '修改交易策略' : 'Modify trading strategy' }, { cmd: t('telegram.cmdStrategy', language), desc: t('telegram.cmdStrategyDesc', language) },
].map((item, i) => ( ].map((item, i) => (
<div key={i} className="flex items-start gap-2 text-xs"> <div key={i} className="flex items-start gap-2 text-xs">
<code className="font-mono px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#1E2329', color: '#2AABEE' }}> <code className="font-mono px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#1E2329', color: '#2AABEE' }}>
@@ -438,14 +428,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }} style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
> >
<Unlink className="w-4 h-4" /> <Unlink className="w-4 h-4" />
{isUnbinding ? (zh ? '解绑中...' : 'Unbinding...') : (zh ? '解绑账号' : 'Unbind Account')} {isUnbinding ? t('telegram.unbinding', language) : t('telegram.unbindAccount', language)}
</button> </button>
<button <button
onClick={onClose} onClick={onClose}
className="flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]" className="flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
style={{ background: '#2AABEE', color: '#000' }} style={{ background: '#2AABEE', color: '#000' }}
> >
{zh ? '完成' : 'Done'} {t('telegram.done', language)}
</button> </button>
</div> </div>
</div> </div>
@@ -461,12 +451,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
// BoundModelSelector — lets the user change the AI model when the bot is already active. // BoundModelSelector — lets the user change the AI model when the bot is already active.
// It updates the model_id without requiring re-entry of the bot token. // It updates the model_id without requiring re-entry of the bot token.
function BoundModelSelector({ function BoundModelSelector({
zh, language,
models, models,
currentModelId, currentModelId,
onSaved, onSaved,
}: { }: {
zh: boolean language: Language
models: AIModel[] models: AIModel[]
currentModelId: string currentModelId: string
onSaved: (modelId: string) => void onSaved: (modelId: string) => void
@@ -483,9 +473,9 @@ function BoundModelSelector({
// POST /api/telegram/model — lightweight endpoint for model-only update // POST /api/telegram/model — lightweight endpoint for model-only update
await api.updateTelegramModel(modelId) await api.updateTelegramModel(modelId)
onSaved(modelId) onSaved(modelId)
toast.success(zh ? 'AI 模型已更新' : 'AI model updated') toast.success(t('telegram.modelUpdated', language))
} catch { } catch {
toast.error(zh ? '更新失败' : 'Update failed') toast.error(t('telegram.modelUpdateFailed', language))
} finally { } finally {
setIsSaving(false) setIsSaving(false)
} }
@@ -496,7 +486,7 @@ function BoundModelSelector({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}> <label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{zh ? 'AI 模型(用于自然语言解析)' : 'AI Model (for natural language)'} {t('telegram.aiModelLabel', language)}
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<select <select
@@ -509,7 +499,7 @@ function BoundModelSelector({
color: modelId ? '#EAECEF' : '#848E9C', color: modelId ? '#EAECEF' : '#848E9C',
}} }}
> >
<option value="">{zh ? '— 自动选择' : '— Auto-select'}</option> <option value="">{t('telegram.aiModelAutoSelect', language)}</option>
{models.map((m) => ( {models.map((m) => (
<option key={m.id} value={m.id}> <option key={m.id} value={m.id}>
{m.name}{m.customModelName ? ` · ${m.customModelName}` : ''} {m.name}{m.customModelName ? ` · ${m.customModelName}` : ''}
@@ -522,7 +512,7 @@ function BoundModelSelector({
className="px-4 py-2.5 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-40 disabled:cursor-not-allowed" className="px-4 py-2.5 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-40 disabled:cursor-not-allowed"
style={{ background: '#F0B90B', color: '#000', whiteSpace: 'nowrap' }} style={{ background: '#F0B90B', color: '#000', whiteSpace: 'nowrap' }}
> >
{isSaving ? '...' : (zh ? '保存' : 'Save')} {isSaving ? '...' : t('telegram.save', language)}
</button> </button>
</div> </div>
</div> </div>
@@ -1,7 +1,9 @@
import type { TraderConfigData } from '../../types' import type { TraderConfigData } from '../../types'
import { t } from '../../i18n/translations'
import { useLanguage } from '../../contexts/LanguageContext'
import { PunkAvatar, getTraderAvatar } from '../common/PunkAvatar' import { PunkAvatar, getTraderAvatar } from '../common/PunkAvatar'
// 提取下划线后面的名称部分 // Extract the name part after the last underscore
function getShortName(fullName: string): string { function getShortName(fullName: string): string {
const parts = fullName.split('_') const parts = fullName.split('_')
return parts.length > 1 ? parts[parts.length - 1] : fullName return parts.length > 1 ? parts[parts.length - 1] : fullName
@@ -18,6 +20,7 @@ export function TraderConfigViewModal({
onClose, onClose,
traderData, traderData,
}: TraderConfigViewModalProps) { }: TraderConfigViewModalProps) {
const { language } = useLanguage()
if (!isOpen || !traderData) return null if (!isOpen || !traderData) return null
const InfoRow = ({ const InfoRow = ({
@@ -30,7 +33,7 @@ export function TraderConfigViewModal({
<div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0"> <div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0">
<span className="text-sm text-[#848E9C] font-medium">{label}</span> <span className="text-sm text-[#848E9C] font-medium">{label}</span>
<span className="text-sm text-[#EAECEF] font-mono text-right"> <span className="text-sm text-[#EAECEF] font-mono text-right">
{typeof value === 'boolean' ? (value ? '是' : '否') : value} {typeof value === 'boolean' ? (value ? t('traderConfigView.yes', language) : t('traderConfigView.no', language)) : value}
</span> </span>
</div> </div>
) )
@@ -50,9 +53,9 @@ export function TraderConfigViewModal({
className="rounded-lg" className="rounded-lg"
/> />
<div> <div>
<h2 className="text-xl font-bold text-[#EAECEF]"></h2> <h2 className="text-xl font-bold text-[#EAECEF]">{t('traderConfigView.traderConfig', language)}</h2>
<p className="text-sm text-[#848E9C] mt-1"> <p className="text-sm text-[#848E9C] mt-1">
{traderData.trader_name} {t('traderConfigView.configInfo', language, { name: traderData.trader_name })}
</p> </p>
</div> </div>
</div> </div>
@@ -67,7 +70,7 @@ export function TraderConfigViewModal({
} }
> >
<span>{traderData.is_running ? '●' : '○'}</span> <span>{traderData.is_running ? '●' : '○'}</span>
{traderData.is_running ? '运行中' : '已停止'} {traderData.is_running ? t('traderConfigView.running', language) : t('traderConfigView.stopped', language)}
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
@@ -83,32 +86,32 @@ export function TraderConfigViewModal({
{/* Basic Info */} {/* Basic Info */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5"> <div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2"> <h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
🤖 {'🤖 ' + t('traderConfigView.basicInfo', language)}
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<InfoRow <InfoRow
label="交易员名称" label={t('traderConfigView.traderName', language)}
value={traderData.trader_name} value={traderData.trader_name}
/> />
<InfoRow <InfoRow
label="AI模型" label={t('traderConfigView.aiModel', language)}
value={getShortName(traderData.ai_model).toUpperCase()} value={getShortName(traderData.ai_model).toUpperCase()}
/> />
<InfoRow <InfoRow
label="交易所" label={t('traderConfigView.exchange', language)}
value={getShortName(traderData.exchange_id).toUpperCase()} value={getShortName(traderData.exchange_id).toUpperCase()}
/> />
<InfoRow <InfoRow
label="初始余额" label={t('traderConfigView.initialBalance', language)}
value={`$${traderData.initial_balance.toLocaleString()}`} value={`$${traderData.initial_balance.toLocaleString()}`}
/> />
<InfoRow <InfoRow
label="保证金模式" label={t('traderConfigView.marginMode', language)}
value={traderData.is_cross_margin ? '全仓' : '逐仓'} value={traderData.is_cross_margin ? t('traderConfigView.crossMargin', language) : t('traderConfigView.isolatedMargin', language)}
/> />
<InfoRow <InfoRow
label="扫描间隔" label={t('traderConfigView.scanIntervalLabel', language)}
value={`${traderData.scan_interval_minutes || 3} 分钟`} value={t('traderConfigView.scanInterval', language, { minutes: traderData.scan_interval_minutes || 3 })}
/> />
</div> </div>
</div> </div>
@@ -117,11 +120,11 @@ export function TraderConfigViewModal({
{traderData.strategy_id && ( {traderData.strategy_id && (
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5"> <div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2"> <h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
📋 使 {'📋 ' + t('traderConfigView.strategyUsed', language)}
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<InfoRow <InfoRow
label="策略名称" label={t('traderConfigView.strategyName', language)}
value={traderData.strategy_name || traderData.strategy_id} value={traderData.strategy_name || traderData.strategy_id}
/> />
</div> </div>
@@ -135,7 +138,7 @@ export function TraderConfigViewModal({
onClick={onClose} onClick={onClose}
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]" className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
> >
{t('traderConfigView.close', language)}
</button> </button>
</div> </div>
</div> </div>
+11 -11
View File
@@ -45,10 +45,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// Reset 401 flag on page load to allow fresh 401 handling // Reset 401 flag on page load to allow fresh 401 handling
reset401Flag() reset401Flag()
// 先检查是否为管理员模式(使用带缓存的系统配置获取) // Check if admin mode is active (uses cached system config)
getSystemConfig() getSystemConfig()
.then(() => { .then(() => {
// 不再在管理员模式下模拟登录;统一检查本地存储 // No longer simulate login in admin mode; check local storage uniformly
const savedToken = localStorage.getItem('auth_token') const savedToken = localStorage.getItem('auth_token')
const savedUser = localStorage.getItem('auth_user') const savedUser = localStorage.getItem('auth_user')
if (savedToken && savedUser) { if (savedToken && savedUser) {
@@ -60,7 +60,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}) })
.catch((err) => { .catch((err) => {
console.error('Failed to fetch system config:', err) console.error('Failed to fetch system config:', err)
// 发生错误时,继续检查本地存储 // On error, continue checking local storage
const savedToken = localStorage.getItem('auth_token') const savedToken = localStorage.getItem('auth_token')
const savedUser = localStorage.getItem('auth_user') const savedUser = localStorage.getItem('auth_user')
@@ -119,7 +119,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
window.history.pushState({}, '', returnUrl) window.history.pushState({}, '', returnUrl)
window.dispatchEvent(new PopStateEvent('popstate')) window.dispatchEvent(new PopStateEvent('popstate'))
} else { } else {
// 跳转到配置页面 // Redirect to config page
window.history.pushState({}, '', '/traders') window.history.pushState({}, '', '/traders')
window.dispatchEvent(new PopStateEvent('popstate')) window.dispatchEvent(new PopStateEvent('popstate'))
} }
@@ -128,7 +128,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
} }
// Unexpected success response // Unexpected success response
return { success: false, message: data.message || '登录响应异常' } return { success: false, message: data.message || 'Unexpected login response' }
} else { } else {
return { return {
success: false, success: false,
@@ -136,7 +136,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
} }
} }
} catch (error) { } catch (error) {
return { success: false, message: '登录失败,请重试' } return { success: false, message: 'Login failed, please try again' }
} }
} }
@@ -168,16 +168,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
window.history.pushState({}, '', returnUrl) window.history.pushState({}, '', returnUrl)
window.dispatchEvent(new PopStateEvent('popstate')) window.dispatchEvent(new PopStateEvent('popstate'))
} else { } else {
// 跳转到仪表盘 // Redirect to dashboard
window.history.pushState({}, '', '/dashboard') window.history.pushState({}, '', '/dashboard')
window.dispatchEvent(new PopStateEvent('popstate')) window.dispatchEvent(new PopStateEvent('popstate'))
} }
return { success: true } return { success: true }
} else { } else {
return { success: false, message: data.error || '登录失败' } return { success: false, message: data.error || 'Login failed' }
} }
} catch (e) { } catch (e) {
return { success: false, message: '登录失败,请重试' } return { success: false, message: 'Login failed, please try again' }
} }
} }
@@ -220,7 +220,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
window.history.pushState({}, '', returnUrl) window.history.pushState({}, '', returnUrl)
window.dispatchEvent(new PopStateEvent('popstate')) window.dispatchEvent(new PopStateEvent('popstate'))
} else { } else {
// 跳转到配置页面 // Redirect to config page
window.history.pushState({}, '', '/traders') window.history.pushState({}, '', '/traders')
window.dispatchEvent(new PopStateEvent('popstate')) window.dispatchEvent(new PopStateEvent('popstate'))
} }
@@ -269,7 +269,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return { success: false, message: data.error } return { success: false, message: data.error }
} }
} catch (error) { } catch (error) {
return { success: false, message: '密码重置失败,请重试' } return { success: false, message: 'Password reset failed, please try again' }
} }
} }
+26
View File
@@ -31,6 +31,20 @@ export const coinSource = {
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded', es: 'Estas monedas serán excluidas de todas las fuentes' }, excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded', es: 'Estas monedas serán excluidas de todas las fuentes' },
addExcludedCoin: { zh: '添加排除', en: 'Add Excluded', es: 'Agregar Excluida' }, addExcludedCoin: { zh: '添加排除', en: 'Add Excluded', es: 'Agregar Excluida' },
nofxosNote: { zh: '使用 NofxOS API Key(在指标配置中设置)', en: 'Uses NofxOS API Key (set in Indicators config)', es: 'Usa API Key de NofxOS' }, nofxosNote: { zh: '使用 NofxOS API Key(在指标配置中设置)', en: 'Uses NofxOS API Key (set in Indicators config)', es: 'Usa API Key de NofxOS' },
ai500Desc: { zh: '使用 AI500 智能筛选的热门币种', en: 'Use AI500 smart-filtered popular coins', es: 'Monedas filtradas por AI500' },
oiTopDesc: { zh: '持仓增加榜,适合做多', en: 'OI increase ranking, for long', es: 'Ranking OI creciente, para largo' },
oi_lowDesc: { zh: '持仓减少榜,适合做空', en: 'OI decrease ranking, for short', es: 'Ranking OI decreciente, para corto' },
mixedDesc: { zh: '组合多种数据源', en: 'Combine multiple sources', es: 'Combinar fuentes múltiples' },
oiIncreaseShort: { zh: 'OI增', en: 'OI↑', es: 'OI↑' },
oiDecreaseShort: { zh: 'OI减', en: 'OI↓', es: 'OI↓' },
custom: { zh: '自定义', en: 'Custom', es: 'Personalizado' },
excludedNone: { zh: '无', en: 'None', es: 'Ninguno' },
oiIncreaseTitle: { zh: 'OI 持仓增加榜', en: 'OI Increase', es: 'OI Aumento' },
oiDecreaseTitle: { zh: 'OI 持仓减少榜', en: 'OI Decrease', es: 'OI Disminución' },
oiIncreaseLabel: { zh: 'OI 增加', en: 'OI Increase', es: 'OI Aumento' },
forLong: { zh: '适合做多', en: 'For long', es: 'Para largo' },
oiDecreaseLabel: { zh: 'OI 减少', en: 'OI Decrease', es: 'OI Disminución' },
forShort: { zh: '适合做空', en: 'For short', es: 'Para corto' },
}; };
// ============================================================================ // ============================================================================
@@ -83,6 +97,8 @@ export const gridConfig = {
modeLong: { zh: '全多:100%买 + 0%卖', en: 'Long: 100% buy + 0% sell', es: 'Largo: 100% compra' }, modeLong: { zh: '全多:100%买 + 0%卖', en: 'Long: 100% buy + 0% sell', es: 'Largo: 100% compra' },
modeShortBias: { zh: '偏空:(100-X)%买 + X%卖', en: 'Short Bias: (100-X)% buy + X% sell', es: 'Sesgo Corto: X% venta' }, modeShortBias: { zh: '偏空:(100-X)%买 + X%卖', en: 'Short Bias: (100-X)% buy + X% sell', es: 'Sesgo Corto: X% venta' },
modeShort: { zh: '全空:0%买 + 100%卖', en: 'Short: 0% buy + 100% sell', es: 'Corto: 100% venta' }, modeShort: { zh: '全空:0%买 + 100%卖', en: 'Short: 0% buy + 100% sell', es: 'Corto: 100% venta' },
buy: { zh: '买', en: 'buy', es: 'compra' },
sell: { zh: '卖', en: 'sell', es: 'venta' },
}; };
// ============================================================================ // ============================================================================
@@ -172,6 +188,7 @@ export const promptSections = {
decisionProcessDesc: { zh: '设定决策步骤和思考流程', en: 'Set decision steps and thinking process', es: 'Establecer proceso' }, decisionProcessDesc: { zh: '设定决策步骤和思考流程', en: 'Set decision steps and thinking process', es: 'Establecer proceso' },
resetToDefault: { zh: '重置为默认', en: 'Reset to Default', es: 'Restablecer' }, resetToDefault: { zh: '重置为默认', en: 'Reset to Default', es: 'Restablecer' },
chars: { zh: '字符', en: 'chars', es: 'caracteres' }, chars: { zh: '字符', en: 'chars', es: 'caracteres' },
modified: { zh: '已修改', en: 'Modified', es: 'Modificado' },
}; };
// ============================================================================ // ============================================================================
@@ -235,6 +252,7 @@ export const indicator = {
connected: { zh: '已配置', en: 'Configured', es: 'Configurado' }, connected: { zh: '已配置', en: 'Configured', es: 'Configurado' },
notConfigured: { zh: '未配置', en: 'Not Configured', es: 'No Configurado' }, notConfigured: { zh: '未配置', en: 'Not Configured', es: 'No Configurado' },
nofxosDataSources: { zh: 'NofxOS 数据源', en: 'NofxOS Data Sources', es: 'Fuentes NofxOS' }, nofxosDataSources: { zh: 'NofxOS 数据源', en: 'NofxOS Data Sources', es: 'Fuentes NofxOS' },
configureApiKey: { zh: '请配置 API Key 以启用 NofxOS 数据源', en: 'Please configure API Key to enable NofxOS data sources', es: 'Configure API Key para habilitar NofxOS' },
}; };
// ============================================================================ // ============================================================================
@@ -262,6 +280,14 @@ export const chartTabs = {
hyperliquid: { zh: 'HL', en: 'HL', es: 'HL' }, hyperliquid: { zh: 'HL', en: 'HL', es: 'HL' },
}; };
// ============================================================================
// HELPER FUNCTION
// ============================================================================
export function ts(entry: { zh: string; en: string; [k: string]: string }, lang: string): string {
return entry[lang] ?? entry.en ?? ''
}
// ============================================================================ // ============================================================================
// AGGREGATED EXPORTS FOR TRANSLATIONS.TS // AGGREGATED EXPORTS FOR TRANSLATIONS.TS
// ============================================================================ // ============================================================================
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -183,13 +183,13 @@ export const backtestApi = {
try { try {
const data = text ? JSON.parse(text) : null const data = text ? JSON.parse(text) : null
throw new Error( throw new Error(
data?.error || data?.message || text || '导出失败,请稍后再试' data?.error || data?.message || text || 'Export failed, please try again later'
) )
} catch (err) { } catch (err) {
if (err instanceof Error && err.message) { if (err instanceof Error && err.message) {
throw err throw err
} }
throw new Error(text || '导出失败,请稍后再试') throw new Error(text || 'Export failed, please try again later')
} }
} }
return res.blob() return res.blob()
+36 -36
View File
@@ -10,7 +10,7 @@ import { API_BASE, httpClient, CryptoService } from './helpers'
export const configApi = { export const configApi = {
async getModelConfigs(): Promise<AIModel[]> { async getModelConfigs(): Promise<AIModel[]> {
const result = await httpClient.get<AIModel[]>(`${API_BASE}/models`) const result = await httpClient.get<AIModel[]>(`${API_BASE}/models`)
if (!result.success) throw new Error('获取模型配置失败') if (!result.success) throw new Error('Failed to fetch model configs')
return Array.isArray(result.data) ? result.data : [] return Array.isArray(result.data) ? result.data : []
}, },
@@ -18,13 +18,13 @@ export const configApi = {
const result = await httpClient.get<AIModel[]>( const result = await httpClient.get<AIModel[]>(
`${API_BASE}/supported-models` `${API_BASE}/supported-models`
) )
if (!result.success) throw new Error('获取支持的模型失败') if (!result.success) throw new Error('Failed to fetch supported models')
return result.data! return result.data!
}, },
async getPromptTemplates(): Promise<string[]> { async getPromptTemplates(): Promise<string[]> {
const res = await fetch(`${API_BASE}/prompt-templates`) const res = await fetch(`${API_BASE}/prompt-templates`)
if (!res.ok) throw new Error('获取提示词模板失败') if (!res.ok) throw new Error('Failed to fetch prompt templates')
const data = await res.json() const data = await res.json()
if (Array.isArray(data.templates)) { if (Array.isArray(data.templates)) {
return data.templates.map((item: { name: string }) => item.name) return data.templates.map((item: { name: string }) => item.name)
@@ -33,41 +33,41 @@ export const configApi = {
}, },
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> { async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
// 检查是否启用了传输加密 // Check if transport encryption is enabled
const config = await CryptoService.fetchCryptoConfig() const config = await CryptoService.fetchCryptoConfig()
if (!config.transport_encryption) { if (!config.transport_encryption) {
// 传输加密禁用时,直接发送明文 // Transport encryption disabled, send plaintext
const result = await httpClient.put(`${API_BASE}/models`, request) const result = await httpClient.put(`${API_BASE}/models`, request)
if (!result.success) throw new Error('更新模型配置失败') if (!result.success) throw new Error('Failed to update model configs')
return return
} }
// 获取RSA公钥 // Fetch RSA public key
const publicKey = await CryptoService.fetchPublicKey() const publicKey = await CryptoService.fetchPublicKey()
// 初始化加密服务 // Initialize crypto service
await CryptoService.initialize(publicKey) await CryptoService.initialize(publicKey)
// 获取用户信息(从localStorage或其他地方) // Get user info from localStorage
const userId = localStorage.getItem('user_id') || '' const userId = localStorage.getItem('user_id') || ''
const sessionId = sessionStorage.getItem('session_id') || '' const sessionId = sessionStorage.getItem('session_id') || ''
// 加密敏感数据 // Encrypt sensitive data
const encryptedPayload = await CryptoService.encryptSensitiveData( const encryptedPayload = await CryptoService.encryptSensitiveData(
JSON.stringify(request), JSON.stringify(request),
userId, userId,
sessionId sessionId
) )
// 发送加密数据 // Send encrypted data
const result = await httpClient.put(`${API_BASE}/models`, encryptedPayload) const result = await httpClient.put(`${API_BASE}/models`, encryptedPayload)
if (!result.success) throw new Error('更新模型配置失败') if (!result.success) throw new Error('Failed to update model configs')
}, },
async getExchangeConfigs(): Promise<Exchange[]> { async getExchangeConfigs(): Promise<Exchange[]> {
const result = await httpClient.get<Exchange[]>(`${API_BASE}/exchanges`) const result = await httpClient.get<Exchange[]>(`${API_BASE}/exchanges`)
if (!result.success) throw new Error('获取交易所配置失败') if (!result.success) throw new Error('Failed to fetch exchange configs')
return result.data! return result.data!
}, },
@@ -75,7 +75,7 @@ export const configApi = {
const result = await httpClient.get<Exchange[]>( const result = await httpClient.get<Exchange[]>(
`${API_BASE}/supported-exchanges` `${API_BASE}/supported-exchanges`
) )
if (!result.success) throw new Error('获取支持的交易所失败') if (!result.success) throw new Error('Failed to fetch supported exchanges')
return result.data! return result.data!
}, },
@@ -83,93 +83,93 @@ export const configApi = {
request: UpdateExchangeConfigRequest request: UpdateExchangeConfigRequest
): Promise<void> { ): Promise<void> {
const result = await httpClient.put(`${API_BASE}/exchanges`, request) const result = await httpClient.put(`${API_BASE}/exchanges`, request)
if (!result.success) throw new Error('更新交易所配置失败') if (!result.success) throw new Error('Failed to update exchange configs')
}, },
async createExchange(request: CreateExchangeRequest): Promise<{ id: string }> { async createExchange(request: CreateExchangeRequest): Promise<{ id: string }> {
const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request) const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request)
if (!result.success) throw new Error('创建交易所账户失败') if (!result.success) throw new Error('Failed to create exchange account')
return result.data! return result.data!
}, },
async createExchangeEncrypted(request: CreateExchangeRequest): Promise<{ id: string }> { async createExchangeEncrypted(request: CreateExchangeRequest): Promise<{ id: string }> {
// 检查是否启用了传输加密 // Check if transport encryption is enabled
const config = await CryptoService.fetchCryptoConfig() const config = await CryptoService.fetchCryptoConfig()
if (!config.transport_encryption) { if (!config.transport_encryption) {
// 传输加密禁用时,直接发送明文 // Transport encryption disabled, send plaintext
const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request) const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request)
if (!result.success) throw new Error('创建交易所账户失败') if (!result.success) throw new Error('Failed to create exchange account')
return result.data! return result.data!
} }
// 获取RSA公钥 // Fetch RSA public key
const publicKey = await CryptoService.fetchPublicKey() const publicKey = await CryptoService.fetchPublicKey()
// 初始化加密服务 // Initialize crypto service
await CryptoService.initialize(publicKey) await CryptoService.initialize(publicKey)
// 获取用户信息 // Get user info
const userId = localStorage.getItem('user_id') || '' const userId = localStorage.getItem('user_id') || ''
const sessionId = sessionStorage.getItem('session_id') || '' const sessionId = sessionStorage.getItem('session_id') || ''
// 加密敏感数据 // Encrypt sensitive data
const encryptedPayload = await CryptoService.encryptSensitiveData( const encryptedPayload = await CryptoService.encryptSensitiveData(
JSON.stringify(request), JSON.stringify(request),
userId, userId,
sessionId sessionId
) )
// 发送加密数据 // Send encrypted data
const result = await httpClient.post<{ id: string }>( const result = await httpClient.post<{ id: string }>(
`${API_BASE}/exchanges`, `${API_BASE}/exchanges`,
encryptedPayload encryptedPayload
) )
if (!result.success) throw new Error('创建交易所账户失败') if (!result.success) throw new Error('Failed to create exchange account')
return result.data! return result.data!
}, },
async deleteExchange(exchangeId: string): Promise<void> { async deleteExchange(exchangeId: string): Promise<void> {
const result = await httpClient.delete(`${API_BASE}/exchanges/${exchangeId}`) const result = await httpClient.delete(`${API_BASE}/exchanges/${exchangeId}`)
if (!result.success) throw new Error('删除交易所账户失败') if (!result.success) throw new Error('Failed to delete exchange account')
}, },
async updateExchangeConfigsEncrypted( async updateExchangeConfigsEncrypted(
request: UpdateExchangeConfigRequest request: UpdateExchangeConfigRequest
): Promise<void> { ): Promise<void> {
// 检查是否启用了传输加密 // Check if transport encryption is enabled
const config = await CryptoService.fetchCryptoConfig() const config = await CryptoService.fetchCryptoConfig()
if (!config.transport_encryption) { if (!config.transport_encryption) {
// 传输加密禁用时,直接发送明文 // Transport encryption disabled, send plaintext
const result = await httpClient.put(`${API_BASE}/exchanges`, request) const result = await httpClient.put(`${API_BASE}/exchanges`, request)
if (!result.success) throw new Error('更新交易所配置失败') if (!result.success) throw new Error('Failed to update exchange configs')
return return
} }
// 获取RSA公钥 // Fetch RSA public key
const publicKey = await CryptoService.fetchPublicKey() const publicKey = await CryptoService.fetchPublicKey()
// 初始化加密服务 // Initialize crypto service
await CryptoService.initialize(publicKey) await CryptoService.initialize(publicKey)
// 获取用户信息(从localStorage或其他地方) // Get user info from localStorage
const userId = localStorage.getItem('user_id') || '' const userId = localStorage.getItem('user_id') || ''
const sessionId = sessionStorage.getItem('session_id') || '' const sessionId = sessionStorage.getItem('session_id') || ''
// 加密敏感数据 // Encrypt sensitive data
const encryptedPayload = await CryptoService.encryptSensitiveData( const encryptedPayload = await CryptoService.encryptSensitiveData(
JSON.stringify(request), JSON.stringify(request),
userId, userId,
sessionId sessionId
) )
// 发送加密数据 // Send encrypted data
const result = await httpClient.put( const result = await httpClient.put(
`${API_BASE}/exchanges`, `${API_BASE}/exchanges`,
encryptedPayload encryptedPayload
) )
if (!result.success) throw new Error('更新交易所配置失败') if (!result.success) throw new Error('Failed to update exchange configs')
}, },
async getServerIP(): Promise<{ async getServerIP(): Promise<{
@@ -180,7 +180,7 @@ export const configApi = {
public_ip: string public_ip: string
message: string message: string
}>(`${API_BASE}/server-ip`) }>(`${API_BASE}/server-ip`)
if (!result.success) throw new Error('获取服务器IP失败') if (!result.success) throw new Error('Failed to fetch server IP')
return result.data! return result.data!
}, },
} }
+12 -13
View File
@@ -15,7 +15,7 @@ export const dataApi = {
? `${API_BASE}/status?trader_id=${traderId}` ? `${API_BASE}/status?trader_id=${traderId}`
: `${API_BASE}/status` : `${API_BASE}/status`
const result = await httpClient.get<SystemStatus>(url) const result = await httpClient.get<SystemStatus>(url)
if (!result.success) throw new Error('获取系统状态失败') if (!result.success) throw new Error('Failed to fetch system status')
return result.data! return result.data!
}, },
@@ -24,8 +24,7 @@ export const dataApi = {
? `${API_BASE}/account?trader_id=${traderId}` ? `${API_BASE}/account?trader_id=${traderId}`
: `${API_BASE}/account` : `${API_BASE}/account`
const result = await httpClient.get<AccountInfo>(url) const result = await httpClient.get<AccountInfo>(url)
if (!result.success) throw new Error('获取账户信息失败') if (!result.success) throw new Error('Failed to fetch account info')
console.log('Account data fetched:', result.data)
return result.data! return result.data!
}, },
@@ -34,7 +33,7 @@ export const dataApi = {
? `${API_BASE}/positions?trader_id=${traderId}` ? `${API_BASE}/positions?trader_id=${traderId}`
: `${API_BASE}/positions` : `${API_BASE}/positions`
const result = await httpClient.get<Position[]>(url) const result = await httpClient.get<Position[]>(url)
if (!result.success) throw new Error('获取持仓列表失败') if (!result.success) throw new Error('Failed to fetch positions')
return result.data! return result.data!
}, },
@@ -43,7 +42,7 @@ export const dataApi = {
? `${API_BASE}/decisions?trader_id=${traderId}` ? `${API_BASE}/decisions?trader_id=${traderId}`
: `${API_BASE}/decisions` : `${API_BASE}/decisions`
const result = await httpClient.get<DecisionRecord[]>(url) const result = await httpClient.get<DecisionRecord[]>(url)
if (!result.success) throw new Error('获取决策日志失败') if (!result.success) throw new Error('Failed to fetch decision logs')
return result.data! return result.data!
}, },
@@ -60,7 +59,7 @@ export const dataApi = {
const result = await httpClient.get<DecisionRecord[]>( const result = await httpClient.get<DecisionRecord[]>(
`${API_BASE}/decisions/latest?${params}` `${API_BASE}/decisions/latest?${params}`
) )
if (!result.success) throw new Error('获取最新决策失败') if (!result.success) throw new Error('Failed to fetch latest decisions')
return result.data! return result.data!
}, },
@@ -69,7 +68,7 @@ export const dataApi = {
? `${API_BASE}/statistics?trader_id=${traderId}` ? `${API_BASE}/statistics?trader_id=${traderId}`
: `${API_BASE}/statistics` : `${API_BASE}/statistics`
const result = await httpClient.get<Statistics>(url) const result = await httpClient.get<Statistics>(url)
if (!result.success) throw new Error('获取统计信息失败') if (!result.success) throw new Error('Failed to fetch statistics')
return result.data! return result.data!
}, },
@@ -78,7 +77,7 @@ export const dataApi = {
? `${API_BASE}/equity-history?trader_id=${traderId}` ? `${API_BASE}/equity-history?trader_id=${traderId}`
: `${API_BASE}/equity-history` : `${API_BASE}/equity-history`
const result = await httpClient.get<any[]>(url) const result = await httpClient.get<any[]>(url)
if (!result.success) throw new Error('获取历史数据失败') if (!result.success) throw new Error('Failed to fetch equity history')
return result.data! return result.data!
}, },
@@ -87,13 +86,13 @@ export const dataApi = {
`${API_BASE}/equity-history-batch`, `${API_BASE}/equity-history-batch`,
{ trader_ids: traderIds, hours: hours || 0 } { trader_ids: traderIds, hours: hours || 0 }
) )
if (!result.success) throw new Error('获取批量历史数据失败') if (!result.success) throw new Error('Failed to fetch batch equity history')
return result.data! return result.data!
}, },
async getTopTraders(): Promise<any[]> { async getTopTraders(): Promise<any[]> {
const result = await httpClient.get<any[]>(`${API_BASE}/top-traders`) const result = await httpClient.get<any[]>(`${API_BASE}/top-traders`)
if (!result.success) throw new Error('获取前5名交易员失败') if (!result.success) throw new Error('Failed to fetch top traders')
return result.data! return result.data!
}, },
@@ -101,7 +100,7 @@ export const dataApi = {
const result = await httpClient.get<any>( const result = await httpClient.get<any>(
`${API_BASE}/trader/${traderId}/config` `${API_BASE}/trader/${traderId}/config`
) )
if (!result.success) throw new Error('获取公开交易员配置失败') if (!result.success) throw new Error('Failed to fetch public trader config')
return result.data! return result.data!
}, },
@@ -109,7 +108,7 @@ export const dataApi = {
const result = await httpClient.get<CompetitionData>( const result = await httpClient.get<CompetitionData>(
`${API_BASE}/competition` `${API_BASE}/competition`
) )
if (!result.success) throw new Error('获取竞赛数据失败') if (!result.success) throw new Error('Failed to fetch competition data')
return result.data! return result.data!
}, },
@@ -117,7 +116,7 @@ export const dataApi = {
const result = await httpClient.get<PositionHistoryResponse>( const result = await httpClient.get<PositionHistoryResponse>(
`${API_BASE}/positions/history?trader_id=${traderId}&limit=${limit}` `${API_BASE}/positions/history?trader_id=${traderId}&limit=${limit}`
) )
if (!result.success) throw new Error('获取历史仓位失败') if (!result.success) throw new Error('Failed to fetch position history')
return result.data! return result.data!
}, },
} }
+1 -1
View File
@@ -31,7 +31,7 @@ export async function handleJSONResponse<T>(res: Response): Promise<T> {
} catch { } catch {
/* ignore JSON parse errors */ /* ignore JSON parse errors */
} }
throw new Error(message || '请求失败') throw new Error(message || 'Request failed')
} }
if (!text) { if (!text) {
return {} as T return {} as T
+9 -9
View File
@@ -7,26 +7,26 @@ import { API_BASE, httpClient } from './helpers'
export const strategyApi = { export const strategyApi = {
async getStrategies(): Promise<Strategy[]> { async getStrategies(): Promise<Strategy[]> {
const result = await httpClient.get<{ strategies: Strategy[] }>(`${API_BASE}/strategies`) const result = await httpClient.get<{ strategies: Strategy[] }>(`${API_BASE}/strategies`)
if (!result.success) throw new Error('获取策略列表失败') if (!result.success) throw new Error('Failed to fetch strategy list')
const strategies = result.data?.strategies const strategies = result.data?.strategies
return Array.isArray(strategies) ? strategies : [] return Array.isArray(strategies) ? strategies : []
}, },
async getStrategy(strategyId: string): Promise<Strategy> { async getStrategy(strategyId: string): Promise<Strategy> {
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/${strategyId}`) const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/${strategyId}`)
if (!result.success) throw new Error('获取策略失败') if (!result.success) throw new Error('Failed to fetch strategy')
return result.data! return result.data!
}, },
async getActiveStrategy(): Promise<Strategy> { async getActiveStrategy(): Promise<Strategy> {
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/active`) const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/active`)
if (!result.success) throw new Error('获取激活策略失败') if (!result.success) throw new Error('Failed to fetch active strategy')
return result.data! return result.data!
}, },
async getDefaultStrategyConfig(): Promise<StrategyConfig> { async getDefaultStrategyConfig(): Promise<StrategyConfig> {
const result = await httpClient.get<StrategyConfig>(`${API_BASE}/strategies/default-config`) const result = await httpClient.get<StrategyConfig>(`${API_BASE}/strategies/default-config`)
if (!result.success) throw new Error('获取默认策略配置失败') if (!result.success) throw new Error('Failed to fetch default strategy config')
return result.data! return result.data!
}, },
@@ -36,7 +36,7 @@ export const strategyApi = {
config: StrategyConfig config: StrategyConfig
}): Promise<Strategy> { }): Promise<Strategy> {
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies`, data) const result = await httpClient.post<Strategy>(`${API_BASE}/strategies`, data)
if (!result.success) throw new Error('创建策略失败') if (!result.success) throw new Error('Failed to create strategy')
return result.data! return result.data!
}, },
@@ -49,24 +49,24 @@ export const strategyApi = {
} }
): Promise<Strategy> { ): Promise<Strategy> {
const result = await httpClient.put<Strategy>(`${API_BASE}/strategies/${strategyId}`, data) const result = await httpClient.put<Strategy>(`${API_BASE}/strategies/${strategyId}`, data)
if (!result.success) throw new Error('更新策略失败') if (!result.success) throw new Error('Failed to update strategy')
return result.data! return result.data!
}, },
async deleteStrategy(strategyId: string): Promise<void> { async deleteStrategy(strategyId: string): Promise<void> {
const result = await httpClient.delete(`${API_BASE}/strategies/${strategyId}`) const result = await httpClient.delete(`${API_BASE}/strategies/${strategyId}`)
if (!result.success) throw new Error('删除策略失败') if (!result.success) throw new Error('Failed to delete strategy')
}, },
async activateStrategy(strategyId: string): Promise<Strategy> { async activateStrategy(strategyId: string): Promise<Strategy> {
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/activate`) const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/activate`)
if (!result.success) throw new Error('激活策略失败') if (!result.success) throw new Error('Failed to activate strategy')
return result.data! return result.data!
}, },
async duplicateStrategy(strategyId: string): Promise<Strategy> { async duplicateStrategy(strategyId: string): Promise<Strategy> {
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/duplicate`) const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/duplicate`)
if (!result.success) throw new Error('复制策略失败') if (!result.success) throw new Error('Failed to duplicate strategy')
return result.data! return result.data!
}, },
} }
+4 -4
View File
@@ -4,22 +4,22 @@ import { API_BASE, httpClient } from './helpers'
export const telegramApi = { export const telegramApi = {
async getTelegramConfig(): Promise<TelegramConfig> { async getTelegramConfig(): Promise<TelegramConfig> {
const result = await httpClient.get<TelegramConfig>(`${API_BASE}/telegram`) const result = await httpClient.get<TelegramConfig>(`${API_BASE}/telegram`)
if (!result.success) throw new Error('获取Telegram配置失败') if (!result.success) throw new Error('Failed to fetch Telegram config')
return result.data! return result.data!
}, },
async updateTelegramConfig(token: string, modelId?: string): Promise<void> { async updateTelegramConfig(token: string, modelId?: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/telegram`, { bot_token: token, model_id: modelId ?? '' }) const result = await httpClient.post(`${API_BASE}/telegram`, { bot_token: token, model_id: modelId ?? '' })
if (!result.success) throw new Error('保存Telegram配置失败') if (!result.success) throw new Error('Failed to save Telegram config')
}, },
async unbindTelegram(): Promise<void> { async unbindTelegram(): Promise<void> {
const result = await httpClient.delete(`${API_BASE}/telegram/binding`) const result = await httpClient.delete(`${API_BASE}/telegram/binding`)
if (!result.success) throw new Error('解绑Telegram失败') if (!result.success) throw new Error('Failed to unbind Telegram')
}, },
async updateTelegramModel(modelId: string): Promise<void> { async updateTelegramModel(modelId: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/telegram/model`, { model_id: modelId }) const result = await httpClient.post(`${API_BASE}/telegram/model`, { model_id: modelId })
if (!result.success) throw new Error('更新Telegram模型失败') if (!result.success) throw new Error('Failed to update Telegram model')
}, },
} }
+11 -11
View File
@@ -8,13 +8,13 @@ import { API_BASE, httpClient } from './helpers'
export const traderApi = { export const traderApi = {
async getTraders(): Promise<TraderInfo[]> { async getTraders(): Promise<TraderInfo[]> {
const result = await httpClient.get<TraderInfo[]>(`${API_BASE}/my-traders`) const result = await httpClient.get<TraderInfo[]>(`${API_BASE}/my-traders`)
if (!result.success) throw new Error('获取trader列表失败') if (!result.success) throw new Error('Failed to fetch trader list')
return Array.isArray(result.data) ? result.data : [] return Array.isArray(result.data) ? result.data : []
}, },
async getPublicTraders(): Promise<any[]> { async getPublicTraders(): Promise<any[]> {
const result = await httpClient.get<any[]>(`${API_BASE}/traders`) const result = await httpClient.get<any[]>(`${API_BASE}/traders`)
if (!result.success) throw new Error('获取公开trader列表失败') if (!result.success) throw new Error('Failed to fetch public trader list')
return result.data! return result.data!
}, },
@@ -23,25 +23,25 @@ export const traderApi = {
`${API_BASE}/traders`, `${API_BASE}/traders`,
request request
) )
if (!result.success) throw new Error('创建交易员失败') if (!result.success) throw new Error('Failed to create trader')
return result.data! return result.data!
}, },
async deleteTrader(traderId: string): Promise<void> { async deleteTrader(traderId: string): Promise<void> {
const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`) const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`)
if (!result.success) throw new Error('删除交易员失败') if (!result.success) throw new Error('Failed to delete trader')
}, },
async startTrader(traderId: string): Promise<void> { async startTrader(traderId: string): Promise<void> {
const result = await httpClient.post( const result = await httpClient.post(
`${API_BASE}/traders/${traderId}/start` `${API_BASE}/traders/${traderId}/start`
) )
if (!result.success) throw new Error('启动交易员失败') if (!result.success) throw new Error('Failed to start trader')
}, },
async stopTrader(traderId: string): Promise<void> { async stopTrader(traderId: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`) const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`)
if (!result.success) throw new Error('停止交易员失败') if (!result.success) throw new Error('Failed to stop trader')
}, },
async toggleCompetition(traderId: string, showInCompetition: boolean): Promise<void> { async toggleCompetition(traderId: string, showInCompetition: boolean): Promise<void> {
@@ -49,7 +49,7 @@ export const traderApi = {
`${API_BASE}/traders/${traderId}/competition`, `${API_BASE}/traders/${traderId}/competition`,
{ show_in_competition: showInCompetition } { show_in_competition: showInCompetition }
) )
if (!result.success) throw new Error('更新竞技场显示设置失败') if (!result.success) throw new Error('Failed to update competition visibility')
}, },
async closePosition(traderId: string, symbol: string, side: string): Promise<{ message: string }> { async closePosition(traderId: string, symbol: string, side: string): Promise<{ message: string }> {
@@ -57,7 +57,7 @@ export const traderApi = {
`${API_BASE}/traders/${traderId}/close-position`, `${API_BASE}/traders/${traderId}/close-position`,
{ symbol, side } { symbol, side }
) )
if (!result.success) throw new Error('平仓失败') if (!result.success) throw new Error('Failed to close position')
return result.data! return result.data!
}, },
@@ -69,14 +69,14 @@ export const traderApi = {
`${API_BASE}/traders/${traderId}/prompt`, `${API_BASE}/traders/${traderId}/prompt`,
{ custom_prompt: customPrompt } { custom_prompt: customPrompt }
) )
if (!result.success) throw new Error('更新自定义策略失败') if (!result.success) throw new Error('Failed to update custom prompt')
}, },
async getTraderConfig(traderId: string): Promise<TraderConfigData> { async getTraderConfig(traderId: string): Promise<TraderConfigData> {
const result = await httpClient.get<TraderConfigData>( const result = await httpClient.get<TraderConfigData>(
`${API_BASE}/traders/${traderId}/config` `${API_BASE}/traders/${traderId}/config`
) )
if (!result.success) throw new Error('获取交易员配置失败') if (!result.success) throw new Error('Failed to fetch trader config')
return result.data! return result.data!
}, },
@@ -88,7 +88,7 @@ export const traderApi = {
`${API_BASE}/traders/${traderId}`, `${API_BASE}/traders/${traderId}`,
request request
) )
if (!result.success) throw new Error('更新交易员失败') if (!result.success) throw new Error('Failed to update trader')
return result.data! return result.data!
}, },
} }
+4 -4
View File
@@ -1,14 +1,14 @@
import { notify } from './notify' import { notify } from './notify'
/** /**
* * Copy text to clipboard and show a toast notification.
*/ */
export async function copyWithToast(text: string, successMsg = '已复制') { export async function copyWithToast(text: string, successMsg = 'Copied') {
try { try {
if (navigator?.clipboard?.writeText) { if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
} else { } else {
// 兼容降级:创建临时文本域执行复制 // Fallback: create temporary textarea for copy
const el = document.createElement('textarea') const el = document.createElement('textarea')
el.value = text el.value = text
el.style.position = 'fixed' el.style.position = 'fixed'
@@ -22,7 +22,7 @@ export async function copyWithToast(text: string, successMsg = '已复制') {
return true return true
} catch (err) { } catch (err) {
console.error('Clipboard copy failed:', err) console.error('Clipboard copy failed:', err)
notify.error('复制失败') notify.error('Copy failed')
return false return false
} }
} }
+2 -1
View File
@@ -1,4 +1,5 @@
import { useLanguage } from '../contexts/LanguageContext' import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
export function DataPage() { export function DataPage() {
const { language } = useLanguage() const { language } = useLanguage()
@@ -7,7 +8,7 @@ export function DataPage() {
<div className="w-full h-[calc(100vh-64px)]"> <div className="w-full h-[calc(100vh-64px)]">
<iframe <iframe
src="https://nofxos.ai/dashboard" src="https://nofxos.ai/dashboard"
title={language === 'zh' ? '数据中心' : 'Data Center'} title={t('dataCenter', language)}
className="w-full h-full border-0" className="w-full h-full border-0"
allow="fullscreen" allow="fullscreen"
/> />
+19 -99
View File
@@ -20,6 +20,7 @@ import {
import { useLanguage } from '../contexts/LanguageContext' import { useLanguage } from '../contexts/LanguageContext'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { toast } from 'sonner' import { toast } from 'sonner'
import { t } from '../i18n/translations'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground' import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
interface PublicStrategy { interface PublicStrategy {
@@ -106,88 +107,7 @@ export function StrategyMarketPage() {
const [selectedCategory, setSelectedCategory] = useState<string>('all') const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [copiedId, setCopiedId] = useState<string | null>(null) const [copiedId, setCopiedId] = useState<string | null>(null)
const texts = { const tr = (key: string) => t(`strategyMarket.${key}`, language)
zh: {
title: '策略市场',
subtitle: 'STRATEGY MARKETPLACE',
description: '发现、学习并复用社区精英交易员的策略配置',
search: '搜索参数...',
all: '全部协议',
popular: '热门配置',
recent: '最新提交',
myStrategies: '我的库',
noStrategies: '无信号',
noStrategiesDesc: '当前频段未检测到策略信号',
author: 'OPERATOR',
createdAt: 'TIMESTAMP',
viewConfig: 'DECRYPT CONFIG',
hideConfig: 'ENCRYPT',
copyConfig: 'CLONE CONFIG',
copied: 'COPIED',
configHidden: 'ENCRYPTED',
configHiddenDesc: '配置参数已加密',
indicators: 'INDICATORS',
maxPositions: 'POS_LIMIT',
maxLeverage: 'LEV_MAX',
shareYours: 'UPLOAD_STRATEGY',
makePublic: 'PUBLISH',
loading: 'INITIALIZING...'
},
en: {
title: 'STRATEGY MARKET',
subtitle: 'GLOBAL STRATEGY DATABASE',
description: 'Discover, analyze, and clone high-performance trading algorithms',
search: 'SEARCH PARAMETERS...',
all: 'ALL PROTOCOLS',
popular: 'TRENDING',
recent: 'LATEST',
myStrategies: 'MY LIBRARY',
noStrategies: 'NO SIGNAL',
noStrategiesDesc: 'No strategic signals detected in this frequency',
author: 'OPERATOR',
createdAt: 'TIMESTAMP',
viewConfig: 'DECRYPT CONFIG',
hideConfig: 'ENCRYPT',
copyConfig: 'CLONE CONFIG',
copied: 'COPIED',
configHidden: 'ENCRYPTED',
configHiddenDesc: 'Configuration parameters encrypted',
indicators: 'INDICATORS',
maxPositions: 'POS_LIMIT',
maxLeverage: 'LEV_MAX',
shareYours: 'UPLOAD_STRATEGY',
makePublic: 'PUBLISH',
loading: 'INITIALIZING...'
},
id: {
title: 'PASAR STRATEGI',
subtitle: 'DATABASE STRATEGI GLOBAL',
description: 'Temukan, analisis, dan kloning algoritma trading berperforma tinggi',
search: 'CARI PARAMETER...',
all: 'SEMUA PROTOKOL',
popular: 'TREN',
recent: 'TERBARU',
myStrategies: 'PERPUSTAKAAN SAYA',
noStrategies: 'TIDAK ADA SINYAL',
noStrategiesDesc: 'Tidak ada sinyal strategis terdeteksi pada frekuensi ini',
author: 'OPERATOR',
createdAt: 'TIMESTAMP',
viewConfig: 'DEKRIPSI CONFIG',
hideConfig: 'ENKRIPSI',
copyConfig: 'KLON CONFIG',
copied: 'DISALIN',
configHidden: 'TERENKRIPSI',
configHiddenDesc: 'Parameter konfigurasi terenkripsi',
indicators: 'INDIKATOR',
maxPositions: 'BATAS_POS',
maxLeverage: 'LEV_MAKS',
shareYours: 'UNGGAH_STRATEGI',
makePublic: 'PUBLIKASI',
loading: 'MENGINISIALISASI...'
}
}
const t = texts[language]
// Fetch public strategies // Fetch public strategies
const { data: strategies, isLoading } = useSWR<PublicStrategy[]>( const { data: strategies, isLoading } = useSWR<PublicStrategy[]>(
@@ -218,7 +138,7 @@ export function StrategyMarketPage() {
try { try {
await navigator.clipboard.writeText(JSON.stringify(strategy.config, null, 2)) await navigator.clipboard.writeText(JSON.stringify(strategy.config, null, 2))
setCopiedId(strategy.id) setCopiedId(strategy.id)
toast.success(t.copied) toast.success(tr('copied'))
setTimeout(() => setCopiedId(null), 2000) setTimeout(() => setCopiedId(null), 2000)
} catch (err) { } catch (err) {
console.error('Failed to copy:', err) console.error('Failed to copy:', err)
@@ -271,16 +191,16 @@ export function StrategyMarketPage() {
<Database className="w-8 h-8 text-nofx-gold relative z-10" /> <Database className="w-8 h-8 text-nofx-gold relative z-10" />
</div> </div>
<div> <div>
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={t.title}> <h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={tr('title')}>
{t.title} {tr('title')}
</h1> </h1>
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1"> <p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
// {t.subtitle} // {tr('subtitle')}
</p> </p>
</div> </div>
</div> </div>
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4"> <p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
{t.description} {tr('description')}
</p> </p>
</div> </div>
@@ -295,7 +215,7 @@ export function StrategyMarketPage() {
</div> </div>
<input <input
type="text" type="text"
placeholder={t.search} placeholder={tr('search')}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono" className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
@@ -324,7 +244,7 @@ export function StrategyMarketPage() {
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }} transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/> />
)} )}
<span className="relative z-10">{t[cat as keyof typeof t]}</span> <span className="relative z-10">{tr(cat)}</span>
</button> </button>
))} ))}
</div> </div>
@@ -340,7 +260,7 @@ export function StrategyMarketPage() {
<Cpu size={24} className="text-nofx-gold/50" /> <Cpu size={24} className="text-nofx-gold/50" />
</div> </div>
</div> </div>
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{t.loading}</p> <p className="text-nofx-gold text-xs tracking-widest animate-pulse">{tr('loading')}</p>
<div className="flex gap-1"> <div className="flex gap-1">
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div> <div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div> <div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
@@ -357,9 +277,9 @@ export function StrategyMarketPage() {
<Activity className="w-16 h-16 text-zinc-700 relative z-10" /> <Activity className="w-16 h-16 text-zinc-700 relative z-10" />
</div> </div>
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2"> <h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
[{t.noStrategies}] [{tr('noStrategies')}]
</h3> </h3>
<p className="text-zinc-600 text-xs tracking-wide uppercase">{t.noStrategiesDesc}</p> <p className="text-zinc-600 text-xs tracking-wide uppercase">{tr('noStrategiesDesc')}</p>
</div> </div>
)} )}
@@ -423,11 +343,11 @@ export function StrategyMarketPage() {
{/* Meta Data */} {/* Meta Data */}
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600"> <div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-zinc-700 uppercase">{t.author}</span> <span className="text-zinc-700 uppercase">{tr('author')}</span>
<span className="text-zinc-400 group-hover:text-white transition-colors">@{strategy.author_email?.split('@')[0] || 'UNKNOWN'}</span> <span className="text-zinc-400 group-hover:text-white transition-colors">@{strategy.author_email?.split('@')[0] || 'UNKNOWN'}</span>
</div> </div>
<div className="flex flex-col text-right"> <div className="flex flex-col text-right">
<span className="text-zinc-700 uppercase">{t.createdAt}</span> <span className="text-zinc-700 uppercase">{tr('createdAt')}</span>
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span> <span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
</div> </div>
</div> </div>
@@ -468,7 +388,7 @@ export function StrategyMarketPage() {
) : ( ) : (
<div className="flex flex-col items-center justify-center h-full text-zinc-600"> <div className="flex flex-col items-center justify-center h-full text-zinc-600">
<EyeOff size={16} className="mb-1 opacity-50" /> <EyeOff size={16} className="mb-1 opacity-50" />
<span className="text-[9px] uppercase tracking-widest">{t.configHiddenDesc}</span> <span className="text-[9px] uppercase tracking-widest">{tr('configHiddenDesc')}</span>
</div> </div>
)} )}
</div> </div>
@@ -483,19 +403,19 @@ export function StrategyMarketPage() {
{copiedId === strategy.id ? ( {copiedId === strategy.id ? (
<> <>
<Check className="w-3 h-3 text-emerald-500" /> <Check className="w-3 h-3 text-emerald-500" />
<span className="text-emerald-500">{t.copied}</span> <span className="text-emerald-500">{tr('copied')}</span>
</> </>
) : ( ) : (
<> <>
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" /> <Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
{t.copyConfig} {tr('copyConfig')}
</> </>
)} )}
</button> </button>
) : ( ) : (
<button disabled className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2"> <button disabled className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2">
<Shield size={12} /> <Shield size={12} />
{t.hideConfig} {tr('hideConfig')}
</button> </button>
)} )}
</div> </div>
@@ -521,7 +441,7 @@ export function StrategyMarketPage() {
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all"> <div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
<Hexagon className="text-nofx-gold animate-spin-slow" size={24} /> <Hexagon className="text-nofx-gold animate-spin-slow" size={24} />
<div className="text-left"> <div className="text-left">
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{t.shareYours}</div> <div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{tr('shareYours')}</div>
<div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div> <div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div>
</div> </div>
<div className="w-[1px] h-8 bg-zinc-800 mx-2"></div> <div className="w-[1px] h-8 bg-zinc-800 mx-2"></div>
+68 -111
View File
@@ -39,6 +39,7 @@ import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEdito
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor' import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor' import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground' import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
import { t } from '../i18n/translations'
const API_BASE = import.meta.env.VITE_API_BASE || '' const API_BASE = import.meta.env.VITE_API_BASE || ''
@@ -108,7 +109,7 @@ export function StrategyStudioPage() {
}) })
if (response.ok) { if (response.ok) {
const data = await response.json() const data = await response.json()
// 后端返回的是数组,不是 { models: [] } // Backend returns an array, not { models: [] }
const allModels = Array.isArray(data) ? data : (data.models || []) const allModels = Array.isArray(data) ? data : (data.models || [])
const enabledModels = allModels.filter((m: AIModel) => m.enabled) const enabledModels = allModels.filter((m: AIModel) => m.enabled)
setAiModels(enabledModels) setAiModels(enabledModels)
@@ -209,7 +210,7 @@ export function StrategyStudioPage() {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
name: language === 'zh' ? '新策略' : 'New Strategy', name: tr('newStrategyName'),
description: '', description: '',
config: defaultConfig, config: defaultConfig,
}), }),
@@ -222,7 +223,7 @@ export function StrategyStudioPage() {
const now = new Date().toISOString() const now = new Date().toISOString()
const newStrategy = { const newStrategy = {
id: result.id, id: result.id,
name: language === 'zh' ? '新策略' : 'New Strategy', name: tr('newStrategyName'),
description: '', description: '',
is_active: false, is_active: false,
is_default: false, is_default: false,
@@ -246,11 +247,11 @@ export function StrategyStudioPage() {
if (!token) return if (!token) return
const confirmed = await confirmToast( const confirmed = await confirmToast(
language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?', tr('confirmDeleteStrategy'),
{ {
title: language === 'zh' ? '确认删除' : 'Confirm Delete', title: tr('confirmDelete'),
okText: language === 'zh' ? '删除' : 'Delete', okText: tr('delete'),
cancelText: language === 'zh' ? '取消' : 'Cancel', cancelText: tr('cancel'),
} }
) )
if (!confirmed) return if (!confirmed) return
@@ -261,7 +262,7 @@ export function StrategyStudioPage() {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}) })
if (!response.ok) throw new Error('Failed to delete strategy') if (!response.ok) throw new Error('Failed to delete strategy')
notify.success(language === 'zh' ? '策略已删除' : 'Strategy deleted') notify.success(tr('strategyDeleted'))
// Clear selection if deleted strategy was selected // Clear selection if deleted strategy was selected
if (selectedStrategy?.id === id) { if (selectedStrategy?.id === id) {
setSelectedStrategy(null) setSelectedStrategy(null)
@@ -287,7 +288,7 @@ export function StrategyStudioPage() {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
name: language === 'zh' ? '策略副本' : 'Strategy Copy', name: tr('strategyCopy'),
}), }),
}) })
if (!response.ok) throw new Error('Failed to duplicate strategy') if (!response.ok) throw new Error('Failed to duplicate strategy')
@@ -330,7 +331,7 @@ export function StrategyStudioPage() {
a.click() a.click()
document.body.removeChild(a) document.body.removeChild(a)
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
notify.success(language === 'zh' ? '策略已导出' : 'Strategy exported') notify.success(tr('strategyExported'))
} }
// Import strategy from JSON file // Import strategy from JSON file
@@ -344,7 +345,7 @@ export function StrategyStudioPage() {
// Validate imported data // Validate imported data
if (!importData.config || !importData.name) { if (!importData.config || !importData.name) {
throw new Error(language === 'zh' ? '无效的策略文件' : 'Invalid strategy file') throw new Error(tr('invalidStrategyFile'))
} }
// Create new strategy with imported config // Create new strategy with imported config
@@ -355,14 +356,14 @@ export function StrategyStudioPage() {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
name: `${importData.name} (${language === 'zh' ? '导入' : 'Imported'})`, name: `${importData.name} (${tr('imported')})`,
description: importData.description || '', description: importData.description || '',
config: importData.config, config: importData.config,
}), }),
}) })
if (!response.ok) throw new Error('Failed to import strategy') if (!response.ok) throw new Error('Failed to import strategy')
notify.success(language === 'zh' ? '策略已导入' : 'Strategy imported') notify.success(tr('strategyImported'))
await fetchStrategies() await fetchStrategies()
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Unknown error' const errorMsg = err instanceof Error ? err.message : 'Unknown error'
@@ -402,7 +403,7 @@ export function StrategyStudioPage() {
) )
if (!response.ok) throw new Error('Failed to save strategy') if (!response.ok) throw new Error('Failed to save strategy')
setHasChanges(false) setHasChanges(false)
notify.success(language === 'zh' ? '策略已保存' : 'Strategy saved') notify.success(tr('strategySaved'))
await fetchStrategies() await fetchStrategies()
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error') setError(err instanceof Error ? err.message : 'Unknown error')
@@ -482,51 +483,7 @@ export function StrategyStudioPage() {
} }
} }
const t = (key: string) => { const tr = (key: string) => t(`strategyStudio.${key}`, language)
const translations: Record<string, Record<string, string>> = {
strategyStudio: { zh: '策略工作室', en: 'Strategy Studio' },
subtitle: { zh: '可视化配置和测试交易策略', en: 'Configure and test trading strategies' },
strategies: { zh: '策略', en: 'Strategies' },
newStrategy: { zh: '新建', en: 'New' },
strategyType: { zh: '策略类型', en: 'Strategy Type' },
aiTrading: { zh: 'AI 智能交易', en: 'AI Trading' },
aiTradingDesc: { zh: 'AI 分析市场并自主决策买卖', en: 'AI analyzes market and makes trading decisions' },
gridTrading: { zh: 'AI 网格交易', en: 'AI Grid Trading' },
gridTradingDesc: { zh: 'AI 控制网格策略,在震荡市场获利', en: 'AI-controlled grid strategy for ranging markets' },
gridConfig: { zh: '网格配置', en: 'Grid Configuration' },
coinSource: { zh: '币种来源', en: 'Coin Source' },
indicators: { zh: '技术指标', en: 'Indicators' },
riskControl: { zh: '风控参数', en: 'Risk Control' },
promptSections: { zh: 'Prompt 编辑', en: 'Prompt Editor' },
customPrompt: { zh: '附加提示', en: 'Extra Prompt' },
save: { zh: '保存', en: 'Save' },
saving: { zh: '保存中...', en: 'Saving...' },
activate: { zh: '激活', en: 'Activate' },
active: { zh: '激活中', en: 'Active' },
default: { zh: '默认', en: 'Default' },
promptPreview: { zh: 'Prompt 预览', en: 'Prompt Preview' },
aiTestRun: { zh: 'AI 测试', en: 'AI Test' },
systemPrompt: { zh: 'System Prompt', en: 'System Prompt' },
userPrompt: { zh: 'User Prompt', en: 'User Prompt' },
loadPrompt: { zh: '生成 Prompt', en: 'Generate Prompt' },
refreshPrompt: { zh: '刷新', en: 'Refresh' },
promptVariant: { zh: '风格', en: 'Style' },
balanced: { zh: '平衡', en: 'Balanced' },
aggressive: { zh: '激进', en: 'Aggressive' },
conservative: { zh: '保守', en: 'Conservative' },
selectModel: { zh: '选择 AI 模型', en: 'Select AI Model' },
runTest: { zh: '运行 AI 测试', en: 'Run AI Test' },
running: { zh: '运行中...', en: 'Running...' },
aiOutput: { zh: 'AI 输出', en: 'AI Output' },
reasoning: { zh: '思维链', en: 'Reasoning' },
decisions: { zh: '决策', en: 'Decisions' },
duration: { zh: '耗时', en: 'Duration' },
noModel: { zh: '请先配置 AI 模型', en: 'Please configure AI model first' },
testNote: { zh: '使用真实 AI 模型测试,不执行交易', en: 'Test with real AI, no trading' },
publishSettings: { zh: '发布设置', en: 'Publish' },
}
return translations[key]?.[language] || key
}
if (isLoading) { if (isLoading) {
return ( return (
@@ -550,7 +507,7 @@ export function StrategyStudioPage() {
key: 'gridConfig' as const, key: 'gridConfig' as const,
icon: Activity, icon: Activity,
color: '#0ECB81', color: '#0ECB81',
title: t('gridConfig'), title: tr('gridConfig'),
forStrategyType: 'grid_trading' as const, forStrategyType: 'grid_trading' as const,
content: editingConfig?.grid_config && ( content: editingConfig?.grid_config && (
<GridConfigEditor <GridConfigEditor
@@ -566,7 +523,7 @@ export function StrategyStudioPage() {
key: 'coinSource' as const, key: 'coinSource' as const,
icon: Target, icon: Target,
color: '#F0B90B', color: '#F0B90B',
title: t('coinSource'), title: tr('coinSource'),
forStrategyType: 'ai_trading' as const, forStrategyType: 'ai_trading' as const,
content: editingConfig && ( content: editingConfig && (
<CoinSourceEditor <CoinSourceEditor
@@ -581,7 +538,7 @@ export function StrategyStudioPage() {
key: 'indicators' as const, key: 'indicators' as const,
icon: BarChart3, icon: BarChart3,
color: '#0ECB81', color: '#0ECB81',
title: t('indicators'), title: tr('indicators'),
forStrategyType: 'ai_trading' as const, forStrategyType: 'ai_trading' as const,
content: editingConfig && ( content: editingConfig && (
<IndicatorEditor <IndicatorEditor
@@ -596,7 +553,7 @@ export function StrategyStudioPage() {
key: 'riskControl' as const, key: 'riskControl' as const,
icon: Shield, icon: Shield,
color: '#F6465D', color: '#F6465D',
title: t('riskControl'), title: tr('riskControl'),
forStrategyType: 'ai_trading' as const, forStrategyType: 'ai_trading' as const,
content: editingConfig && ( content: editingConfig && (
<RiskControlEditor <RiskControlEditor
@@ -611,7 +568,7 @@ export function StrategyStudioPage() {
key: 'promptSections' as const, key: 'promptSections' as const,
icon: FileText, icon: FileText,
color: '#a855f7', color: '#a855f7',
title: t('promptSections'), title: tr('promptSections'),
forStrategyType: 'ai_trading' as const, forStrategyType: 'ai_trading' as const,
content: editingConfig && ( content: editingConfig && (
<PromptSectionsEditor <PromptSectionsEditor
@@ -626,18 +583,18 @@ export function StrategyStudioPage() {
key: 'customPrompt' as const, key: 'customPrompt' as const,
icon: Settings, icon: Settings,
color: '#60a5fa', color: '#60a5fa',
title: t('customPrompt'), title: tr('customPrompt'),
forStrategyType: 'ai_trading' as const, forStrategyType: 'ai_trading' as const,
content: editingConfig && ( content: editingConfig && (
<div> <div>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{language === 'zh' ? '附加在 System Prompt 末尾的额外提示,用于补充个性化交易风格' : 'Extra prompt appended to System Prompt for personalized trading style'} {tr('customPromptDesc')}
</p> </p>
<textarea <textarea
value={editingConfig.custom_prompt || ''} value={editingConfig.custom_prompt || ''}
onChange={(e) => updateConfig('custom_prompt', e.target.value)} onChange={(e) => updateConfig('custom_prompt', e.target.value)}
disabled={selectedStrategy?.is_default} disabled={selectedStrategy?.is_default}
placeholder={language === 'zh' ? '输入自定义提示词...' : 'Enter custom prompt...'} placeholder={tr('customPromptPlaceholder')}
className="w-full h-32 px-3 py-2 rounded-lg resize-none font-mono text-xs" className="w-full h-32 px-3 py-2 rounded-lg resize-none font-mono text-xs"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/> />
@@ -648,7 +605,7 @@ export function StrategyStudioPage() {
key: 'publishSettings' as const, key: 'publishSettings' as const,
icon: Globe, icon: Globe,
color: '#0ECB81', color: '#0ECB81',
title: t('publishSettings'), title: tr('publishSettings'),
forStrategyType: 'both' as const, forStrategyType: 'both' as const,
content: selectedStrategy && ( content: selectedStrategy && (
<PublishSettingsEditor <PublishSettingsEditor
@@ -683,8 +640,8 @@ export function StrategyStudioPage() {
<Sparkles className="w-5 h-5 text-black" /> <Sparkles className="w-5 h-5 text-black" />
</div> </div>
<div> <div>
<h1 className="text-lg font-bold text-nofx-text">{t('strategyStudio')}</h1> <h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
<p className="text-xs text-nofx-text-muted">{t('subtitle')}</p> <p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
</div> </div>
</div> </div>
{error && ( {error && (
@@ -702,10 +659,10 @@ export function StrategyStudioPage() {
<div className="w-48 flex-shrink-0 border-r border-nofx-gold/20 overflow-y-auto bg-nofx-bg/30 backdrop-blur-sm z-10"> <div className="w-48 flex-shrink-0 border-r border-nofx-gold/20 overflow-y-auto bg-nofx-bg/30 backdrop-blur-sm z-10">
<div className="p-2"> <div className="p-2">
<div className="flex items-center justify-between mb-2 px-2"> <div className="flex items-center justify-between mb-2 px-2">
<span className="text-xs font-medium text-nofx-text-muted">{t('strategies')}</span> <span className="text-xs font-medium text-nofx-text-muted">{tr('strategies')}</span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{/* Import button with hidden file input */} {/* Import button with hidden file input */}
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer text-nofx-text-muted hover:text-white" title={language === 'zh' ? '导入策略' : 'Import Strategy'}> <label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer text-nofx-text-muted hover:text-white" title={tr('importStrategy')}>
<Upload className="w-4 h-4" /> <Upload className="w-4 h-4" />
<input <input
type="file" type="file"
@@ -717,7 +674,7 @@ export function StrategyStudioPage() {
<button <button
onClick={handleCreateStrategy} onClick={handleCreateStrategy}
className="p-1 rounded hover:bg-white/10 transition-colors text-nofx-gold" className="p-1 rounded hover:bg-white/10 transition-colors text-nofx-gold"
title={language === 'zh' ? '新建策略' : 'New Strategy'} title={tr('newStrategyTooltip')}
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
</button> </button>
@@ -745,7 +702,7 @@ export function StrategyStudioPage() {
<button <button
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }} onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white" className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
title={language === 'zh' ? '导出' : 'Export'} title={tr('export')}
> >
<Download className="w-3 h-3" /> <Download className="w-3 h-3" />
</button> </button>
@@ -754,14 +711,14 @@ export function StrategyStudioPage() {
<button <button
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }} onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white" className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
title={language === 'zh' ? '复制' : 'Duplicate'} title={tr('duplicate')}
> >
<Copy className="w-3 h-3" /> <Copy className="w-3 h-3" />
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }} onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger" className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
title={language === 'zh' ? '删除' : 'Delete'} title={tr('deleteTooltip')}
> >
<Trash2 className="w-3 h-3" /> <Trash2 className="w-3 h-3" />
</button> </button>
@@ -772,18 +729,18 @@ export function StrategyStudioPage() {
<div className="flex items-center gap-1 mt-1 flex-wrap"> <div className="flex items-center gap-1 mt-1 flex-wrap">
{strategy.is_active && ( {strategy.is_active && (
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-success/15 text-nofx-success"> <span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-success/15 text-nofx-success">
{t('active')} {tr('active')}
</span> </span>
)} )}
{strategy.is_default && ( {strategy.is_default && (
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-gold/15 text-nofx-gold"> <span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-gold/15 text-nofx-gold">
{t('default')} {tr('default')}
</span> </span>
)} )}
{strategy.is_public && ( {strategy.is_public && (
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5 bg-blue-400/15 text-blue-400"> <span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5 bg-blue-400/15 text-blue-400">
<Globe className="w-2.5 h-2.5" /> <Globe className="w-2.5 h-2.5" />
{language === 'zh' ? '公开' : 'Public'} {tr('public')}
</span> </span>
)} )}
</div> </div>
@@ -818,11 +775,11 @@ export function StrategyStudioPage() {
setHasChanges(true) setHasChanges(true)
}} }}
disabled={selectedStrategy.is_default} disabled={selectedStrategy.is_default}
placeholder={language === 'zh' ? '添加策略简介...' : 'Add strategy description...'} placeholder={tr('addDescription')}
className="text-xs bg-transparent border-none outline-none w-full text-nofx-text-muted placeholder-nofx-text-muted/50 mt-1" className="text-xs bg-transparent border-none outline-none w-full text-nofx-text-muted placeholder-nofx-text-muted/50 mt-1"
/> />
{hasChanges && ( {hasChanges && (
<span className="text-xs text-nofx-gold"> {language === 'zh' ? '未保存' : 'Unsaved'}</span> <span className="text-xs text-nofx-gold"> {tr('unsaved')}</span>
)} )}
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
@@ -832,7 +789,7 @@ export function StrategyStudioPage() {
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors bg-nofx-success/10 border border-nofx-success/30 text-nofx-success hover:bg-nofx-success/20" className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors bg-nofx-success/10 border border-nofx-success/30 text-nofx-success hover:bg-nofx-success/20"
> >
<Check className="w-3 h-3" /> <Check className="w-3 h-3" />
{t('activate')} {tr('activate')}
</button> </button>
)} )}
{!selectedStrategy.is_default && ( {!selectedStrategy.is_default && (
@@ -843,7 +800,7 @@ export function StrategyStudioPage() {
${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`} ${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`}
> >
<Save className="w-3 h-3" /> <Save className="w-3 h-3" />
{isSaving ? t('saving') : t('save')} {isSaving ? tr('saving') : tr('save')}
</button> </button>
)} )}
</div> </div>
@@ -854,7 +811,7 @@ export function StrategyStudioPage() {
<div className="mb-4 p-4 rounded-lg bg-nofx-bg-lighter border border-nofx-gold/20"> <div className="mb-4 p-4 rounded-lg bg-nofx-bg-lighter border border-nofx-gold/20">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} /> <Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium text-nofx-text">{t('strategyType')}</span> <span className="text-sm font-medium text-nofx-text">{tr('strategyType')}</span>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<button <button
@@ -874,9 +831,9 @@ export function StrategyStudioPage() {
> >
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Bot className="w-4 h-4" style={{ color: '#F0B90B' }} /> <Bot className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium text-nofx-text">{t('aiTrading')}</span> <span className="text-sm font-medium text-nofx-text">{tr('aiTrading')}</span>
</div> </div>
<p className="text-xs text-nofx-text-muted text-left">{t('aiTradingDesc')}</p> <p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -897,9 +854,9 @@ export function StrategyStudioPage() {
> >
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} /> <Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
<span className="text-sm font-medium text-nofx-text">{t('gridTrading')}</span> <span className="text-sm font-medium text-nofx-text">{tr('gridTrading')}</span>
</div> </div>
<p className="text-xs text-nofx-text-muted text-left">{t('gridTradingDesc')}</p> <p className="text-xs text-nofx-text-muted text-left">{tr('gridTradingDesc')}</p>
</button> </button>
</div> </div>
</div> </div>
@@ -940,7 +897,7 @@ export function StrategyStudioPage() {
<div className="text-center"> <div className="text-center">
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" /> <Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" />
<p className="text-sm text-nofx-text-muted"> <p className="text-sm text-nofx-text-muted">
{language === 'zh' ? '选择或创建策略' : 'Select or create a strategy'} {tr('selectOrCreate')}
</p> </p>
</div> </div>
</div> </div>
@@ -957,7 +914,7 @@ export function StrategyStudioPage() {
}`} }`}
> >
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
{t('promptPreview')} {tr('promptPreview')}
</button> </button>
<button <button
onClick={() => setActiveRightTab('test')} onClick={() => setActiveRightTab('test')}
@@ -965,7 +922,7 @@ export function StrategyStudioPage() {
}`} }`}
> >
<Play className="w-4 h-4" /> <Play className="w-4 h-4" />
{t('aiTestRun')} {tr('aiTestRun')}
</button> </button>
</div> </div>
@@ -981,9 +938,9 @@ export function StrategyStudioPage() {
onChange={(e) => setSelectedVariant(e.target.value)} onChange={(e) => setSelectedVariant(e.target.value)}
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text outline-none focus:border-nofx-gold" className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text outline-none focus:border-nofx-gold"
> >
<option value="balanced">{t('balanced')}</option> <option value="balanced">{tr('balanced')}</option>
<option value="aggressive">{t('aggressive')}</option> <option value="aggressive">{tr('aggressive')}</option>
<option value="conservative">{t('conservative')}</option> <option value="conservative">{tr('conservative')}</option>
</select> </select>
<button <button
onClick={fetchPromptPreview} onClick={fetchPromptPreview}
@@ -991,7 +948,7 @@ export function StrategyStudioPage() {
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 bg-purple-600 hover:bg-purple-700 text-white" className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 bg-purple-600 hover:bg-purple-700 text-white"
> >
{isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />} {isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
{promptPreview ? t('refreshPrompt') : t('loadPrompt')} {promptPreview ? tr('refreshPrompt') : tr('loadPrompt')}
</button> </button>
</div> </div>
@@ -1018,7 +975,7 @@ export function StrategyStudioPage() {
<div className="flex items-center justify-between mb-1.5"> <div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<FileText className="w-3 h-3 text-purple-500" /> <FileText className="w-3 h-3 text-purple-500" />
<span className="text-xs font-medium text-nofx-text">{t('systemPrompt')}</span> <span className="text-xs font-medium text-nofx-text">{tr('systemPrompt')}</span>
</div> </div>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-lighter text-nofx-text-muted"> <span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-lighter text-nofx-text-muted">
{promptPreview.system_prompt.length.toLocaleString()} chars {promptPreview.system_prompt.length.toLocaleString()} chars
@@ -1035,7 +992,7 @@ export function StrategyStudioPage() {
) : ( ) : (
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted"> <div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
<Eye className="w-10 h-10 mb-2 opacity-30" /> <Eye className="w-10 h-10 mb-2 opacity-30" />
<p className="text-sm">{language === 'zh' ? '点击生成 Prompt 预览' : 'Click to generate prompt preview'}</p> <p className="text-sm">{tr('generatePromptPreview')}</p>
</div> </div>
)} )}
</div> </div>
@@ -1046,7 +1003,7 @@ export function StrategyStudioPage() {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Bot className="w-4 h-4 text-green-500" /> <Bot className="w-4 h-4 text-green-500" />
<span className="text-xs font-medium text-nofx-text">{t('selectModel')}</span> <span className="text-xs font-medium text-nofx-text">{tr('selectModel')}</span>
</div> </div>
{aiModels.length > 0 ? ( {aiModels.length > 0 ? (
<select <select
@@ -1062,7 +1019,7 @@ export function StrategyStudioPage() {
</select> </select>
) : ( ) : (
<div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger"> <div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger">
{t('noModel')} {tr('noModel')}
</div> </div>
)} )}
@@ -1072,9 +1029,9 @@ export function StrategyStudioPage() {
onChange={(e) => setSelectedVariant(e.target.value)} onChange={(e) => setSelectedVariant(e.target.value)}
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text" className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
> >
<option value="balanced">{t('balanced')}</option> <option value="balanced">{tr('balanced')}</option>
<option value="aggressive">{t('aggressive')}</option> <option value="aggressive">{tr('aggressive')}</option>
<option value="conservative">{t('conservative')}</option> <option value="conservative">{tr('conservative')}</option>
</select> </select>
<button <button
onClick={runAiTest} onClick={runAiTest}
@@ -1084,17 +1041,17 @@ export function StrategyStudioPage() {
{isRunningAiTest ? ( {isRunningAiTest ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
{t('running')} {tr('running')}
</> </>
) : ( ) : (
<> <>
<Send className="w-4 h-4" /> <Send className="w-4 h-4" />
{t('runTest')} {tr('runTest')}
</> </>
)} )}
</button> </button>
</div> </div>
<p className="text-[10px] text-nofx-text-muted">{t('testNote')}</p> <p className="text-[10px] text-nofx-text-muted">{tr('testNote')}</p>
</div> </div>
{/* Test Results */} {/* Test Results */}
@@ -1110,7 +1067,7 @@ export function StrategyStudioPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className="w-3 h-3 text-nofx-text-muted" /> <Clock className="w-3 h-3 text-nofx-text-muted" />
<span className="text-xs text-nofx-text-muted"> <span className="text-xs text-nofx-text-muted">
{t('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s {tr('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
</span> </span>
</div> </div>
)} )}
@@ -1120,7 +1077,7 @@ export function StrategyStudioPage() {
<div> <div>
<div className="flex items-center gap-1.5 mb-1.5"> <div className="flex items-center gap-1.5 mb-1.5">
<Terminal className="w-3 h-3 text-blue-400" /> <Terminal className="w-3 h-3 text-blue-400" />
<span className="text-xs font-medium text-nofx-text">{t('userPrompt')} (Input)</span> <span className="text-xs font-medium text-nofx-text">{tr('userPrompt')} (Input)</span>
</div> </div>
<pre <pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text" className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
@@ -1136,7 +1093,7 @@ export function StrategyStudioPage() {
<div> <div>
<div className="flex items-center gap-1.5 mb-1.5"> <div className="flex items-center gap-1.5 mb-1.5">
<Sparkles className="w-3 h-3 text-nofx-gold" /> <Sparkles className="w-3 h-3 text-nofx-gold" />
<span className="text-xs font-medium text-nofx-text">{t('reasoning')}</span> <span className="text-xs font-medium text-nofx-text">{tr('reasoning')}</span>
</div> </div>
<pre <pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/30 text-nofx-text" className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/30 text-nofx-text"
@@ -1152,7 +1109,7 @@ export function StrategyStudioPage() {
<div> <div>
<div className="flex items-center gap-1.5 mb-1.5"> <div className="flex items-center gap-1.5 mb-1.5">
<Activity className="w-3 h-3 text-green-500" /> <Activity className="w-3 h-3 text-green-500" />
<span className="text-xs font-medium text-nofx-text">{t('decisions')}</span> <span className="text-xs font-medium text-nofx-text">{tr('decisions')}</span>
</div> </div>
<pre <pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-green-500/30 text-nofx-text" className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-green-500/30 text-nofx-text"
@@ -1168,7 +1125,7 @@ export function StrategyStudioPage() {
<div> <div>
<div className="flex items-center gap-1.5 mb-1.5"> <div className="flex items-center gap-1.5 mb-1.5">
<FileText className="w-3 h-3 text-nofx-text-muted" /> <FileText className="w-3 h-3 text-nofx-text-muted" />
<span className="text-xs font-medium text-nofx-text">{t('aiOutput')} (Raw)</span> <span className="text-xs font-medium text-nofx-text">{tr('aiOutput')} (Raw)</span>
</div> </div>
<pre <pre
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/20 text-nofx-text" className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
@@ -1184,7 +1141,7 @@ export function StrategyStudioPage() {
) : ( ) : (
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted"> <div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
<Play className="w-10 h-10 mb-2 opacity-30" /> <Play className="w-10 h-10 mb-2 opacity-30" />
<p className="text-sm">{language === 'zh' ? '点击运行 AI 测试' : 'Click to run AI test'}</p> <p className="text-sm">{tr('runAiTestHint')}</p>
</div> </div>
)} )}
</div> </div>
+29 -43
View File
@@ -23,7 +23,7 @@ import type {
// --- Helper Functions --- // --- Helper Functions ---
// 获取友好的AI模型名称 // Get friendly AI model display name
function getModelDisplayName(modelId: string): string { function getModelDisplayName(modelId: string): string {
switch (modelId.toLowerCase()) { switch (modelId.toLowerCase()) {
case 'deepseek': case 'deepseek':
@@ -189,19 +189,17 @@ export function TraderDashboardPage({
}, 100) }, 100)
} }
// 平仓操作 // Close position handler
const handleClosePosition = async (symbol: string, side: string) => { const handleClosePosition = async (symbol: string, side: string) => {
if (!selectedTraderId) return if (!selectedTraderId) return
const confirmMsg = const sideLabel = side === 'LONG' ? 'LONG' : 'SHORT'
language === 'zh' const confirmMsg = t('traderDashboard.confirmClosePosition', language, { symbol, side: sideLabel })
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
const confirmed = await confirmToast(confirmMsg, { const confirmed = await confirmToast(confirmMsg, {
title: language === 'zh' ? '确认平仓' : 'Confirm Close', title: t('traderDashboard.confirmClose', language),
okText: language === 'zh' ? '确认' : 'Confirm', okText: t('traderDashboard.confirm', language),
cancelText: language === 'zh' ? '取消' : 'Cancel', cancelText: t('traderDashboard.cancel', language),
}) })
if (!confirmed) return if (!confirmed) return
@@ -209,10 +207,8 @@ export function TraderDashboardPage({
setClosingPosition(symbol) setClosingPosition(symbol)
try { try {
await api.closePosition(selectedTraderId, symbol, side) await api.closePosition(selectedTraderId, symbol, side)
notify.success( notify.success(t('traderDashboard.positionClosed', language))
language === 'zh' ? '平仓成功' : 'Position closed successfully' // Use SWR mutate to refresh data instead of reloading page
)
// 使用 SWR mutate 刷新数据而非重新加载页面
await Promise.all([ await Promise.all([
mutate(`positions-${selectedTraderId}`), mutate(`positions-${selectedTraderId}`),
mutate(`account-${selectedTraderId}`), mutate(`account-${selectedTraderId}`),
@@ -221,9 +217,7 @@ export function TraderDashboardPage({
const errorMsg = const errorMsg =
err instanceof Error err instanceof Error
? err.message ? err.message
: language === 'zh' : t('traderDashboard.closeFailed', language)
? '平仓失败'
: 'Failed to close position'
notify.error(errorMsg) notify.error(errorMsg)
} finally { } finally {
setClosingPosition(null) setClosingPosition(null)
@@ -257,18 +251,16 @@ export function TraderDashboardPage({
</svg> </svg>
</div> </div>
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main"> <h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
{language === 'zh' ? '无法连接到服务器' : 'Connection Failed'} {t('traderDashboard.connectionFailed', language)}
</h2> </h2>
<p className="text-base mb-6 text-nofx-text-muted"> <p className="text-base mb-6 text-nofx-text-muted">
{language === 'zh' {t('traderDashboard.connectionFailedDesc', language)}
? '请确认后端服务已启动。'
: 'Please check if the backend service is running.'}
</p> </p>
<button <button
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10" className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10"
> >
{language === 'zh' ? '重试' : 'Retry'} {t('traderDashboard.retry', language)}
</button> </button>
</div> </div>
</div> </div>
@@ -414,12 +406,8 @@ export function TraderDashboardPage({
className="p-1 rounded hover:bg-white/10 transition-colors" className="p-1 rounded hover:bg-white/10 transition-colors"
title={ title={
showWalletAddress showWalletAddress
? language === 'zh' ? t('traderDashboard.hideAddress', language)
? '隐藏地址' : t('traderDashboard.showFullAddress', language)
: 'Hide address'
: language === 'zh'
? '显示完整地址'
: 'Show full address'
} }
> >
{showWalletAddress ? ( {showWalletAddress ? (
@@ -432,7 +420,7 @@ export function TraderDashboardPage({
type="button" type="button"
onClick={handleCopyAddress} onClick={handleCopyAddress}
className="p-1 rounded hover:bg-white/10 transition-colors" className="p-1 rounded hover:bg-white/10 transition-colors"
title={language === 'zh' ? '复制地址' : 'Copy address'} title={t('traderDashboard.copyAddress', language)}
> >
{copiedAddress ? ( {copiedAddress ? (
<Check className="w-3.5 h-3.5 text-nofx-green" /> <Check className="w-3.5 h-3.5 text-nofx-green" />
@@ -443,7 +431,7 @@ export function TraderDashboardPage({
</> </>
) : ( ) : (
<span className="text-xs text-nofx-text-muted"> <span className="text-xs text-nofx-text-muted">
{language === 'zh' ? '未配置地址' : 'No address configured'} {t('traderDashboard.noAddressConfigured', language)}
</span> </span>
)} )}
</div> </div>
@@ -599,14 +587,14 @@ export function TraderDashboardPage({
<tr> <tr>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left">{t('symbol', language)}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left">{t('symbol', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('side', language)}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('side', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{language === 'zh' ? '操作' : 'Action'}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('traderDashboard.action', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('entryPrice', language)}>{language === 'zh' ? '入场价' : 'Entry'}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('entryPrice', language)}>{t('traderDashboard.entry', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('markPrice', language)}>{language === 'zh' ? '标记价' : 'Mark'}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('markPrice', language)}>{t('traderDashboard.mark', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{language === 'zh' ? '数量' : 'Qty'}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{t('traderDashboard.qty', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('positionValue', language)}>{language === 'zh' ? '价值' : 'Value'}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('positionValue', language)}>{t('traderDashboard.value', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center hidden md:table-cell" title={t('leverage', language)}>{language === 'zh' ? '杠杆' : 'Lev.'}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center hidden md:table-cell" title={t('leverage', language)}>{t('traderDashboard.lev', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{language === 'zh' ? '未实现盈亏' : 'uPnL'}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{t('traderDashboard.uPnL', language)}</th>
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('liqPrice', language)}>{language === 'zh' ? '强平价' : 'Liq.'}</th> <th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('liqPrice', language)}>{t('traderDashboard.liq', language)}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -644,14 +632,14 @@ export function TraderDashboardPage({
}} }}
disabled={closingPosition === pos.symbol} disabled={closingPosition === pos.symbol}
className="inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto bg-nofx-red/10 text-nofx-red border border-nofx-red/30 hover:bg-nofx-red/20" className="inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto bg-nofx-red/10 text-nofx-red border border-nofx-red/30 hover:bg-nofx-red/20"
title={language === 'zh' ? '平仓' : 'Close Position'} title={t('traderDashboard.closePosition', language)}
> >
{closingPosition === pos.symbol ? ( {closingPosition === pos.symbol ? (
<Loader2 className="w-3 h-3 animate-spin" /> <Loader2 className="w-3 h-3 animate-spin" />
) : ( ) : (
<LogOut className="w-3 h-3" /> <LogOut className="w-3 h-3" />
)} )}
{language === 'zh' ? '平仓' : 'Close'} {t('traderDashboard.close', language)}
</button> </button>
</td> </td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.entry_price)}</td> <td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.entry_price)}</td>
@@ -678,13 +666,11 @@ export function TraderDashboardPage({
{totalPositions > 10 && ( {totalPositions > 10 && (
<div className="flex flex-wrap items-center justify-between gap-3 pt-4 mt-4 text-xs border-t border-white/5 text-nofx-text-muted"> <div className="flex flex-wrap items-center justify-between gap-3 pt-4 mt-4 text-xs border-t border-white/5 text-nofx-text-muted">
<span> <span>
{language === 'zh' {t('traderDashboard.showingPositions', language, { shown: paginatedPositions.length, total: totalPositions })}
? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓`
: `Showing ${paginatedPositions.length} of ${totalPositions} positions`}
</span> </span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{language === 'zh' ? '每页' : 'Per page'}:</span> <span>{t('traderDashboard.perPage', language)}:</span>
<select <select
value={positionsPageSize} value={positionsPageSize}
onChange={(e) => setPositionsPageSize(Number(e.target.value))} onChange={(e) => setPositionsPageSize(Number(e.target.value))}