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
# 数据库配置 - SQLite(默认)
# Database configuration - SQLite (default)
DB_TYPE=sqlite
DB_PATH=data/data.db
+19 -19
View File
@@ -1,7 +1,7 @@
name: PR Docker Build Check
# PR 时只做轻量级构建检查,不推送镜像
# 策略: 快速验证 amd64 + 抽样检查 arm64 (backend only)
# Lightweight build check on PR only, no image push
# Strategy: Quick verify amd64 + spot check arm64 (backend only)
on:
pull_request:
branches:
@@ -18,7 +18,7 @@ on:
- '.github/workflows/pr-docker-check.yml'
jobs:
# 快速检查: 所有镜像的 amd64 版本
# Quick check: amd64 builds for all images
docker-build-amd64:
name: Build Check (amd64)
runs-on: ubuntu-22.04
@@ -31,7 +31,7 @@ jobs:
include:
- name: backend
dockerfile: ./docker/Dockerfile.backend
test_run: true # 需要测试运行
test_run: true # Needs test run
- name: frontend
dockerfile: ./docker/Dockerfile.frontend
test_run: true
@@ -51,7 +51,7 @@ jobs:
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
push: false
load: true # 加载到本地 Docker,用于测试运行
load: true # Load into local Docker for test run
tags: nofx-${{ matrix.name }}:pr-test
cache-from: type=gha,scope=${{ matrix.name }}-amd64
cache-to: type=gha,mode=max,scope=${{ matrix.name }}-amd64
@@ -66,12 +66,12 @@ jobs:
run: |
echo "🧪 Testing container startup..."
# 启动容器
# Start container
docker run -d --name test-${{ matrix.name }} \
--health-cmd="exit 0" \
nofx-${{ matrix.name }}:pr-test
# 等待容器启动 (最多 30 秒)
# Wait for container to start (up to 30 seconds)
for i in {1..30}; do
if docker ps | grep -q test-${{ matrix.name }}; then
echo "✅ Container started successfully"
@@ -93,7 +93,7 @@ jobs:
echo "📦 Image size: ${SIZE_MB} MB"
# 警告阈值
# Warning thresholds
if [ "${{ matrix.name }}" = "backend" ] && [ $SIZE_MB -gt 500 ]; then
echo "⚠️ Warning: Backend image is larger than 500MB"
elif [ "${{ matrix.name }}" = "frontend" ] && [ $SIZE_MB -gt 200 ]; then
@@ -102,10 +102,10 @@ jobs:
echo "✅ Image size is reasonable"
fi
# ARM64 原生构建检查: 使用 GitHub 原生 ARM64 runner (快速!)
# ARM64 native build check: Uses GitHub native ARM64 runner (fast!)
docker-build-arm64-native:
name: Build Check (arm64 native - backend)
runs-on: ubuntu-22.04-arm # 原生 ARM64 runner
runs-on: ubuntu-22.04-arm # Native ARM64 runner
permissions:
contents: read
@@ -113,19 +113,19 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
# 原生 ARM64 不需要 QEMU,直接构建
# Native ARM64 does not need QEMU, builds directly
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build backend image (arm64 native)
uses: docker/build-push-action@v5
timeout-minutes: 15 # 原生构建更快!
timeout-minutes: 15 # Native builds are faster!
with:
context: .
file: ./docker/Dockerfile.backend
platforms: linux/arm64
push: false
load: true # 加载到本地,用于测试
load: true # Load locally for testing
tags: nofx-backend:pr-test-arm64
cache-from: type=gha,scope=backend-arm64
cache-to: type=gha,mode=max,scope=backend-arm64
@@ -139,12 +139,12 @@ jobs:
run: |
echo "🧪 Testing ARM64 container startup..."
# 启动容器
# Start container
docker run -d --name test-backend-arm64 \
--health-cmd="exit 0" \
nofx-backend:pr-test-arm64
# 等待启动
# Wait for startup
for i in {1..30}; do
if docker ps | grep -q test-backend-arm64; then
echo "✅ ARM64 container started successfully"
@@ -165,14 +165,14 @@ jobs:
echo "Using GitHub native ARM64 runner - no QEMU needed!"
echo "Build time is ~3x faster than emulation"
# 汇总检查结果
# Aggregate check results
check-summary:
name: Docker Build Summary
needs: [docker-build-amd64, docker-build-arm64-native]
runs-on: ubuntu-22.04
if: always()
permissions:
pull-requests: write # 用于发布评论
pull-requests: write # For posting comments
steps:
- name: Check build results
id: check
@@ -180,7 +180,7 @@ jobs:
echo "## 🐳 Docker Build Check Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# 检查 amd64 构建
# Check amd64 build
if [[ "${{ needs.docker-build-amd64.result }}" == "success" ]]; then
echo "✅ **AMD64 builds**: All passed" >> $GITHUB_STEP_SUMMARY
AMD64_OK=true
@@ -189,7 +189,7 @@ jobs:
AMD64_OK=false
fi
# 检查 arm64 构建
# Check arm64 build
if [[ "${{ needs.docker-build-arm64-native.result }}" == "success" ]]; then
echo "✅ **ARM64 build** (native): Backend passed (frontend will be verified after merge)" >> $GITHUB_STEP_SUMMARY
ARM64_OK=true
@@ -1,6 +1,6 @@
name: PR Docker Compose Healthcheck
# 驗證 docker-compose.yml healthcheck 配置在 Alpine 容器中正常工作
# Verify docker-compose.yml healthcheck config works correctly in Alpine containers
on:
pull_request:
branches:
+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
# 从前端镜像提取静态文件
# Extract static files from frontend image
FROM ghcr.io/nofxaios/nofx/nofx-frontend:latest AS frontend
# 最终镜像
# Final image
FROM alpine:latest
RUN apk add --no-cache ca-certificates tzdata sqlite nginx openssl gettext
# 复制后端二进制
# Copy backend binary
COPY --from=backend /app/nofx /app/nofx
# 复制 TA-Lib
# Copy TA-Lib libraries
COPY --from=backend /usr/local/lib/libta_lib* /usr/local/lib/
RUN ldconfig /usr/local/lib 2>/dev/null || true
# 复制前端静态文件
# Copy frontend static files
COPY --from=frontend /usr/share/nginx/html /usr/share/nginx/html
WORKDIR /app
RUN mkdir -p /app/data
# 启动脚本(包含 nginx 配置生成)
# Startup script (includes nginx config generation)
COPY railway/start.sh /app/start.sh
RUN chmod +x /app/start.sh
ENV DB_PATH=/app/data/data.db
# Railway 会自动设置 PORT 环境变量
# Railway automatically sets the PORT environment variable
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
+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
// Coinank: Volume = BTC 数量, Quantity = USDT 成交额
// Coinank: Volume = BTC quantity, Quantity = USDT turnover
klines := make([]market.Kline, len(coinankKlines))
for i, ck := range coinankKlines {
klines[i] = market.Kline{
@@ -196,8 +196,8 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i
High: ck.High,
Low: ck.Low,
Close: ck.Close,
Volume: ck.Volume, // BTC 数量
QuoteVolume: ck.Quantity, // USDT 成交额
Volume: ck.Volume, // BTC quantity
QuoteVolume: ck.Quantity, // USDT turnover
CloseTime: ck.EndTime,
}
}
@@ -229,8 +229,8 @@ func (s *Server) getKlinesFromAlpaca(symbol, interval string, limit int) ([]mark
High: bar.High,
Low: bar.Low,
Close: bar.Close,
Volume: float64(bar.Volume), // 股数
QuoteVolume: float64(bar.Volume) * bar.Close, // 成交额 = 股数 * 收盘价 (USD)
Volume: float64(bar.Volume), // share count
QuoteVolume: float64(bar.Volume) * bar.Close, // turnover = shares * close price (USD)
CloseTime: bar.Timestamp.UnixMilli(),
}
}
@@ -311,8 +311,8 @@ func (s *Server) getKlinesFromHyperliquid(symbol, interval string, limit int) ([
High: high,
Low: low,
Close: close,
Volume: volume, // 合约数量
QuoteVolume: volume * close, // 成交额 (USD)
Volume: volume, // contract quantity
QuoteVolume: volume * close, // turnover (USD)
CloseTime: candle.CloseTime,
}
}
+1 -1
View File
@@ -195,7 +195,7 @@ func (cfg *BacktestConfig) ToStrategyConfig() *store.StrategyConfig {
if cfg.loadedStrategy != nil {
result := *cfg.loadedStrategy // Make a copy
// Override coin source with backtest symbols (回测指定的币对优先)
// Override coin source with backtest symbols (backtest-specified pairs take priority)
if len(cfg.Symbols) > 0 {
result.CoinSource.SourceType = "static"
result.CoinSource.StaticCoins = cfg.Symbols
+1 -1
View File
@@ -6,7 +6,7 @@ services:
dockerfile: ./docker/Dockerfile.backend
container_name: nofx-trading
restart: unless-stopped
stop_grace_period: 30s # 允许应用有 30 秒时间优雅关闭
stop_grace_period: 30s # Allow the app 30 seconds for graceful shutdown
ports:
- "${NOFX_BACKEND_PORT:-8080}:8080"
- "6060:6060" # pprof profiling
+6 -6
View File
@@ -247,7 +247,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
return e.filterExcludedCoins(candidates), nil
case "ai500":
// 检查 use_ai500 标志,如果为 false 则回退到静态币种
// Check use_ai500 flag; if false, fall back to static coins
if !coinSource.UseAI500 {
logger.Infof("⚠️ source_type is 'ai500' but use_ai500 is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins {
@@ -263,11 +263,11 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
if err != nil {
return nil, err
}
// 空列表是正常情况,直接返回
// Empty list is a normal condition, return directly
return e.filterExcludedCoins(coins), nil
case "oi_top":
// 检查 use_oi_top 标志,如果为 false 则回退到静态币种
// Check use_oi_top flag; if false, fall back to static coins
if !coinSource.UseOITop {
logger.Infof("⚠️ source_type is 'oi_top' but use_oi_top is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins {
@@ -283,11 +283,11 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
if err != nil {
return nil, err
}
// 空列表是正常情况,直接返回
// Empty list is a normal condition, return directly
return e.filterExcludedCoins(coins), nil
case "oi_low":
// 持仓减少榜,适合做空
// OI decrease ranking, suitable for short positions
if !coinSource.UseOILow {
logger.Infof("⚠️ source_type is 'oi_low' but use_oi_low is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins {
@@ -303,7 +303,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
if err != nil {
return nil, err
}
// 空列表是正常情况,直接返回
// Empty list is a normal condition, return directly
return e.filterExcludedCoins(coins), nil
case "hyper_all":
+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 {
if len(sources) > 1 {
// 多信号源组合
// Multiple signal source combination
hasAI500 := false
hasOITop := false
hasOILow := false
@@ -482,9 +482,9 @@ func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
case "ai500":
return " (AI500)"
case "oi_top":
return " (OI_Top 持仓增加)"
return " (OI_Top OI increase)"
case "oi_low":
return " (OI_Low 持仓减少)"
return " (OI_Low OI decrease)"
case "static":
return " (Manual selection)"
case "hyper_all":
@@ -504,7 +504,7 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string {
var sb strings.Builder
indicators := e.config.Indicators
// 明确标注币种
// Clearly label the coin symbol
sb.WriteString(fmt.Sprintf("=== %s Market Data ===\n\n", data.Symbol))
sb.WriteString(fmt.Sprintf("current_price = %.4f", data.CurrentPrice))
+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 {
var sb strings.Builder
// 1. 添加Schema说明(让AI理解数据格式)
// 1. Add schema description (so AI understands data format)
sb.WriteString(GetSchemaPrompt(lang))
sb.WriteString("\n---\n\n")
// 2. 当前状态概览
// 2. Current state overview
sb.WriteString(formatContextData(ctx, lang))
return sb.String()
}
// FormatContextDataOnly 仅格式化上下文数据,不包含Schema(用于已有Schema的场景)
// FormatContextDataOnly formats context data only, without schema (for use when schema is already present)
func FormatContextDataOnly(ctx *Context, lang Language) string {
return formatContextData(ctx, lang)
}
// formatContextData 格式化核心数据部分
// formatContextData formats the core data section
func formatContextData(ctx *Context, lang Language) string {
var sb strings.Builder
// 1. 当前状态概览
// 1. Current state overview
if lang == LangChinese {
sb.WriteString(formatHeaderZH(ctx))
} else {
sb.WriteString(formatHeaderEN(ctx))
}
// 3. 账户信息
// 3. Account information
if lang == LangChinese {
sb.WriteString(formatAccountZH(ctx))
} else {
sb.WriteString(formatAccountEN(ctx))
}
// 4. 历史交易统计
// 4. Historical trading statistics
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
if lang == LangChinese {
sb.WriteString(formatTradingStatsZH(ctx.TradingStats))
@@ -61,7 +62,7 @@ func formatContextData(ctx *Context, lang Language) string {
}
}
// 5. 最近交易记录
// 5. Recent trade records
if len(ctx.RecentOrders) > 0 {
if lang == LangChinese {
sb.WriteString(formatRecentTradesZH(ctx.RecentOrders))
@@ -70,7 +71,7 @@ func formatContextData(ctx *Context, lang Language) string {
}
}
// 5. 当前持仓
// 5. Current positions
if len(ctx.Positions) > 0 {
if lang == LangChinese {
sb.WriteString(formatCurrentPositionsZH(ctx))
@@ -79,7 +80,7 @@ func formatContextData(ctx *Context, lang Language) string {
}
}
// 6. 候选币种(带市场数据)
// 6. Candidate coins (with market data)
if len(ctx.CandidateCoins) > 0 {
if lang == LangChinese {
sb.WriteString(formatCandidateCoinsZH(ctx))
@@ -88,7 +89,7 @@ func formatContextData(ctx *Context, lang Language) string {
}
}
// 7. OI排名数据(如果有)
// 7. OI ranking data (if available)
if ctx.OIRankingData != nil {
nofxosLang := nofxos.LangEnglish
if lang == LangChinese {
@@ -100,15 +101,15 @@ func formatContextData(ctx *Context, lang Language) string {
return sb.String()
}
// ========== 中文格式化函数 ==========
// ========== Chinese Formatting Functions ==========
// formatHeaderZH 格式化头部信息(中文)
// formatHeaderZH formats header information (Chinese)
func formatHeaderZH(ctx *Context) string {
return fmt.Sprintf("# 📊 交易决策请求\n\n时间: %s | 周期: #%d | 运行时长: %d 分钟\n\n",
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
}
// formatAccountZH 格式化账户信息(中文)
// formatAccountZH formats account information (Chinese)
func formatAccountZH(ctx *Context) string {
acc := ctx.Account
var sb strings.Builder
@@ -120,7 +121,7 @@ func formatAccountZH(ctx *Context) string {
sb.WriteString(fmt.Sprintf("保证金使用率: %.1f%% | ", acc.MarginUsedPct))
sb.WriteString(fmt.Sprintf("持仓数: %d\n\n", acc.PositionCount))
// 添加风险提示
// Add risk warnings
if acc.MarginUsedPct > 70 {
sb.WriteString("⚠️ **风险警告**: 保证金使用率 > 70%,处于高风险状态!\n\n")
} else if acc.MarginUsedPct > 50 {
@@ -130,25 +131,25 @@ func formatAccountZH(ctx *Context) string {
return sb.String()
}
// formatTradingStatsZH 格式化历史交易统计(中文)
// formatTradingStatsZH formats historical trading statistics (Chinese)
func formatTradingStatsZH(stats *TradingStats) string {
var sb strings.Builder
sb.WriteString("## 历史交易统计\n\n")
// 盈亏比计算
// Win/loss ratio calculation
var winLossRatio float64
if stats.AvgLoss > 0 {
winLossRatio = stats.AvgWin / stats.AvgLoss
}
// 指标定义说明(去掉胜率,聚焦核心指标)
// Metric definitions (focusing on core metrics, excluding win rate)
sb.WriteString("**指标说明**:\n")
sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利,>1.5为良好,>2为优秀)\n")
sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好,>2优秀)\n")
sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀)\n")
sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n")
// 数据值
// Data values
sb.WriteString("**当前数据**:\n")
sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades))
sb.WriteString(fmt.Sprintf("- 盈利因子: %.2f\n", stats.ProfitFactor))
@@ -159,10 +160,10 @@ func formatTradingStatsZH(stats *TradingStats) string {
sb.WriteString(fmt.Sprintf("- 平均亏损: -%.2f USDT\n", stats.AvgLoss))
sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct))
// 综合分析和决策建议
// Comprehensive analysis and decision guidance
sb.WriteString("**决策参考**:\n")
// 根据统计数据给出具体建议
// Provide specific recommendations based on statistics
if stats.TotalTrades < 10 {
sb.WriteString("- 样本量较小(<10笔),统计结果参考意义有限\n")
}
@@ -191,13 +192,13 @@ func formatTradingStatsZH(stats *TradingStats) string {
return sb.String()
}
// formatRecentTradesZH 格式化最近交易(中文)
// formatRecentTradesZH formats recent trades (Chinese)
func formatRecentTradesZH(orders []RecentOrder) string {
var sb strings.Builder
sb.WriteString("## 最近完成的交易\n\n")
for i, order := range orders {
// 判断盈亏
// Determine profit or loss
profitOrLoss := "盈利"
if order.RealizedPnL < 0 {
profitOrLoss = "亏损"
@@ -222,13 +223,13 @@ func formatRecentTradesZH(orders []RecentOrder) string {
return sb.String()
}
// formatCurrentPositionsZH 格式化当前持仓(中文)
// formatCurrentPositionsZH formats current positions (Chinese)
func formatCurrentPositionsZH(ctx *Context) string {
var sb strings.Builder
sb.WriteString("## 当前持仓\n\n")
for i, pos := range ctx.Positions {
// 计算回撤
// Calculate drawdown
drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct
sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
@@ -242,7 +243,7 @@ func formatCurrentPositionsZH(ctx *Context) string {
sb.WriteString(fmt.Sprintf("保证金 %.0f USDT | ", pos.MarginUsed))
sb.WriteString(fmt.Sprintf("强平价 %.4f\n", pos.LiquidationPrice))
// 添加分析提示
// Add analysis hints
if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 {
sb.WriteString(fmt.Sprintf(" ⚠️ **止盈提示**: 当前盈亏从峰值 %.2f%% 回撤到 %.2f%%,回撤幅度 %.2f%%,建议考虑止盈\n",
pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100))
@@ -252,7 +253,7 @@ func formatCurrentPositionsZH(ctx *Context) string {
sb.WriteString(" ⚠️ **止损提示**: 亏损接近-5%止损线,建议考虑止损\n")
}
// 显示当前价格(如果有市场数据)
// Show current price (if market data available)
if ctx.MarketDataMap != nil {
if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok {
sb.WriteString(fmt.Sprintf(" 📈 当前价格: %.4f\n", mdata.CurrentPrice))
@@ -265,7 +266,7 @@ func formatCurrentPositionsZH(ctx *Context) string {
return sb.String()
}
// formatCandidateCoinsZH 格式化候选币种(中文)
// formatCandidateCoinsZH formats candidate coins (Chinese)
func formatCandidateCoinsZH(ctx *Context) string {
var sb strings.Builder
sb.WriteString("## 候选币种\n\n")
@@ -273,19 +274,19 @@ func formatCandidateCoinsZH(ctx *Context) string {
for i, coin := range ctx.CandidateCoins {
sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol))
// 当前价格
// Current price
if ctx.MarketDataMap != nil {
if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok {
sb.WriteString(fmt.Sprintf("当前价格: %.4f\n\n", mdata.CurrentPrice))
// K线数据(多时间框架)
// Kline data (multi-timeframe)
if mdata.TimeframeData != nil {
sb.WriteString(formatKlineDataZH(coin.Symbol, mdata.TimeframeData, ctx.Timeframes))
}
}
}
// OI数据(如果有)
// OI data (if available)
if ctx.OITopDataMap != nil {
if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok {
sb.WriteString(fmt.Sprintf("**持仓量变化**: OI排名 #%d | 变化 %+.2f%% (%+.2fM USDT) | 价格变化 %+.2f%%\n\n",
@@ -295,7 +296,7 @@ func formatCandidateCoinsZH(ctx *Context) string {
oiData.PriceDeltaPercent,
))
// OI解读
// OI interpretation
oiChange := "增加"
if oiData.OIDeltaPercent < 0 {
oiChange = "减少"
@@ -314,7 +315,7 @@ func formatCandidateCoinsZH(ctx *Context) string {
return sb.String()
}
// formatKlineDataZH 格式化K线数据(中文)
// formatKlineDataZH formats kline data (Chinese)
func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
var sb strings.Builder
@@ -324,7 +325,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD
sb.WriteString("```\n")
sb.WriteString("时间(UTC) 开盘 最高 最低 收盘 成交量\n")
// 只显示最近30根K线
// Only show the latest 30 klines
startIdx := 0
if len(data.Klines) > 30 {
startIdx = len(data.Klines) - 30
@@ -343,7 +344,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD
))
}
// 标记最后一根K线
// Mark the last kline
if len(data.Klines) > 0 {
sb.WriteString(" <- 当前\n")
}
@@ -356,7 +357,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD
}
// getOIInterpretationZH 获取OI变化解读(中文)
// getOIInterpretationZH returns OI change interpretation (Chinese)
func getOIInterpretationZH(oiChange, priceChange string) string {
if oiChange == "增加" && priceChange == "上涨" {
return OIInterpretation.OIUp_PriceUp.ZH
@@ -369,15 +370,15 @@ func getOIInterpretationZH(oiChange, priceChange string) string {
}
}
// ========== 英文格式化函数 ==========
// ========== English Formatting Functions ==========
// formatHeaderEN 格式化头部信息(英文)
// formatHeaderEN formats header information (English)
func formatHeaderEN(ctx *Context) string {
return fmt.Sprintf("# 📊 Trading Decision Request\n\nTime: %s | Period: #%d | Runtime: %d minutes\n\n",
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
}
// formatAccountEN 格式化账户信息(英文)
// formatAccountEN formats account information (English)
func formatAccountEN(ctx *Context) string {
acc := ctx.Account
var sb strings.Builder
@@ -399,7 +400,7 @@ func formatAccountEN(ctx *Context) string {
return sb.String()
}
// formatTradingStatsEN 格式化历史交易统计(英文)
// formatTradingStatsEN formats historical trading statistics (English)
func formatTradingStatsEN(stats *TradingStats) string {
var sb strings.Builder
sb.WriteString("## Historical Trading Statistics\n\n")
@@ -460,7 +461,7 @@ func formatTradingStatsEN(stats *TradingStats) string {
return sb.String()
}
// formatRecentTradesEN 格式化最近交易(英文)
// formatRecentTradesEN formats recent trades (English)
func formatRecentTradesEN(orders []RecentOrder) string {
var sb strings.Builder
sb.WriteString("## Recent Completed Trades\n\n")
@@ -490,7 +491,7 @@ func formatRecentTradesEN(orders []RecentOrder) string {
return sb.String()
}
// formatCurrentPositionsEN 格式化当前持仓(英文)
// formatCurrentPositionsEN formats current positions (English)
func formatCurrentPositionsEN(ctx *Context) string {
var sb strings.Builder
sb.WriteString("## Current Positions\n\n")
@@ -531,7 +532,7 @@ func formatCurrentPositionsEN(ctx *Context) string {
return sb.String()
}
// formatCandidateCoinsEN 格式化候选币种(英文)
// formatCandidateCoinsEN formats candidate coins (English)
func formatCandidateCoinsEN(ctx *Context) string {
var sb strings.Builder
sb.WriteString("## Candidate Coins\n\n")
@@ -576,7 +577,7 @@ func formatCandidateCoinsEN(ctx *Context) string {
return sb.String()
}
// formatKlineDataEN 格式化K线数据(英文)
// formatKlineDataEN formats kline data (English)
func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
var sb strings.Builder
@@ -621,7 +622,7 @@ func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesD
}
// getOIInterpretationEN 获取OI变化解读(英文)
// getOIInterpretationEN returns OI change interpretation (English)
func getOIInterpretationEN(oiChange, priceChange string) string {
if oiChange == "increase" && priceChange == "up" {
return OIInterpretation.OIUp_PriceUp.EN
+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 {
lang Language
}
// NewPromptBuilder 创建提示词构建器
// NewPromptBuilder creates a new prompt builder for the given language
func NewPromptBuilder(lang Language) *PromptBuilder {
return &PromptBuilder{lang: lang}
}
// BuildSystemPrompt 构建系统提示词
// BuildSystemPrompt builds the system prompt
func (pb *PromptBuilder) BuildSystemPrompt() string {
if pb.lang == LangChinese {
return pb.buildSystemPromptZH()
@@ -29,19 +29,19 @@ func (pb *PromptBuilder) BuildSystemPrompt() string {
return pb.buildSystemPromptEN()
}
// BuildUserPrompt 构建用户提示词(包含完整的交易上下文)
// BuildUserPrompt builds the user prompt with full trading context
func (pb *PromptBuilder) BuildUserPrompt(ctx *Context) string {
// 使用Formatter格式化交易上下文
// Use Formatter to format the trading context
formattedData := FormatContextForAI(ctx, pb.lang)
// 添加决策要求
// Append decision requirements
if pb.lang == LangChinese {
return formattedData + pb.getDecisionRequirementsZH()
}
return formattedData + pb.getDecisionRequirementsEN()
}
// ========== 中文提示词 ==========
// ========== Chinese Prompts ==========
func (pb *PromptBuilder) buildSystemPromptZH() string {
return `你是一个专业的量化交易AI助手,负责分析市场数据并做出交易决策。
@@ -176,7 +176,7 @@ func (pb *PromptBuilder) getDecisionRequirementsZH() string {
**请立即输出你的决策(JSON格式)**:`
}
// ========== 英文提示词 ==========
// ========== English Prompts ==========
func (pb *PromptBuilder) buildSystemPromptEN() string {
return `You are a professional quantitative trading AI assistant responsible for analyzing market data and making trading decisions.
@@ -311,9 +311,9 @@ func (pb *PromptBuilder) getDecisionRequirementsEN() string {
**Please output your decision (JSON format) immediately**:`
}
// ========== 辅助函数 ==========
// ========== Helper Functions ==========
// FormatDecisionExample 格式化决策示例(用于文档)
// FormatDecisionExample formats a decision example (for documentation)
func FormatDecisionExample(lang Language) string {
example := Decision{
Symbol: "BTCUSDT",
@@ -323,32 +323,32 @@ func FormatDecisionExample(lang Language) string {
StopLoss: 42000,
TakeProfit: 48000,
Confidence: 85,
Reasoning: "详细的推理过程...",
Reasoning: "Detailed reasoning process...",
}
data, _ := json.MarshalIndent([]Decision{example}, "", " ")
return string(data)
}
// ValidateDecisionFormat 验证决策格式是否正确
// ValidateDecisionFormat validates that the decision format is correct
func ValidateDecisionFormat(decisions []Decision) error {
if len(decisions) == 0 {
return fmt.Errorf("决策列表不能为空")
return fmt.Errorf("decision list cannot be empty")
}
for i, d := range decisions {
// 必需字段检查
// Required field checks
if d.Symbol == "" {
return fmt.Errorf("决策#%d: symbol不能为空", i+1)
return fmt.Errorf("decision #%d: symbol cannot be empty", i+1)
}
if d.Action == "" {
return fmt.Errorf("决策#%d: action不能为空", i+1)
return fmt.Errorf("decision #%d: action cannot be empty", i+1)
}
if d.Reasoning == "" {
return fmt.Errorf("决策#%d: reasoning不能为空", i+1)
return fmt.Errorf("decision #%d: reasoning cannot be empty", i+1)
}
// 动作类型检查
// Action type validation
validActions := map[string]bool{
"HOLD": true,
"PARTIAL_CLOSE": true,
@@ -358,16 +358,16 @@ func ValidateDecisionFormat(decisions []Decision) error {
"WAIT": true,
}
if !validActions[d.Action] {
return fmt.Errorf("决策#%d: 无效的action类型: %s", i+1, d.Action)
return fmt.Errorf("decision #%d: invalid action type: %s", i+1, d.Action)
}
// 开新仓位的必需参数检查
// Required parameters for opening new positions
if d.Action == "OPEN_NEW" {
if d.Leverage == 0 {
return fmt.Errorf("决策#%d: OPEN_NEW动作需要提供leverage", i+1)
return fmt.Errorf("decision #%d: OPEN_NEW action requires leverage", i+1)
}
if d.PositionSizeUSD == 0 {
return fmt.Errorf("决策#%d: OPEN_NEW动作需要提供position_size_usd", i+1)
return fmt.Errorf("decision #%d: OPEN_NEW action requires position_size_usd", i+1)
}
}
}
+4 -4
View File
@@ -170,8 +170,8 @@ func TestValidateDecisionFormat(t *testing.T) {
t.Error("Empty decisions should return error")
}
if !strings.Contains(err.Error(), "不能为空") {
t.Errorf("Error message should mention '不能为空', got: %v", err)
if !strings.Contains(err.Error(), "cannot be empty") {
t.Errorf("Error message should mention 'cannot be empty', got: %v", err)
}
})
@@ -238,8 +238,8 @@ func TestValidateDecisionFormat(t *testing.T) {
t.Error("Invalid action should return error")
}
if !strings.Contains(err.Error(), "无效的action") {
t.Errorf("Error should mention '无效的action', got: %v", err)
if !strings.Contains(err.Error(), "invalid action") {
t.Errorf("Error should mention 'invalid action', got: %v", err)
}
})
+39 -39
View File
@@ -1,17 +1,17 @@
package kernel
// ============================================================================
// Trading Data Schema - 交易数据字典
// Trading Data Schema
// ============================================================================
// 双语数据字典,支持中文和英文
// 确保AI能够100%理解数据格式,无论使用哪种语言
// Bilingual data dictionary supporting Chinese and English.
// Ensures AI can fully understand data formats regardless of language.
// ============================================================================
const (
SchemaVersion = "1.0.0"
)
// Language 语言类型
// Language represents the language type
type Language string
const (
@@ -19,20 +19,20 @@ const (
LangEnglish Language = "en-US"
)
// ========== 双语字段定义 ==========
// ========== Bilingual Field Definitions ==========
// BilingualFieldDef 双语字段定义
// BilingualFieldDef defines a field with bilingual name, formula, and description
type BilingualFieldDef struct {
NameZH string // 中文名称
NameZH string // Chinese name
NameEN string // English name
Unit string // 单位
FormulaZH string // 中文公式
Unit string // unit of measurement
FormulaZH string // Chinese formula
FormulaEN string // English formula
DescZH string // 中文描述
DescZH string // Chinese description
DescEN string // English description
}
// GetName 获取字段名称(根据语言)
// GetName returns the field name based on language
func (d BilingualFieldDef) GetName(lang Language) string {
if lang == LangChinese {
return d.NameZH
@@ -40,7 +40,7 @@ func (d BilingualFieldDef) GetName(lang Language) string {
return d.NameEN
}
// GetFormula 获取公式(根据语言)
// GetFormula returns the formula based on language
func (d BilingualFieldDef) GetFormula(lang Language) string {
if lang == LangChinese {
return d.FormulaZH
@@ -48,7 +48,7 @@ func (d BilingualFieldDef) GetFormula(lang Language) string {
return d.FormulaEN
}
// GetDesc 获取描述(根据语言)
// GetDesc returns the description based on language
func (d BilingualFieldDef) GetDesc(lang Language) string {
if lang == LangChinese {
return d.DescZH
@@ -56,9 +56,9 @@ func (d BilingualFieldDef) GetDesc(lang Language) string {
return d.DescEN
}
// ========== 数据字典 ==========
// ========== Data Dictionary ==========
// DataDictionary 数据字典:定义所有字段的含义
// DataDictionary defines the meaning of all fields
var DataDictionary = map[string]map[string]BilingualFieldDef{
"AccountMetrics": {
"Equity": {
@@ -217,18 +217,18 @@ var DataDictionary = map[string]map[string]BilingualFieldDef{
},
}
// ========== 双语规则定义 ==========
// ========== Bilingual Rule Definitions ==========
// BilingualRuleDef 双语规则定义
// BilingualRuleDef defines a trading rule with bilingual description and reason
type BilingualRuleDef struct {
Value interface{} // 规则值
DescZH string // 中文描述
Value interface{} // rule value
DescZH string // Chinese description
DescEN string // English description
ReasonZH string // 中文原因
ReasonZH string // Chinese reason
ReasonEN string // English reason
}
// GetDesc 获取描述(根据语言)
// GetDesc returns the description based on language
func (d BilingualRuleDef) GetDesc(lang Language) string {
if lang == LangChinese {
return d.DescZH
@@ -236,7 +236,7 @@ func (d BilingualRuleDef) GetDesc(lang Language) string {
return d.DescEN
}
// GetReason 获取原因(根据语言)
// GetReason returns the reason based on language
func (d BilingualRuleDef) GetReason(lang Language) string {
if lang == LangChinese {
return d.ReasonZH
@@ -244,9 +244,9 @@ func (d BilingualRuleDef) GetReason(lang Language) string {
return d.ReasonEN
}
// ========== 交易规则 ==========
// ========== Trading Rules ==========
// TradingRules 交易规则定义
// TradingRules defines the trading rules
var TradingRules = struct {
RiskManagement map[string]BilingualRuleDef
EntrySignals map[string]BilingualRuleDef
@@ -340,9 +340,9 @@ var TradingRules = struct {
},
}
// ========== OI解读 ==========
// ========== OI Interpretation ==========
// OIInterpretation OI变化的市场解读(双语)
// OIInterpretation defines bilingual market interpretations for OI changes
type OIInterpretationType struct {
OIUp_PriceUp struct {
ZH string
@@ -393,9 +393,9 @@ var OIInterpretation = OIInterpretationType{
},
}
// ========== 常见错误 ==========
// ========== Common Mistakes ==========
// CommonMistake 常见错误定义
// CommonMistake defines a common mistake with bilingual fields
type CommonMistake struct {
ErrorZH string
ErrorEN string
@@ -440,9 +440,9 @@ var CommonMistakes = []CommonMistake{
},
}
// ========== Prompt生成函数 ==========
// ========== Prompt Generation Functions ==========
// GetSchemaPrompt 生成Schema说明文本,用于AI Prompt
// GetSchemaPrompt generates schema description text for AI prompts
func GetSchemaPrompt(lang Language) string {
if lang == LangChinese {
return getSchemaPromptZH()
@@ -450,36 +450,36 @@ func GetSchemaPrompt(lang Language) string {
return getSchemaPromptEN()
}
// getSchemaPromptZH 生成中文Prompt
// getSchemaPromptZH generates the Chinese prompt
func getSchemaPromptZH() string {
prompt := "# 📖 数据字典与交易规则\n\n"
prompt += "## 📊 字段含义说明\n\n"
// 账户指标
// Account metrics
prompt += "### 账户指标\n"
for key, field := range DataDictionary["AccountMetrics"] {
prompt += formatFieldDefZH(key, field)
}
// 交易指标
// Trade metrics
prompt += "\n### 交易指标\n"
for key, field := range DataDictionary["TradeMetrics"] {
prompt += formatFieldDefZH(key, field)
}
// 持仓指标
// Position metrics
prompt += "\n### 持仓指标\n"
for key, field := range DataDictionary["PositionMetrics"] {
prompt += formatFieldDefZH(key, field)
}
// 市场数据
// Market data
prompt += "\n### 市场数据\n"
for key, field := range DataDictionary["MarketData"] {
prompt += formatFieldDefZH(key, field)
}
// OI解读
// OI interpretation
prompt += "\n## 💹 持仓量(OI)变化解读\n\n"
prompt += "- **OI增加 + 价格上涨**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n"
prompt += "- **OI增加 + 价格下跌**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n"
@@ -489,7 +489,7 @@ func getSchemaPromptZH() string {
return prompt
}
// getSchemaPromptEN 生成英文Prompt
// getSchemaPromptEN generates the English prompt
func getSchemaPromptEN() string {
prompt := "# 📖 Data Dictionary & Trading Rules\n\n"
prompt += "## 📊 Field Definitions\n\n"
@@ -528,7 +528,7 @@ func getSchemaPromptEN() string {
return prompt
}
// formatFieldDefZH 格式化中文字段定义
// formatFieldDefZH formats a field definition in Chinese
func formatFieldDefZH(key string, field BilingualFieldDef) string {
result := "- **" + key + "**" + field.NameZH + ": " + field.DescZH
if field.FormulaZH != "" {
@@ -541,7 +541,7 @@ func formatFieldDefZH(key string, field BilingualFieldDef) string {
return result
}
// formatFieldDefEN 格式化英文字段定义
// formatFieldDefEN formats a field definition in English
func formatFieldDefEN(key string, field BilingualFieldDef) string {
result := "- **" + key + "** (" + field.NameEN + "): " + field.DescEN
if field.FormulaEN != "" {
+5 -5
View File
@@ -210,11 +210,11 @@ type BoxData struct {
type RegimeLevel string
const (
RegimeLevelNarrow RegimeLevel = "narrow" // 窄幅震荡
RegimeLevelStandard RegimeLevel = "standard" // 标准震荡
RegimeLevelWide RegimeLevel = "wide" // 宽幅震荡
RegimeLevelVolatile RegimeLevel = "volatile" // 剧烈震荡
RegimeLevelTrending RegimeLevel = "trending" // 趋势
RegimeLevelNarrow RegimeLevel = "narrow" // narrow range oscillation
RegimeLevelStandard RegimeLevel = "standard" // standard oscillation
RegimeLevelWide RegimeLevel = "wide" // wide range oscillation
RegimeLevelVolatile RegimeLevel = "volatile" // extreme volatility
RegimeLevelTrending RegimeLevel = "trending" // trending
)
// BreakoutLevel represents which box level has been broken
+1 -1
View File
@@ -73,7 +73,7 @@ func (c *Client) fetchAI500() ([]CoinData, error) {
return nil, fmt.Errorf("API returned failure status")
}
// 空列表是正常情况,不是错误
// Empty list is a normal condition, not an error
if len(response.Data.Coins) == 0 {
log.Printf("️ AI500 returned empty coin list (no coins meet criteria currently)")
return []CoinData{}, nil
+1 -1
View File
@@ -23,7 +23,7 @@ type NetFlowResponse struct {
Netflows []NetFlowPosition `json:"netflows"`
Count int `json:"count"`
Type string `json:"type"` // institution or personal
Trade string `json:"trade"` // 合约 or 现货
Trade string `json:"trade"` // futures or spot
TimeRange string `json:"time_range"`
RankType string `json:"rank_type"` // top or low
Limit int `json:"limit"`
+6 -6
View File
@@ -1,11 +1,11 @@
#!/bin/sh
set -e
# Railway 会设置 PORT 环境变量
# Railway sets the PORT environment variable
export PORT=${PORT:-8080}
echo "🚀 Starting NOFX on port $PORT..."
# 生成加密密钥(如果没有设置)
# Generate encryption keys (if not already set)
if [ -z "$RSA_PRIVATE_KEY" ]; then
export RSA_PRIVATE_KEY=$(openssl genrsa 2048 2>/dev/null)
fi
@@ -13,7 +13,7 @@ if [ -z "$DATA_ENCRYPTION_KEY" ]; then
export DATA_ENCRYPTION_KEY=$(openssl rand -base64 32)
fi
# 生成 nginx 配置
# Generate nginx config
cat > /etc/nginx/http.d/default.conf << NGINX_EOF
server {
listen $PORT;
@@ -44,14 +44,14 @@ server {
}
NGINX_EOF
# 启动后端(端口 8081
# Start backend (port 8081)
API_SERVER_PORT=8081 /app/nofx &
sleep 2
# 启动 nginx(后台)
# Start nginx (background)
nginx
echo "✅ NOFX started successfully"
# 保持容器运行
# Keep the container running
tail -f /dev/null
+2 -2
View File
@@ -111,11 +111,11 @@ type CoinSourceConfig struct {
UseAI500 bool `json:"use_ai500"`
// AI500 coin pool maximum count
AI500Limit int `json:"ai500_limit,omitempty"`
// whether to use OI Top (持仓增加榜,适合做多)
// whether to use OI Top (OI increase ranking, suitable for long positions)
UseOITop bool `json:"use_oi_top"`
// OI Top maximum count
OITopLimit int `json:"oi_top_limit,omitempty"`
// whether to use OI Low (持仓减少榜,适合做空)
// whether to use OI Low (OI decrease ranking, suitable for short positions)
UseOILow bool `json:"use_oi_low"`
// OI Low maximum count
OILowLimit int `json:"oi_low_limit,omitempty"`
+1 -1
View File
@@ -273,7 +273,7 @@ func (a *Agent) Run(userMessage string, onChunk func(string)) string {
// Safety: max iterations reached.
logger.Warnf("Agent: max iterations (%d) reached for message: %q", maxIterations, userMessage)
reply := "操作已完成,请检查您的账户查看最新状态。"
reply := "Operation completed. Please check your account for the latest status. / 操作已完成,请检查您的账户查看最新状态。"
a.memory.Add("user", userMessage)
a.memory.Add("assistant", reply)
return reply
+1 -1
View File
@@ -45,7 +45,7 @@ func (m *Manager) Run(chatID int64, userMessage string, onChunk func(string)) st
case lane <- struct{}{}:
case <-time.After(60 * time.Second):
logger.Warnf("Agent: lane wait timeout for chat %d — previous message still processing", chatID)
return "上一条消息仍在处理中,请稍等片刻后再试。"
return "Previous message is still being processed. Please wait a moment and try again. / 上一条消息仍在处理中,请稍等片刻后再试。"
}
defer func() { <-lane }()
return a.Run(userMessage, onChunk)
+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
at.saveEquitySnapshot(ctx)
// 如果没有候选币种,记录但不报错
// If no candidate coins available, log but do not error
if len(ctx.CandidateCoins) == 0 {
logger.Infof("️ No candidate coins available, skipping this cycle")
record.Success = true // 不是错误,只是没有候选币
record.Success = true // Not an error, just no candidate coins
record.ExecutionLog = append(record.ExecutionLog, "No candidate coins available, cycle skipped")
record.AccountState = store.AccountSnapshot{
TotalBalance: ctx.Account.TotalEquity,
+1 -1
View File
@@ -115,7 +115,7 @@ func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[str
resp, err := t.client.Do(req)
if err != nil {
// ✅ 正确做法:查询失败返回错误,而不是假设成交
// Correct approach: return error on query failure, do not assume order is filled
return nil, fmt.Errorf("failed to query order status: %w", err)
}
defer resp.Body.Close()
@@ -2,6 +2,7 @@ import { motion, AnimatePresence } from 'framer-motion'
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
import { DeepVoidBackground } from '../common/DeepVoidBackground'
import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
interface LoginRequiredOverlayProps {
isOpen: boolean
@@ -12,52 +13,19 @@ interface LoginRequiredOverlayProps {
export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequiredOverlayProps) {
const { language } = useLanguage()
const texts = {
zh: {
title: '系统访问受限',
subtitle: featureName ? `访问「${featureName}」需要更高权限` : '此模块需要授权访问',
description: '初始化身份验证协议以解锁完整系统功能:AI 交易员配置、策略市场数据流、回测模拟核心。',
benefits: [
'AI 交易员控制权',
'高频策略核心市场',
'历史数据回测引擎',
'全系统数据可视化'
],
login: '执行登录指令',
register: '注册新用户 ID',
later: '中止操作'
},
en: {
title: 'SYSTEM ACCESS DENIED',
subtitle: featureName ? `Module "${featureName}" requires elevated privileges` : 'Authorization required for this module',
description: 'Initialize authentication protocol to unlock full system capabilities: AI Trader configuration, Strategy Market data streams, and Backtest Simulation core.',
benefits: [
'AI Trader Control',
'HFT Strategy Market',
'Historical Backtest Engine',
'Full System Visualization'
],
login: 'EXECUTE LOGIN',
register: 'REGISTER NEW ID',
later: 'ABORT'
},
id: {
title: 'AKSES SISTEM DITOLAK',
subtitle: featureName ? `Modul "${featureName}" memerlukan hak akses lebih tinggi` : 'Otorisasi diperlukan untuk modul ini',
description: 'Inisialisasi protokol autentikasi untuk membuka kemampuan sistem penuh: konfigurasi Trader AI, aliran data Pasar Strategi, dan inti Simulasi Backtest.',
benefits: [
'Kontrol Trader AI',
'Pasar Strategi HFT',
'Mesin Backtest Historis',
'Visualisasi Sistem Penuh'
],
login: 'JALANKAN LOGIN',
register: 'DAFTAR ID BARU',
later: 'BATALKAN'
}
}
const tr = (key: string, params?: Record<string, string | number>) =>
t(`loginRequired.${key}`, language, params)
const t = texts[language]
const subtitle = featureName
? tr('subtitleWithFeature', { featureName })
: tr('subtitleDefault')
const benefits = [
tr('benefit1'),
tr('benefit2'),
tr('benefit3'),
tr('benefit4'),
]
return (
<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="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" />
<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>
@@ -116,19 +84,19 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
{/* Terminal Text */}
<div className="space-y-4 mb-8">
<div className="text-center">
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{t.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>
<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">{subtitle}</p>
</div>
<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">
<span className="text-green-500 mr-2">$</span>
{t.description}
{tr('description')}
</p>
</div>
<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">
<span className="text-nofx-gold"></span> {benefit}
</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"
>
<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>
</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"
>
<UserPlus size={14} />
<span>{t.register}</span>
<span>{tr('registerButton')}</span>
</a>
</div>
@@ -161,7 +129,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
onClick={onClose}
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
>
[ {t.later} ]
[ {tr('abort')} ]
</button>
</div>
@@ -29,6 +29,7 @@ import {
CandlestickChart as CandlestickIcon,
} from 'lucide-react'
import { api } from '../../lib/api'
import { t, type Language } from '../../i18n/translations'
import type {
BacktestEquityPoint,
BacktestTradeEvent,
@@ -136,7 +137,7 @@ export function EquityChart({ equity, trades }: EquityChartProps) {
interface CandlestickChartProps {
runId: string
trades: BacktestTradeEvent[]
language: string
language: Language
}
export function CandlestickChartComponent({ runId, trades, language }: CandlestickChartProps) {
@@ -289,7 +290,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
if (symbols.length === 0) {
return (
<div className="py-12 text-center" style={{ color: '#5E6673' }}>
{language === 'zh' ? '没有交易记录' : 'No trades to display'}
{t('backtestChart.noTrades', language)}
</div>
)
}
@@ -300,7 +301,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
<div className="flex items-center gap-2">
<CandlestickIcon size={16} style={{ color: '#F0B90B' }} />
<span className="text-sm" style={{ color: '#848E9C' }}>
{language === 'zh' ? '币种' : 'Symbol'}
{t('backtestChart.symbol', language)}
</span>
<select
value={selectedSymbol}
@@ -319,7 +320,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
<div className="flex items-center gap-2">
<Clock size={14} style={{ color: '#848E9C' }} />
<span className="text-sm" style={{ color: '#848E9C' }}>
{language === 'zh' ? '周期' : 'Interval'}
{t('backtestChart.interval', language)}
</span>
<div className="flex rounded overflow-hidden" style={{ border: '1px solid #2B3139' }}>
{CHART_TIMEFRAMES.map((tf) => (
@@ -339,7 +340,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
</div>
<span className="text-xs" style={{ color: '#5E6673' }}>
({symbolTrades.length} {language === 'zh' ? '笔交易' : 'trades'})
({symbolTrades.length} {t('backtestChart.trades', language)})
</span>
</div>
@@ -351,7 +352,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
{isLoading && (
<div className="flex items-center justify-center h-[400px]" style={{ color: '#848E9C' }}>
<RefreshCw className="animate-spin mr-2" size={16} />
{language === 'zh' ? '加载K线数据...' : 'Loading kline data...'}
{t('backtestChart.loadingKline', language)}
</div>
)}
{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-1.5">
<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 className="flex items-center gap-1.5">
<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>
<span style={{ color: '#5E6673' }}>|</span>
<span> Long · Short · {language === 'zh' ? '平仓' : 'Close'}</span>
<span> Long · Short · {t('backtestChart.close', language)}</span>
</div>
</div>
)
@@ -384,7 +385,7 @@ interface BacktestChartTabProps {
equity: BacktestEquityPoint[] | undefined
trades: BacktestTradeEvent[] | undefined
selectedRunId: string
language: string
language: Language
tr: (key: string) => string
}
@@ -405,7 +406,7 @@ export function BacktestChartTab({
>
<div>
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '资金曲线' : 'Equity Curve'}
{t('backtestChart.equityCurve', language)}
</h4>
{equity && equity.length > 0 ? (
<EquityChart equity={equity} trades={trades ?? []} />
@@ -419,7 +420,7 @@ export function BacktestChartTab({
{selectedRunId && trades && trades.length > 0 && (
<div>
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
{language === 'zh' ? 'K线图 & 交易标记' : 'Candlestick & Trade Markers'}
{t('backtestChart.candlestickTradeMarkers', language)}
</h4>
<CandlestickChartComponent
runId={selectedRunId}
@@ -7,6 +7,8 @@ import {
Zap,
} from 'lucide-react'
import type { AIModel, Strategy } from '../../types'
import { t as globalT } from '../../i18n/translations'
import type { Language } from '../../i18n/translations'
// ============ Types ============
@@ -104,12 +106,12 @@ export function BacktestConfigForm({
}
}, [selectedStrategy])
const zh = language === 'zh'
const lang = language as Language
const quickRanges = [
{ label: zh ? '24小时' : '24h', hours: 24 },
{ label: zh ? '3天' : '3d', hours: 72 },
{ label: zh ? '7天' : '7d', hours: 168 },
{ label: zh ? '30' : '30d', hours: 720 },
{ label: globalT('backtestConfigForm.quickRange24h', lang), hours: 24 },
{ label: globalT('backtestConfigForm.quickRange3d', lang), hours: 72 },
{ label: globalT('backtestConfigForm.quickRange7d', lang), hours: 168 },
{ label: globalT('backtestConfigForm.quickRange30d', lang), hours: 720 },
]
const applyQuickRange = (hours: number) => {
@@ -144,9 +146,9 @@ export function BacktestConfigForm({
</div>
))}
<span className="ml-2 text-xs" style={{ color: '#848E9C' }}>
{wizardStep === 1 ? (zh ? '选择模型' : 'Select Model')
: wizardStep === 2 ? (zh ? '配置参数' : 'Configure')
: (zh ? '确认启动' : 'Confirm')}
{wizardStep === 1 ? globalT('backtestConfigForm.selectModel', lang)
: wizardStep === 2 ? globalT('backtestConfigForm.configure', lang)
: globalT('backtestConfigForm.confirmStart', lang)}
</span>
</div>
@@ -196,7 +198,7 @@ export function BacktestConfigForm({
{/* Strategy Selection (Optional) */}
<div>
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
{zh ? '策略配置(可选)' : 'Strategy (Optional)'}
{globalT('backtestConfigForm.strategyOptional', lang)}
</label>
<select
className="w-full p-3 rounded-lg text-sm"
@@ -204,7 +206,7 @@ export function BacktestConfigForm({
value={formState.strategyId}
onChange={(e) => onFormChange('strategyId', e.target.value)}
>
<option value="">{zh ? '不使用保存的策略' : 'No saved strategy'}</option>
<option value="">{globalT('backtestConfigForm.noSavedStrategy', lang)}</option>
{strategies?.map((s) => (
<option key={s.id} value={s.id}>
{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="flex items-center gap-2 text-xs">
<span style={{ color: '#F0B90B' }}>
{zh ? '币种来源:' : 'Coin Source:'}
{globalT('backtestConfigForm.coinSource', lang)}
</span>
<span className="font-medium" style={{ color: '#EAECEF' }}>
{coinSourceDescription.type}
@@ -225,9 +227,7 @@ export function BacktestConfigForm({
</div>
{strategyHasDynamicCoins && (
<div className="text-xs mt-1" style={{ color: '#F0B90B' }}>
{zh
? '⚡ 清空下方币种输入框即可使用策略的动态币种'
: '⚡ Clear the symbols field below to use strategy\'s dynamic coins'}
{globalT('backtestConfigForm.clearDynamicCoins', lang)}
</div>
)}
</div>
@@ -239,7 +239,7 @@ export function BacktestConfigForm({
{tr('form.symbolsLabel')}
{strategyHasDynamicCoins && (
<span className="ml-2" style={{ color: '#5E6673' }}>
({zh ? '可选 - 策略已配置币种来源' : 'Optional - strategy has coin source'})
({globalT('backtestConfigForm.optionalCoinSource', lang)})
</span>
)}
</label>
@@ -283,7 +283,7 @@ export function BacktestConfigForm({
onChange={(e) => onFormChange('symbols', e.target.value)}
rows={2}
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"
style={{ background: '#F0B90B', color: '#0B0E11' }}
>
{zh ? '清空使用策略币种' : 'Clear to use strategy'}
{globalT('backtestConfigForm.clearToUseStrategy', lang)}
</button>
)}
</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"
style={{ background: '#F0B90B', color: '#0B0E11' }}
>
{zh ? '下一步' : 'Next'}
{globalT('backtestConfigForm.next', lang)}
<ChevronRight className="w-4 h-4" />
</button>
</motion.div>
@@ -359,7 +359,7 @@ export function BacktestConfigForm({
<div>
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
{zh ? '时间周期' : 'Timeframes'}
{globalT('backtestConfigForm.timeframes', lang)}
</label>
<div className="flex flex-wrap gap-1">
{TIMEFRAME_OPTIONS.map((tf) => {
@@ -428,7 +428,7 @@ export function BacktestConfigForm({
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
>
<ChevronLeft className="w-4 h-4" />
{zh ? '上一步' : 'Back'}
{globalT('backtestConfigForm.back', lang)}
</button>
<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"
style={{ background: '#F0B90B', color: '#0B0E11' }}
>
{zh ? '下一步' : 'Next'}
{globalT('backtestConfigForm.next', lang)}
<ChevronRight className="w-4 h-4" />
</button>
</div>
@@ -520,7 +520,7 @@ export function BacktestConfigForm({
<div>
<label className="block text-xs mb-1" style={{ color: '#848E9C' }}>
{zh ? '策略风格' : 'Strategy Style'}
{globalT('backtestConfigForm.strategyStyle', lang)}
</label>
<div className="flex flex-wrap gap-1">
{['baseline', 'aggressive', 'conservative', 'scalping'].map((p) => (
@@ -570,7 +570,7 @@ export function BacktestConfigForm({
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
>
<ChevronLeft className="w-4 h-4" />
{zh ? '上一步' : 'Back'}
{globalT('backtestConfigForm.back', lang)}
</button>
<button
type="submit"
@@ -7,6 +7,7 @@ import {
ArrowDownRight,
} from 'lucide-react'
import { MetricTooltip } from '../common/MetricTooltip'
import { t, type Language } from '../../i18n/translations'
import { EquityChart } from './BacktestChartTab'
import type {
BacktestEquityPoint,
@@ -131,7 +132,7 @@ export function ProgressRing({ progress, size = 120 }: ProgressRingProps) {
interface PositionsDisplayProps {
positions: BacktestPositionStatus[]
language: string
language: Language
}
export function PositionsDisplay({ positions, language }: PositionsDisplayProps) {
@@ -151,7 +152,7 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
<div className="flex items-center gap-2">
<Activity className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '当前持仓' : 'Active Positions'}
{t('backtestOverview.activePositions', language)}
</span>
<span
className="px-1.5 py-0.5 rounded text-xs"
@@ -162,13 +163,13 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
</div>
<div className="flex items-center gap-3 text-xs">
<span style={{ color: '#848E9C' }}>
{language === 'zh' ? '保证金' : 'Margin'}: ${totalMargin.toFixed(2)}
{t('backtestOverview.margin', language)}: ${totalMargin.toFixed(2)}
</span>
<span
className="font-medium"
style={{ color: totalUnrealizedPnL >= 0 ? '#0ECB81' : '#F6465D' }}
>
{language === 'zh' ? '浮盈' : 'Unrealized'}: {totalUnrealizedPnL >= 0 ? '+' : ''}
{t('backtestOverview.unrealized', language)}: {totalUnrealizedPnL >= 0 ? '+' : ''}
${totalUnrealizedPnL.toFixed(2)}
</span>
</div>
@@ -214,8 +215,8 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
</span>
</div>
<div className="text-[10px]" style={{ color: '#5E6673' }}>
{language === 'zh' ? '数量' : 'Qty'}: {pos.quantity.toFixed(4)} ·{' '}
{language === 'zh' ? '保证金' : 'Margin'}: ${pos.margin_used.toFixed(2)}
{t('backtestOverview.qty', language)}: {pos.quantity.toFixed(4)} ·{' '}
{t('backtestOverview.margin', language)}: ${pos.margin_used.toFixed(2)}
</div>
</div>
</div>
@@ -223,10 +224,10 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
<div className="text-right">
<div className="flex items-center gap-2 text-xs">
<span style={{ color: '#848E9C' }}>
{language === 'zh' ? '开仓' : 'Entry'}: ${pos.entry_price.toFixed(2)}
{t('backtestOverview.entry', language)}: ${pos.entry_price.toFixed(2)}
</span>
<span style={{ color: '#EAECEF' }}>
{language === 'zh' ? '现价' : 'Mark'}: ${pos.mark_price.toFixed(2)}
{t('backtestOverview.mark', language)}: ${pos.mark_price.toFixed(2)}
</span>
</div>
<div className="flex items-center justify-end gap-1.5 mt-0.5">
@@ -255,7 +256,7 @@ interface BacktestOverviewTabProps {
equity: BacktestEquityPoint[] | undefined
trades: BacktestTradeEvent[] | undefined
metrics: BacktestMetrics | undefined
language: string
language: Language
tr: (key: string) => string
}
@@ -285,7 +286,7 @@ export function BacktestOverviewTab({
<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="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} />
</div>
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
@@ -294,7 +295,7 @@ export function BacktestOverviewTab({
</div>
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
<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} />
</div>
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
@@ -303,7 +304,7 @@ export function BacktestOverviewTab({
</div>
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
<div className="text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '总交易数' : 'Total Trades'}
{t('backtestOverview.totalTrades', language)}
</div>
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
{metrics.trades ?? 0}
@@ -311,7 +312,7 @@ export function BacktestOverviewTab({
</div>
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
<div className="text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '最佳币种' : 'Best Symbol'}
{t('backtestOverview.bestSymbol', language)}
</div>
<div className="text-lg font-bold" style={{ color: '#0ECB81' }}>
{metrics.best_symbol?.replace('USDT', '') || '-'}
+12 -20
View File
@@ -244,9 +244,9 @@ export function BacktestPage() {
const handleDelete = async () => {
if (!selectedRunId) return
const confirmed = await confirmToast(tr('toasts.confirmDelete', { id: selectedRunId }), {
title: language === 'zh' ? '确认删除' : 'Confirm Delete',
okText: language === 'zh' ? '删除' : 'Delete',
cancelText: language === 'zh' ? '取消' : 'Cancel',
title: t('backtestPageExtra.confirmDelete', language),
okText: t('backtestPageExtra.delete', language),
cancelText: t('backtestPageExtra.cancel', language),
})
if (!confirmed) return
try {
@@ -328,7 +328,7 @@ export function BacktestPage() {
style={{ background: '#F0B90B', color: '#0B0E11' }}
>
<Play className="w-4 h-4" />
{language === 'zh' ? '新建回测' : 'New Backtest'}
{t('backtestPageExtra.newBacktest', language)}
</button>
</div>
@@ -474,14 +474,14 @@ export function BacktestPage() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard
icon={Target}
label={language === 'zh' ? '当前净值' : 'Equity'}
label={t('backtestPageExtra.equity', language)}
value={(status?.equity ?? 0).toFixed(2)}
suffix="USDT"
language={language}
/>
<StatCard
icon={TrendingUp}
label={language === 'zh' ? '总收益率' : 'Return'}
label={t('backtestPageExtra.totalReturn', language)}
value={`${(metrics?.total_return_pct ?? 0).toFixed(2)}%`}
trend={(metrics?.total_return_pct ?? 0) >= 0 ? 'up' : 'down'}
color={(metrics?.total_return_pct ?? 0) >= 0 ? '#0ECB81' : '#F6465D'}
@@ -490,7 +490,7 @@ export function BacktestPage() {
/>
<StatCard
icon={AlertTriangle}
label={language === 'zh' ? '最大回撤' : 'Max DD'}
label={t('backtestPageExtra.maxDD', language)}
value={`${(metrics?.max_drawdown_pct ?? 0).toFixed(2)}%`}
color="#F6465D"
metricKey="max_drawdown"
@@ -498,7 +498,7 @@ export function BacktestPage() {
/>
<StatCard
icon={BarChart3}
label={language === 'zh' ? '夏普比率' : 'Sharpe'}
label={t('backtestPageExtra.sharpe', language)}
value={(metrics?.sharpe_ratio ?? 0).toFixed(2)}
metricKey="sharpe_ratio"
language={language}
@@ -516,20 +516,12 @@ export function BacktestPage() {
style={{ color: viewTab === tab ? '#F0B90B' : '#848E9C' }}
>
{tab === 'overview'
? language === 'zh'
? '概览'
: 'Overview'
? t('backtestPageExtra.tabOverview', language)
: tab === 'chart'
? language === 'zh'
? '图表'
: 'Chart'
? t('backtestPageExtra.tabChart', language)
: tab === 'trades'
? language === 'zh'
? '交易'
: 'Trades'
: language === 'zh'
? 'AI决策'
: 'Decisions'}
? t('backtestPageExtra.tabTrades', language)
: t('backtestPageExtra.tabDecisions', language)}
{viewTab === tab && (
<motion.div
layoutId="tab-indicator"
@@ -7,6 +7,7 @@ import {
Layers,
Eye,
} from 'lucide-react'
import { t, type Language } from '../../i18n/translations'
// ============ Types ============
@@ -61,7 +62,7 @@ interface BacktestRunListProps {
runs: BacktestRunItem[]
selectedRunId: string | undefined
compareRunIds: string[]
language: string
language: Language
tr: (key: string, params?: Record<string, string | number>) => string
onSelectRun: (runId: string) => void
onToggleCompare: (runId: string) => void
@@ -84,7 +85,7 @@ export function BacktestRunList({
{tr('runList.title')}
</h3>
<span className="text-xs" style={{ color: '#848E9C' }}>
{runs.length} {language === 'zh' ? '条' : 'runs'}
{runs.length} {t('backtestPageExtra.runs', language)}
</span>
</div>
@@ -131,7 +132,7 @@ export function BacktestRunList({
? 'rgba(240,185,11,0.2)'
: 'transparent',
}}
title={language === 'zh' ? '添加到对比' : 'Add to compare'}
title={t('backtestPageExtra.addToCompare', language)}
>
<Eye
className="w-3 h-3"
+115 -114
View File
@@ -12,6 +12,7 @@ import {
} from 'lightweight-charts'
import { useLanguage } from '../../contexts/LanguageContext'
import { httpClient } from '../../lib/httpClient'
import { t } from '../../i18n/translations'
import {
calculateSMA,
calculateEMA,
@@ -20,26 +21,26 @@ import {
} from '../../utils/indicators'
import { Settings, BarChart2 } from 'lucide-react'
// 订单接口定义
// Order marker interface
interface OrderMarker {
time: number
price: number
side: 'long' | 'short'
rawSide: string // 原始 side 字段 (buy/sell from database)
rawSide: string // Original side field (buy/sell from database)
action: 'open' | 'close'
pnl?: number
symbol: string
}
// 挂单接口定义 (交易所的止盈止损订单)
// Open orders interface (exchange TP/SL orders)
interface OpenOrder {
order_id: string
symbol: string
side: string // BUY/SELL
position_side: string // LONG/SHORT
type: string // LIMIT/STOP_MARKET/TAKE_PROFIT_MARKET
price: number // 限价单价格
stop_price: number // 触发价格 (止损/止盈)
price: number // Limit order price
stop_price: number // Trigger price (SL/TP)
quantity: number
status: string
}
@@ -49,11 +50,11 @@ interface AdvancedChartProps {
interval?: string
traderID?: string
height?: number
exchange?: string // 交易所类型:binance, bybit, okx, bitget, hyperliquid, aster, lighter
onSymbolChange?: (symbol: string) => void // 币种切换回调
exchange?: string // Exchange type: binance, bybit, okx, bitget, hyperliquid, aster, lighter
onSymbolChange?: (symbol: string) => void // Symbol change callback
}
// 指标配置
// Indicator configuration
interface IndicatorConfig {
id: string
name: string
@@ -62,31 +63,31 @@ interface IndicatorConfig {
params?: any
}
// 获取成交额货币单位
// Get quote currency unit
const getQuoteUnit = (exchange: string): string => {
if (['alpaca'].includes(exchange)) {
return 'USD'
}
if (['forex', 'metals'].includes(exchange)) {
return '' // 外汇/贵金属没有真实成交量
return '' // Forex/metals have no real volume
}
return 'USDT' // 加密货币默认 USDT
return 'USDT' // Crypto defaults to USDT
}
// 获取成交量数量单位
const getBaseUnit = (exchange: string, symbol: string): string => {
// Get base volume unit
const getBaseUnit = (exchange: string, symbol: string, language: string): string => {
if (['alpaca'].includes(exchange)) {
return '股'
return t('advancedChart.shares', language as 'en' | 'zh' | 'id')
}
if (['forex', 'metals'].includes(exchange)) {
return ''
}
// 加密货币:从 symbol 提取基础资产
// Crypto: extract base asset from symbol
const base = symbol.replace(/USDT$|USD$|BUSD$/, '')
return base || '个'
return base || t('advancedChart.units', language as 'en' | 'zh' | 'id')
}
// 格式化大数字
// Format large numbers
const formatVolume = (value: number): string => {
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B'
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M'
@@ -99,43 +100,43 @@ export function AdvancedChart({
interval = '5m',
traderID,
height = 550,
exchange = 'binance', // 默认使用 binance
exchange = 'binance', // Default to binance
onSymbolChange: _onSymbolChange, // Available for future use
}: AdvancedChartProps) {
void _onSymbolChange // Prevent unused warning
const { language } = useLanguage()
const quoteUnit = getQuoteUnit(exchange)
const baseUnit = getBaseUnit(exchange, symbol)
const baseUnit = getBaseUnit(exchange, symbol, language)
const chartContainerRef = useRef<HTMLDivElement>(null)
const chartRef = useRef<IChartApi | null>(null)
const candlestickSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null)
const volumeSeriesRef = useRef<ISeriesApi<'Histogram'> | null>(null)
const indicatorSeriesRef = useRef<Map<string, ISeriesApi<any>>>(new Map())
const seriesMarkersRef = useRef<any>(null) // Markers primitive for v5
const currentMarkersDataRef = useRef<any[]>([]) // 存储当前的标记数据
const klineDataRef = useRef<Map<number, { volume: number; quoteVolume: number }>>(new Map()) // 存储 kline 额外数据
const priceLinesRef = useRef<any[]>([]) // 存储挂单价格线
const currentMarkersDataRef = useRef<any[]>([]) // Store current marker data
const klineDataRef = useRef<Map<number, { volume: number; quoteVolume: number }>>(new Map()) // Store kline extra data
const priceLinesRef = useRef<any[]>([]) // Store open order price lines
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showIndicatorPanel, setShowIndicatorPanel] = useState(false)
const [showOrderMarkers, setShowOrderMarkers] = useState(true) // 订单标记显示开关,默认显示
const isInitialLoadRef = useRef(true) // 跟踪是否为初始加载
const [showOrderMarkers, setShowOrderMarkers] = useState(true) // Order marker toggle, default on
const isInitialLoadRef = useRef(true) // Track if this is initial load
const [tooltipData, setTooltipData] = useState<any>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
// 行情统计数据(当前K线)
// Market stats (current candle)
const [marketStats, setMarketStats] = useState<{
price: number
priceChange: number
priceChangePercent: number
high: number
low: number
volume: number // 数量(BTC/股数)
quoteVolume: number // 成交额(USDT/USD
volume: number // Quantity (BTC/shares)
quoteVolume: number // Turnover (USDT/USD)
} | null>(null)
// 指标配置
// Indicator configuration
const [indicators, setIndicators] = useState<IndicatorConfig[]>([
{ id: 'volume', name: 'Volume', enabled: true, color: '#3B82F6' },
{ id: 'ma5', name: 'MA5', enabled: false, color: '#FF6B6B', params: { period: 5 } },
@@ -147,7 +148,7 @@ export function AdvancedChart({
{ id: 'bb', name: 'Bollinger Bands', enabled: false, color: '#9B59B6' },
])
// 从服务获取K线数据
// Fetch kline data from service
const fetchKlineData = async (symbol: string, interval: string) => {
try {
const limit = 1500
@@ -158,18 +159,18 @@ export function AdvancedChart({
throw new Error('Failed to fetch kline data')
}
// 转换数据格式
// Convert data format
const rawData = result.data.map((candle: any) => ({
time: Math.floor(candle.openTime / 1000) as UTCTimestamp,
open: candle.open,
high: candle.high,
low: candle.low,
close: candle.close,
volume: candle.volume, // 数量(BTC/股数)
quoteVolume: candle.quoteVolume, // 成交额(USDT/USD
volume: candle.volume, // Quantity (BTC/shares)
quoteVolume: candle.quoteVolume, // Turnover (USDT/USD)
}))
// 按时间排序并去重(lightweight-charts 要求数据按时间升序且无重复)
// Sort by time and deduplicate (lightweight-charts requires ascending, unique times)
const sortedData = rawData.sort((a: any, b: any) => a.time - b.time)
const dedupedData = sortedData.filter((item: any, index: number, arr: any[]) =>
index === 0 || item.time !== arr[index - 1].time
@@ -186,16 +187,16 @@ export function AdvancedChart({
}
}
// 解析时间:支持 Unix 时间戳(数字)或字符串格式
// Parse time: supports Unix timestamp (number) or string format
const parseCustomTime = (time: any): number => {
if (!time) {
console.warn('[AdvancedChart] Empty time value')
return 0
}
// 如果已经是数字(Unix 时间戳)
// If already a number (Unix timestamp)
if (typeof time === 'number') {
// 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳)
// Determine ms vs seconds: if > 10^12, treat as milliseconds
if (time > 1000000000000) {
const seconds = Math.floor(time / 1000)
console.log('[AdvancedChart] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
@@ -208,7 +209,7 @@ export function AdvancedChart({
const timeStr = String(time)
console.log('[AdvancedChart] Parsing time string:', timeStr)
// 尝试标准ISO格式
// Try standard ISO format
const isoTime = new Date(timeStr).getTime()
if (!isNaN(isoTime) && isoTime > 0) {
const timestamp = Math.floor(isoTime / 1000)
@@ -216,7 +217,7 @@ export function AdvancedChart({
return timestamp
}
// 解析自定义格式 "MM-DD HH:mm UTC" (兼容旧数据)
// Parse custom format "MM-DD HH:mm UTC" (for legacy data)
const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
if (match) {
const currentYear = new Date().getFullYear()
@@ -237,11 +238,11 @@ export function AdvancedChart({
return 0
}
// 获取订单数据
// Fetch order data
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
try {
console.log('[AdvancedChart] Fetching orders for trader:', traderID, 'symbol:', symbol)
// 获取已成交的订单,增加到200条以显示更多历史订单
// Fetch filled orders, up to 200 for more history
const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=200`)
console.log('[AdvancedChart] Orders API response:', result)
@@ -258,14 +259,14 @@ export function AdvancedChart({
orders.forEach((order: any) => {
console.log('[AdvancedChart] Processing order:', order)
// 处理字段名:支持PascalCasesnake_case
// Handle field names: support PascalCase and snake_case
const filledAt = order.filled_at || order.FilledAt || order.created_at || order.CreatedAt
const avgPrice = order.avg_fill_price || order.AvgFillPrice || order.price || order.Price
const orderAction = order.order_action || order.OrderAction
const side = (order.side || order.Side)?.toLowerCase() // BUY/SELL
const symbol = order.symbol || order.Symbol
// 跳过没有成交时间或价格的订单
// Skip orders without fill time or price
if (!filledAt || !avgPrice || avgPrice === 0) {
console.warn('[AdvancedChart] Skipping order - missing data:', { filledAt, avgPrice })
return
@@ -277,7 +278,7 @@ export function AdvancedChart({
return
}
// 根据 order_action 判断是开仓还是平仓
// Determine open/close from order_action
let action: 'open' | 'close' = 'open'
let positionSide: 'long' | 'short' = 'long'
@@ -290,7 +291,7 @@ export function AdvancedChart({
positionSide = orderAction.includes('LONG') ? 'long' : 'short'
}
} else {
// 如果没有 order_action,根据 side 判断
// If no order_action, infer from side
positionSide = side === 'buy' ? 'long' : 'short'
}
@@ -307,7 +308,7 @@ export function AdvancedChart({
time: timeSeconds,
price: avgPrice,
side: positionSide,
rawSide: side, // 原始 side 字段 (buy/sell)
rawSide: side, // Original side field (buy/sell)
action: action,
symbol,
})
@@ -321,7 +322,7 @@ export function AdvancedChart({
}
}
// 获取交易所挂单 (止盈止损订单)
// Fetch exchange open orders (TP/SL)
const fetchOpenOrders = async (traderID: string, symbol: string): Promise<OpenOrder[]> => {
try {
console.log('[AdvancedChart] Fetching open orders for trader:', traderID, 'symbol:', symbol)
@@ -341,7 +342,7 @@ export function AdvancedChart({
}
}
// 初始化图表
// Initialize chart
useEffect(() => {
if (!chartContainerRef.current) return
@@ -424,7 +425,7 @@ export function AdvancedChart({
chartRef.current = chart
// 创建K线系列
// Create candlestick series
const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#0ECB81',
downColor: '#F6465D',
@@ -435,7 +436,7 @@ export function AdvancedChart({
})
candlestickSeriesRef.current = candlestickSeries as any
// 创建成交量系列
// Create volume series
const volumeSeries = chart.addSeries(HistogramSeries, {
color: '#26a69a',
priceFormat: {
@@ -447,7 +448,7 @@ export function AdvancedChart({
})
volumeSeriesRef.current = volumeSeries as any
// 响应式调整 (ResizeObserver)
// Responsive resize (ResizeObserver)
const resizeObserver = new ResizeObserver((entries) => {
if (entries.length === 0 || !entries[0].contentRect) return
const { width, height } = entries[0].contentRect
@@ -458,7 +459,7 @@ export function AdvancedChart({
resizeObserver.observe(chartContainerRef.current)
}
// 监听鼠标移动,显示 OHLC 信息
// Listen for crosshair movement to show OHLC info
chart.subscribeCrosshairMove((param) => {
if (!param.time || !param.point || !candlestickSeriesRef.current) {
setTooltipData(null)
@@ -473,7 +474,7 @@ export function AdvancedChart({
const candleData = data as any
// 从存储的数据中获取 volume quoteVolume
// Get volume and quoteVolume from stored data
const klineExtra = klineDataRef.current.get(param.time as number) || { volume: 0, quoteVolume: 0 }
setTooltipData({
@@ -496,18 +497,18 @@ export function AdvancedChart({
}, []) // Chart is created once, ResizeObserver handles dimension changes
// 加载数据和指标
// Load data and indicators
useEffect(() => {
// symbolinterval 改变时,重置初始加载标志(以便自动适配新数据)
// Reset initial load flag when symbol/interval changes (for auto-fit)
isInitialLoadRef.current = true
// 清除旧的标记数据,避免旧数据影响新图表
// Clear old marker data to prevent stale data in new chart
currentMarkersDataRef.current = []
if (seriesMarkersRef.current) {
try {
seriesMarkersRef.current.setMarkers([])
} catch (e) {
// 忽略错误,稍后会重新创建
// Ignore errors, will be recreated later
}
seriesMarkersRef.current = null
}
@@ -516,30 +517,30 @@ export function AdvancedChart({
if (!candlestickSeriesRef.current) return
console.log('[AdvancedChart] Loading data for', symbol, interval, isRefresh ? '(refresh)' : '')
// 只在首次加载时显示 loading,刷新时不显示避免闪烁
// Only show loading on first load, avoid flicker on refresh
if (!isRefresh) {
setLoading(true)
}
setError(null)
try {
// 1. 获取K线数据
// 1. Fetch kline data
const klineData = await fetchKlineData(symbol, interval)
console.log('[AdvancedChart] Loaded', klineData.length, 'klines')
candlestickSeriesRef.current.setData(klineData)
// 存储 volume/quoteVolume 数据供 tooltip 使用
// Store volume/quoteVolume data for tooltip
klineDataRef.current.clear()
klineData.forEach((k: any) => {
klineDataRef.current.set(k.time, { volume: k.volume || 0, quoteVolume: k.quoteVolume || 0 })
})
// 1.5 计算行情统计数据
// 1.5 Calculate market stats
if (klineData.length > 1) {
const latestKline = klineData[klineData.length - 1]
const prevKline = klineData[klineData.length - 2]
// 涨跌幅:当前K线收盘价 vs 前一根K线收盘价
// Price change: current candle close vs previous candle close
const priceChange = latestKline.close - prevKline.close
const priceChangePercent = (priceChange / prevKline.close) * 100
@@ -565,7 +566,7 @@ export function AdvancedChart({
})
}
// 2. 显示成交量
// 2. Display volume
if (volumeSeriesRef.current) {
const volumeEnabled = indicators.find(i => i.id === 'volume')?.enabled
if (volumeEnabled) {
@@ -576,15 +577,15 @@ export function AdvancedChart({
}))
volumeSeriesRef.current.setData(volumeData)
} else {
// 关闭成交量时清空数据
// Clear data when volume is disabled
volumeSeriesRef.current.setData([])
}
}
// 3. 添加指标
// 3. Add indicators
updateIndicators(klineData)
// 4. 获取并显示订单标记
// 4. Fetch and display order markers
if (traderID && candlestickSeriesRef.current) {
console.log('[AdvancedChart] Starting to fetch orders...')
const orders = await fetchOrders(traderID, symbol)
@@ -593,17 +594,17 @@ export function AdvancedChart({
if (orders.length > 0) {
console.log('[AdvancedChart] Creating markers from', orders.length, 'orders')
// 提取 K 线时间数组(已排序)
// Extract sorted kline time array
const klineTimes = klineData.map((k: any) => k.time as number)
const klineMinTime = klineTimes[0] || 0
const klineMaxTime = klineTimes[klineTimes.length - 1] || 0
console.log('[AdvancedChart] Kline time range:', klineMinTime, '-', klineMaxTime, '(', klineTimes.length, 'candles)')
// 二分查找:找到订单时间所属的 K 线蜡烛
// 返回 time <= orderTime 的最大 K 线时间
// Binary search: find the kline candle for the order time
// Return the largest kline time <= orderTime
const findCandleTime = (orderTime: number): number | null => {
if (orderTime < klineMinTime || orderTime > klineMaxTime) {
return null // 超出范围
return null // Out of range
}
let left = 0
@@ -621,11 +622,11 @@ export function AdvancedChart({
return klineTimes[left]
}
// 按 K 线时间分组统计订单
// Group orders by kline time
const ordersByCandle = new Map<number, { buys: number; sells: number }>()
orders.forEach(order => {
// 使用二分查找找到对应的 K 线蜡烛时间
// Use binary search to find matching kline candle time
const candleTime = findCandleTime(order.time)
if (candleTime === null) {
@@ -643,7 +644,7 @@ export function AdvancedChart({
ordersByCandle.set(candleTime, existing)
})
// 为每个有订单的 K 线创建标记
// Create markers for each kline with orders
const markers: Array<{
time: Time
position: 'belowBar' | 'aboveBar'
@@ -654,7 +655,7 @@ export function AdvancedChart({
}> = []
ordersByCandle.forEach((counts, candleTime) => {
// 显示买入标记(绿色,在K线下方)
// Show buy markers (green, below bar)
if (counts.buys > 0) {
markers.push({
time: candleTime as Time,
@@ -665,7 +666,7 @@ export function AdvancedChart({
size: 1,
})
}
// 显示卖出标记(红色,在K线上方)
// Show sell markers (red, above bar)
if (counts.sells > 0) {
markers.push({
time: candleTime as Time,
@@ -678,7 +679,7 @@ export function AdvancedChart({
}
})
// 按时间排序(lightweight-charts 要求标记按时间顺序)
// Sort by time (lightweight-charts requires chronological order)
markers.sort((a, b) => (a.time as number) - (b.time as number))
console.log('[AdvancedChart] Valid markers:', markers.length, 'out of', orders.length)
@@ -687,17 +688,17 @@ export function AdvancedChart({
console.log('[AdvancedChart] Markers data:', JSON.stringify(markers, null, 2))
try {
// 存储标记数据供后续切换使用
// Store marker data for later toggle use
currentMarkersDataRef.current = markers
// 使用 v5 API: createSeriesMarkers
// Using v5 API: createSeriesMarkers
const markersToShow = showOrderMarkers ? markers : []
if (seriesMarkersRef.current) {
// 如果已经存在,更新标记
// If already exists, update markers
seriesMarkersRef.current.setMarkers(markersToShow)
} else {
// 首次创建标记
// First time creating markers
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markersToShow)
}
console.log('[AdvancedChart] ✅ Markers updated! Count:', markersToShow.length, 'Visible:', showOrderMarkers)
@@ -721,7 +722,7 @@ export function AdvancedChart({
})
}
// 只在初始加载时自动适配视图,避免刷新时抖动
// Auto-fit view only on initial load, avoid jitter on refresh
if (isInitialLoadRef.current) {
chartRef.current?.timeScale().fitContent()
isInitialLoadRef.current = false
@@ -734,26 +735,26 @@ export function AdvancedChart({
}
}
loadData(false) // 首次加载
loadData(false) // Initial load
// 实时自动刷新 (5秒更新一次)
// Real-time auto-refresh (every 5 seconds)
const refreshInterval = setInterval(() => loadData(true), 5000)
return () => clearInterval(refreshInterval)
}, [symbol, interval, traderID, exchange])
// 单独刷新挂单价格线 (60秒刷新一次,避免频繁调用交易所API)
// Refresh open order price lines separately (every 60s, avoid frequent exchange API calls)
useEffect(() => {
if (!traderID || !candlestickSeriesRef.current) return
// 加载挂单并显示价格线
// Load open orders and display price lines
const loadOpenOrders = async () => {
try {
// 先清除旧的价格线
// Clear old price lines first
priceLinesRef.current.forEach(line => {
try {
candlestickSeriesRef.current?.removePriceLine(line)
} catch (e) {
// 忽略清除错误
// Ignore clear error
}
})
priceLinesRef.current = []
@@ -763,28 +764,28 @@ export function AdvancedChart({
if (openOrders.length > 0 && candlestickSeriesRef.current) {
openOrders.forEach(order => {
// 获取触发价格 (止损/止盈用 stop_price,限价单用 price)
// Get trigger price (SL/TP use stop_price, limit orders use price)
const linePrice = order.stop_price > 0 ? order.stop_price : order.price
if (linePrice <= 0) return
// 判断订单类型
// Determine order type
const isStopLoss = order.type.includes('STOP') || order.type.includes('SL')
const isTakeProfit = order.type.includes('TAKE_PROFIT') || order.type.includes('TP')
const isLimit = order.type === 'LIMIT'
// 设置价格线样式
let lineColor = '#F0B90B' // 默认黄色
const lineStyle = 2 // 虚线
// Set price line style
let lineColor = '#F0B90B' // Default yellow
const lineStyle = 2 // dashed
let title = ''
if (isStopLoss) {
lineColor = '#F6465D' // 红色 - 止损
lineColor = '#F6465D' // red - stop loss
title = `SL ${order.quantity}`
} else if (isTakeProfit) {
lineColor = '#0ECB81' // 绿色 - 止盈
lineColor = '#0ECB81' // green - take profit
title = `TP ${order.quantity}`
} else if (isLimit) {
lineColor = '#F0B90B' // 黄色 - 限价单
lineColor = '#F0B90B' // yellow - limit order
title = `Limit ${order.side} ${order.quantity}`
} else {
title = `${order.type} ${order.quantity}`
@@ -810,10 +811,10 @@ export function AdvancedChart({
}
}
// 初始加载 (延迟1秒等待图表初始化完成)
// Initial load (delay 1s to wait for chart initialization)
const initialTimeout = setTimeout(loadOpenOrders, 1000)
// 60秒刷新一次挂单
// Refresh open orders every 60 seconds
const openOrdersInterval = setInterval(loadOpenOrders, 60000)
return () => {
@@ -822,7 +823,7 @@ export function AdvancedChart({
}
}, [symbol, traderID])
// 单独处理订单标记的显示/隐藏,避免重新加载数据
// Handle order marker show/hide separately to avoid reloading data
useEffect(() => {
if (!seriesMarkersRef.current) return
@@ -835,17 +836,17 @@ export function AdvancedChart({
}
}, [showOrderMarkers])
// 更新指标
// Update indicators
const updateIndicators = (klineData: Kline[]) => {
if (!chartRef.current) return
// 清除旧指标
// Clear old indicators
indicatorSeriesRef.current.forEach(series => {
chartRef.current?.removeSeries(series as any)
})
indicatorSeriesRef.current.clear()
// 添加启用的指标
// Add enabled indicators
indicators.forEach(indicator => {
if (!indicator.enabled || !chartRef.current) return
@@ -864,7 +865,7 @@ export function AdvancedChart({
color: indicator.color,
lineWidth: 2,
title: indicator.name,
lineStyle: 2, // 虚线
lineStyle: 2, // dashed
})
series.setData(emaData as any)
indicatorSeriesRef.current.set(indicator.id, series)
@@ -900,7 +901,7 @@ export function AdvancedChart({
})
}
// 切换指标
// Toggle indicator
const toggleIndicator = (id: string) => {
setIndicators(prev =>
prev.map(ind => (ind.id === id ? { ...ind, enabled: !ind.enabled } : ind))
@@ -980,7 +981,7 @@ export function AdvancedChart({
<div className="flex items-center gap-1.5">
{loading && (
<span className="text-[10px] text-yellow-400 animate-pulse mr-2">
{language === 'zh' ? '更新中...' : 'Updating...'}
{t('advancedChart.updating', language)}
</span>
)}
<button
@@ -992,7 +993,7 @@ export function AdvancedChart({
}}
>
<Settings className="w-3 h-3" />
<span>{language === 'zh' ? '指标' : 'Indicators'}</span>
<span>{t('advancedChart.indicators', language)}</span>
</button>
<button
@@ -1002,14 +1003,14 @@ export function AdvancedChart({
background: showOrderMarkers ? 'rgba(16, 185, 129, 0.15)' : 'transparent',
color: showOrderMarkers ? '#10B981' : '#6B7280',
}}
title={language === 'zh' ? '订单标记' : 'Order Markers'}
title={t('advancedChart.orderMarkers', language)}
>
<span>B/S</span>
</button>
</div>
</div>
{/* 指标面板 - 专业化设计 */}
{/* Indicator panel - professional design */}
{showIndicatorPanel && (
<div
className="absolute top-16 right-4 z-10 rounded-lg shadow-2xl backdrop-blur-sm"
@@ -1021,7 +1022,7 @@ export function AdvancedChart({
overflowY: 'auto',
}}
>
{/* 标题栏 */}
{/* Title bar */}
<div
className="flex items-center justify-between px-4 py-3 border-b"
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
@@ -1029,7 +1030,7 @@ export function AdvancedChart({
<div className="flex items-center gap-2">
<BarChart2 className="w-4 h-4 text-yellow-400" />
<h4 className="text-sm font-bold text-white">
{language === 'zh' ? '技术指标' : 'Technical Indicators'}
{t('advancedChart.technicalIndicators', language)}
</h4>
</div>
<button
@@ -1040,7 +1041,7 @@ export function AdvancedChart({
</button>
</div>
{/* 指标列表 */}
{/* Indicator list */}
<div className="p-3 space-y-1">
{indicators.map(indicator => (
<label
@@ -1069,17 +1070,17 @@ export function AdvancedChart({
))}
</div>
{/* 底部提示 */}
{/* Bottom hint */}
<div
className="px-4 py-2 text-xs text-gray-500 border-t"
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
>
{language === 'zh' ? '点击选择需要显示的指标' : 'Click to toggle indicators'}
{t('advancedChart.clickToToggle', language)}
</div>
</div>
)}
{/* 图表容器 */}
{/* Chart container */}
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
<div ref={chartContainerRef} style={{ height: '100%', width: '100%' }} />
@@ -1151,7 +1152,7 @@ export function AdvancedChart({
</div>
)}
{/* NOFX 水印 */}
{/* NOFX watermark */}
<div
style={{
position: 'absolute',
@@ -1177,7 +1178,7 @@ export function AdvancedChart({
</div>
</div>
{/* 错误提示 */}
{/* Error message */}
{error && (
<div
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 { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
import { chartTabs, ts } from '../../i18n/strategy-translations'
import { BarChart3, CandlestickChart, ChevronDown, Search } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
interface ChartTabsProps {
traderId: string
selectedSymbol?: string // 从外部选择的币种
updateKey?: number // 强制更新的 key
exchangeId?: string // 交易所ID
selectedSymbol?: string // Externally selected symbol
updateKey?: number // Force update key
exchangeId?: string // Exchange ID
}
type ChartTab = 'equity' | 'kline'
@@ -23,13 +24,13 @@ interface SymbolInfo {
category: string
}
// 市场类型配置
// Market type configuration
const MARKET_CONFIG = {
hyperliquid: { exchange: 'hyperliquid', defaultSymbol: 'BTC', icon: '🔷', label: { zh: 'HL', en: 'HL' }, color: 'cyan', hasDropdown: true },
crypto: { exchange: 'binance', defaultSymbol: 'BTCUSDT', icon: '₿', label: { zh: '加密', en: 'Crypto' }, color: 'yellow', hasDropdown: false },
stocks: { exchange: 'alpaca', defaultSymbol: 'AAPL', icon: '📈', label: { zh: '美股', en: 'Stocks' }, color: 'green', hasDropdown: false },
forex: { exchange: 'forex', defaultSymbol: 'EUR/USD', icon: '💱', label: { zh: '外汇', en: 'Forex' }, color: 'blue', hasDropdown: false },
metals: { exchange: 'metals', defaultSymbol: 'XAU/USD', icon: '🥇', label: { zh: '金属', en: 'Metals' }, color: 'amber', hasDropdown: false },
hyperliquid: { exchange: 'hyperliquid', defaultSymbol: 'BTC', icon: '🔷', labelKey: 'hyperliquid' as const, color: 'cyan', hasDropdown: true },
crypto: { exchange: 'binance', defaultSymbol: 'BTCUSDT', icon: '₿', labelKey: 'crypto' as const, color: 'yellow', hasDropdown: false },
stocks: { exchange: 'alpaca', defaultSymbol: 'AAPL', icon: '📈', labelKey: 'stocks' as const, color: 'green', hasDropdown: false },
forex: { exchange: 'forex', defaultSymbol: 'EUR/USD', icon: '💱', labelKey: 'forex' as const, color: 'blue', hasDropdown: false },
metals: { exchange: 'metals', defaultSymbol: 'XAU/USD', icon: '🥇', labelKey: 'metals' as const, color: 'amber', hasDropdown: false },
}
const INTERVALS: { value: Interval; label: string }[] = [
@@ -42,12 +43,12 @@ const INTERVALS: { value: Interval; label: string }[] = [
{ value: '1d', label: '1d' },
]
// 根据交易所ID推断市场类型
// Infer market type from exchange ID
function getMarketTypeFromExchange(exchangeId: string | undefined): MarketType {
if (!exchangeId) return 'hyperliquid'
const lower = exchangeId.toLowerCase()
if (lower.includes('hyperliquid')) return 'hyperliquid'
// 其他交易所默认使用 crypto 类型
// Other exchanges default to crypto type
return 'crypto'
}
@@ -63,25 +64,25 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
const [searchFilter, setSearchFilter] = useState('')
const dropdownRef = useRef<HTMLDivElement>(null)
// 当交易所ID变化时,自动切换市场类型
// Auto-switch market type when exchange ID changes
useEffect(() => {
const newMarketType = getMarketTypeFromExchange(exchangeId)
setMarketType(newMarketType)
}, [exchangeId])
// 根据市场类型确定交易所
// Determine exchange from market type
const marketConfig = MARKET_CONFIG[marketType]
// 优先使用传入的 exchangeId(非 hyperliquid 时)
// Prefer passed-in exchangeId (when not hyperliquid)
const currentExchange = marketType === 'hyperliquid' ? 'hyperliquid' : (exchangeId || marketConfig.exchange)
// 获取可用币种列表
// Fetch available symbol list
useEffect(() => {
if (marketConfig.hasDropdown) {
fetch(`/api/symbols?exchange=${marketConfig.exchange}`)
.then(res => res.json())
.then(data => {
if (data.symbols) {
// 按类别排序: crypto > stock > forex > commodity > index
// Sort by category: crypto > stock > forex > commodity > index
const categoryOrder: Record<string, number> = { crypto: 0, stock: 1, forex: 2, commodity: 3, index: 4 }
const sorted = [...data.symbols].sort((a: SymbolInfo, b: SymbolInfo) => {
const orderA = categoryOrder[a.category] ?? 5
@@ -96,7 +97,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
}
}, [marketType, marketConfig.exchange, marketConfig.hasDropdown])
// 点击外部关闭下拉
// Close dropdown on outside click
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
@@ -107,33 +108,33 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
// 切换市场类型时更新默认符号
// Update default symbol when switching market type
const handleMarketTypeChange = (type: MarketType) => {
setMarketType(type)
setChartSymbol(MARKET_CONFIG[type].defaultSymbol)
setShowDropdown(false)
}
// 过滤后的币种列表
// Filtered symbol list
const filteredSymbols = availableSymbols.filter(s =>
s.symbol.toLowerCase().includes(searchFilter.toLowerCase())
)
// 当从外部选择币种时,自动切换到K线图
// Auto-switch to kline chart when symbol selected externally
useEffect(() => {
if (selectedSymbol) {
console.log('[ChartTabs] 收到币种选择:', selectedSymbol, 'updateKey:', updateKey)
console.log('[ChartTabs] Symbol selected:', selectedSymbol, 'updateKey:', updateKey)
setChartSymbol(selectedSymbol)
setActiveTab('kline')
}
}, [selectedSymbol, updateKey])
// 处理手动输入符号
// Handle manual symbol input
const handleSymbolSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (symbolInput.trim()) {
let symbol = symbolInput.trim().toUpperCase()
// 加密货币自动加 USDT 后缀
// Auto-append USDT suffix for crypto
if (marketType === 'crypto' && !symbol.endsWith('USDT')) {
symbol = symbol + 'USDT'
}
@@ -198,7 +199,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
}`}
>
<span className="mr-1 opacity-70">{config.icon}</span>
{language === 'zh' ? config.label.zh : config.label.en}
{ts(chartTabs[config.labelKey], language)}
</button>
)
})}
+49 -48
View File
@@ -10,8 +10,9 @@ import {
} from 'lightweight-charts'
import { useLanguage } from '../../contexts/LanguageContext'
import { httpClient } from '../../lib/httpClient'
import { t } from '../../i18n/translations'
// 订单接口定义
// Order marker interface
interface OrderMarker {
time: number // Unix timestamp (seconds)
price: number
@@ -21,7 +22,7 @@ interface OrderMarker {
symbol: string
}
// K线数据接口
// Kline data interface
interface KlineData {
time: UTCTimestamp
open: number
@@ -34,9 +35,9 @@ interface KlineData {
interface ChartWithOrdersProps {
symbol: string
interval?: string // 1m, 5m, 15m, 1h, 4h, 1d
traderID?: string // 用于获取该trader的订单
traderID?: string // Used to fetch orders for this trader
height?: number
exchange?: string // 交易所类型:binance, bybit, okx, bitget, hyperliquid, aster, lighter
exchange?: string // Exchange type: binance, bybit, okx, bitget, hyperliquid, aster, lighter
}
export function ChartWithOrders({
@@ -44,7 +45,7 @@ export function ChartWithOrders({
interval = '5m',
traderID,
height = 500,
exchange = 'binance', // 默认使用 binance
exchange = 'binance', // Default to binance
}: ChartWithOrdersProps) {
const { language } = useLanguage()
const chartContainerRef = useRef<HTMLDivElement>(null)
@@ -56,16 +57,16 @@ export function ChartWithOrders({
const [tooltipData, setTooltipData] = useState<any>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
// 解析时间:支持 Unix 时间戳(数字)或字符串格式
// Parse time: supports Unix timestamp (number) or string format
const parseCustomTime = (time: any): number => {
if (!time) {
console.warn('[ChartWithOrders] Empty time value')
return 0
}
// 如果已经是数字(Unix 时间戳)
// If already a number (Unix timestamp)
if (typeof time === 'number') {
// 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳)
// Determine ms vs seconds: if > 10^12, treat as milliseconds
if (time > 1000000000000) {
const seconds = Math.floor(time / 1000)
console.log('[ChartWithOrders] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
@@ -78,7 +79,7 @@ export function ChartWithOrders({
const timeStr = String(time)
console.log('[ChartWithOrders] Parsing time string:', timeStr)
// 尝试标准ISO格式
// Try standard ISO format
const isoTime = new Date(timeStr).getTime()
if (!isNaN(isoTime) && isoTime > 0) {
const timestamp = Math.floor(isoTime / 1000)
@@ -86,7 +87,7 @@ export function ChartWithOrders({
return timestamp
}
// 解析自定义格式 "MM-DD HH:mm UTC" (兼容旧数据)
// Parse custom format "MM-DD HH:mm UTC" (for legacy data)
const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
if (match) {
const currentYear = new Date().getFullYear()
@@ -107,10 +108,10 @@ export function ChartWithOrders({
return 0
}
// 从我们的服务获取K线数据
// Fetch kline data from our service
const fetchKlineData = async (symbol: string, interval: string): Promise<KlineData[]> => {
try {
const limit = 2000 // 获取最近2000根K线 (更多历史数据)
const limit = 2000 // Fetch recent 2000 candles (more historical data)
const klineUrl = `/api/klines?symbol=${symbol}&interval=${interval}&limit=${limit}&exchange=${exchange}`
const result = await httpClient.get(klineUrl)
@@ -121,10 +122,10 @@ export function ChartWithOrders({
const data = result.data
// 转换后端数据格式到 lightweight-charts 格式
// 后端返回的是 market.Kline 格式: {OpenTime, Open, High, Low, Close, Volume, ...}
// Convert backend data format to lightweight-charts format
// Backend returns market.Kline format: {OpenTime, Open, High, Low, Close, Volume, ...}
return data.map((candle: any) => ({
time: Math.floor(candle.openTime / 1000) as UTCTimestamp, // 毫秒转秒
time: Math.floor(candle.openTime / 1000) as UTCTimestamp, // ms to seconds
open: candle.open,
high: candle.high,
low: candle.low,
@@ -137,10 +138,10 @@ export function ChartWithOrders({
}
}
// 获取订单数据
// Fetch order data
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
try {
// 从后端 API 获取该 trader 的订单记录(只获取已成交的订单)
// Fetch filled orders for this trader from backend API
const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=50`)
if (!result.success || !result.data) {
@@ -151,7 +152,7 @@ export function ChartWithOrders({
const orders = result.data
const markers: OrderMarker[] = []
// 转换订单数据为标记格式
// Convert order data to marker format
orders.forEach((order: any) => {
const createdAt = order.created_at || order.CreatedAt
const filledAt = order.filled_at || order.FilledAt
@@ -162,14 +163,14 @@ export function ChartWithOrders({
const status = order.status || order.Status
const symbol = order.symbol || order.Symbol
// 使用成交时间(如果有)或创建时间
// Use fill time (if available) or creation time
const orderTime = filledAt || createdAt
if (!orderTime) return
const timeSeconds = parseCustomTime(orderTime)
if (timeSeconds === 0) return
// 使用平均成交价(如果有)或订单价格
// Use average fill price (if available) or order price
const orderPrice = avgPrice || price
if (!orderPrice || orderPrice === 0) return
@@ -191,7 +192,7 @@ export function ChartWithOrders({
}
}
// 初始化图表
// Initialize chart
useEffect(() => {
if (!chartContainerRef.current) {
console.error('[ChartWithOrders] Container ref is null')
@@ -201,7 +202,7 @@ export function ChartWithOrders({
console.log('[ChartWithOrders] Initializing chart for', symbol, interval)
try {
// 创建图表
// Create chart
const chart = createChart(chartContainerRef.current, {
width: chartContainerRef.current.clientWidth,
height: height,
@@ -240,7 +241,7 @@ export function ChartWithOrders({
chartRef.current = chart
// 创建K线系列 (使用 v5 API)
// Create candlestick series (using v5 API)
const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#0ECB81',
downColor: '#F6465D',
@@ -252,7 +253,7 @@ export function ChartWithOrders({
candlestickSeriesRef.current = candlestickSeries as any
// 响应式调整
// Responsive resize
const handleResize = () => {
if (chartContainerRef.current && chartRef.current) {
chartRef.current.applyOptions({
@@ -263,7 +264,7 @@ export function ChartWithOrders({
window.addEventListener('resize', handleResize)
// 监听鼠标移动,显示 OHLC 信息
// Listen for crosshair movement to show OHLC info
chart.subscribeCrosshairMove((param) => {
if (!param.time || !param.point || !candlestickSeriesRef.current) {
setTooltipData(null)
@@ -298,7 +299,7 @@ export function ChartWithOrders({
}
}, [height])
// 加载数据
// Load data
useEffect(() => {
const loadData = async () => {
if (!candlestickSeriesRef.current) {
@@ -311,22 +312,22 @@ export function ChartWithOrders({
setError(null)
try {
// 1. 获取K线数据
// 1. Fetch kline data
console.log('[ChartWithOrders] Fetching kline data...')
const klineData = await fetchKlineData(symbol, interval)
console.log('[ChartWithOrders] Kline data received:', klineData.length, 'candles')
candlestickSeriesRef.current.setData(klineData)
// 构建 K 线时间集合,用于快速查找
// Build kline time set for quick lookup
const klineTimeSet = new Set(klineData.map(k => k.time as number))
const klineMinTime = klineData.length > 0 ? klineData[0].time : 0
const klineMaxTime = klineData.length > 0 ? klineData[klineData.length - 1].time : 0
console.log('[ChartWithOrders] Kline time range:', klineMinTime, '-', klineMaxTime, 'candles:', klineData.length)
// 计算时间周期的秒数
// Calculate interval in seconds
const getIntervalSeconds = (interval: string): number => {
const match = interval.match(/(\d+)([smhd])/)
if (!match) return 60 // 默认1分钟
if (!match) return 60 // Default 1 minute
const [, num, unit] = match
const n = parseInt(num)
switch (unit) {
@@ -340,7 +341,7 @@ export function ChartWithOrders({
const intervalSeconds = getIntervalSeconds(interval)
console.log('[ChartWithOrders] Interval:', interval, '=', intervalSeconds, 'seconds')
// 2. 获取订单数据并添加标记
// 2. Fetch order data and add markers
if (traderID) {
console.log('[ChartWithOrders] Fetching orders for trader:', traderID, 'symbol:', symbol)
const orders = await fetchOrders(traderID, symbol)
@@ -350,7 +351,7 @@ export function ChartWithOrders({
console.log('[ChartWithOrders] No orders to display')
}
// 转换订单为图表标记,并对齐到 K 线时间
// Convert orders to chart markers, aligned to kline time
const markers: Array<{
time: Time
position: 'belowBar'
@@ -362,10 +363,10 @@ export function ChartWithOrders({
}> = []
orders.forEach((order) => {
// 将订单时间对齐到 K 线周期(向下取整)
// Align order time to kline interval (floor)
const alignedTime = Math.floor(order.time / intervalSeconds) * intervalSeconds
// 检查对齐后的时间是否在 K 线数据中存在
// Check if aligned time exists in kline data
if (!klineTimeSet.has(alignedTime)) {
console.warn('[ChartWithOrders] ⚠️ Skipping order - no matching kline:',
order.time, '→', alignedTime, '(', new Date(order.time * 1000).toISOString(), ')')
@@ -389,12 +390,12 @@ export function ChartWithOrders({
console.log('[ChartWithOrders] Setting', markers.length, 'markers on chart')
try {
// 使用 v5 API: createSeriesMarkers
// Using v5 API: createSeriesMarkers
if (seriesMarkersRef.current) {
// 如果已经存在,更新标记
// If already exists, update markers
seriesMarkersRef.current.setMarkers(markers)
} else {
// 首次创建标记
// First time creating markers
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markers)
}
console.log('[ChartWithOrders] ✅ Markers set successfully!')
@@ -403,23 +404,23 @@ export function ChartWithOrders({
}
}
// 自动适配视图
// Auto-fit view
chartRef.current?.timeScale().fitContent()
setLoading(false)
} catch (err) {
console.error('Error loading chart data:', err)
setError(language === 'zh' ? '加载图表数据失败' : 'Failed to load chart data')
setError(t('chartWithOrders.failedToLoad', language))
setLoading(false)
}
}
loadData()
// 自动刷新 - 每30秒更新一次K线数据
// Auto-refresh - update kline data every 30 seconds
const refreshInterval = setInterval(() => {
loadData()
}, 30000) // 30
}, 30000) // 30 seconds
return () => {
clearInterval(refreshInterval)
@@ -428,7 +429,7 @@ export function ChartWithOrders({
return (
<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 gap-3">
<span className="text-xl">📈</span>
@@ -438,12 +439,12 @@ export function ChartWithOrders({
</div>
{loading && (
<div className="text-sm" style={{ color: '#848E9C' }}>
{language === 'zh' ? '加载中...' : 'Loading...'}
{t('chartWithOrders.loading', language)}
</div>
)}
</div>
{/* 图表容器 */}
{/* Chart container */}
<div style={{ position: 'relative' }}>
<div ref={chartContainerRef} />
@@ -498,7 +499,7 @@ export function ChartWithOrders({
)}
</div>
{/* 错误提示 */}
{/* Error display */}
{error && (
<div
className="absolute inset-0 flex items-center justify-center"
@@ -511,15 +512,15 @@ export function ChartWithOrders({
</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-2">
<span className="font-bold" style={{ color: '#0ECB81' }}>B</span>
<span>{language === 'zh' ? 'BUY (买入)' : 'BUY'}</span>
<span>{t('chartWithOrders.buy', language)}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-bold" style={{ color: '#F6465D' }}>S</span>
<span>{language === 'zh' ? 'SELL (卖出)' : 'SELL'}</span>
<span>{t('chartWithOrders.sell', language)}</span>
</div>
</div>
</div>
@@ -21,12 +21,12 @@ import { BarChart3, TrendingUp, TrendingDown, Zap } from 'lucide-react'
// Time period options: 1D, 3D, 7D, 30D, All
const TIME_PERIODS = [
{ key: '1d', hours: 24, label: { en: '1D', zh: '1天' } },
{ key: '3d', hours: 72, label: { en: '3D', zh: '3天' } },
{ key: '7d', hours: 168, label: { en: '7D', zh: '7天' } },
{ key: '30d', hours: 720, label: { en: '30D', zh: '30天' } },
{ key: 'all', hours: 0, label: { en: 'All', zh: '全部' } },
]
{ key: '1d', hours: 24 },
{ key: '3d', hours: 72 },
{ key: '7d', hours: 168 },
{ key: '30d', hours: 720 },
{ key: 'all', hours: 0 },
] as const
interface ComparisonChartProps {
traders: CompetitionTraderData[]
@@ -352,7 +352,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
}}
>
{language === 'zh' ? period.label.zh : period.label.en}
{t(`comparisonChart.${period.key}`, language)}
</button>
))}
</div>
+5 -5
View File
@@ -58,8 +58,8 @@ export function ConfirmDialogProvider({
const [state, setState] = useState<ConfirmState>({
isOpen: false,
message: '',
okText: '确认',
cancelText: '取消',
okText: 'Confirm',
cancelText: 'Cancel',
})
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
@@ -68,14 +68,14 @@ export function ConfirmDialogProvider({
isOpen: true,
title: options.title,
message: options.message,
okText: options.okText || '确认',
cancelText: options.cancelText || '取消',
okText: options.okText || 'Confirm',
cancelText: options.cancelText || 'Cancel',
resolve,
})
})
}, [])
// 注册全局 confirm 函数
// Register global confirm function
useEffect(() => {
setGlobalConfirm(confirm)
}, [confirm])
+3 -1
View File
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'
import { HelpCircle } from 'lucide-react'
import katex from 'katex'
import 'katex/dist/katex.min.css'
import { t } from '../../i18n/translations'
export interface MetricDefinition {
key: string
@@ -241,6 +242,7 @@ export function MetricTooltip({
const name = language === 'zh' ? metric.nameZh : metric.nameEn
const description = language === 'zh' ? metric.descriptionZh : metric.descriptionEn
const formulaLabel = t('metricTooltip.formula', language as 'en' | 'zh' | 'id')
const tooltipContent = (
<div
@@ -292,7 +294,7 @@ export function MetricTooltip({
marginBottom: '12px'
}}>
<div style={{ fontSize: '12px', color: '#848E9C', marginBottom: '8px' }}>
{language === 'zh' ? '计算公式' : 'Formula'}
{formulaLabel}
</div>
<div style={{
display: 'flex',
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap, Shuffle } from 'lucide-react'
import type { CoinSourceConfig } from '../../types'
import { coinSource, ts } from '../../i18n/strategy-translations'
interface CoinSourceEditorProps {
config: CoinSourceConfig
@@ -18,52 +19,6 @@ export function CoinSourceEditor({
const [newCoin, setNewCoin] = 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 = [
{ value: 'static', icon: List, color: '#848E9C' },
{ value: 'ai500', icon: Database, color: '#F0B90B' },
@@ -82,15 +37,15 @@ export function CoinSourceEditor({
totalLimit += config.ai500_limit || 10
}
if (config.use_oi_top) {
sources.push(`${language === 'zh' ? 'OI增' : 'OI↑'}(${config.oi_top_limit || 10})`)
sources.push(`${ts(coinSource.oiIncreaseShort, language)}(${config.oi_top_limit || 10})`)
totalLimit += config.oi_top_limit || 10
}
if (config.use_oi_low) {
sources.push(`${language === 'zh' ? 'OI减' : 'OI↓'}(${config.oi_low_limit || 10})`)
sources.push(`${ts(coinSource.oiDecreaseShort, language)}(${config.oi_low_limit || 10})`)
totalLimit += config.oi_low_limit || 10
}
if ((config.static_coins || []).length > 0) {
sources.push(`${language === 'zh' ? '自定义' : 'Custom'}(${config.static_coins?.length || 0})`)
sources.push(`${ts(coinSource.custom, language)}(${config.static_coins?.length || 0})`)
totalLimit += config.static_coins?.length || 0
}
@@ -191,7 +146,7 @@ export function CoinSourceEditor({
{/* Source Type Selector */}
<div>
<label className="block text-sm font-medium mb-3 text-nofx-text">
{t('sourceType')}
{ts(coinSource.sourceType, language)}
</label>
<div className="grid grid-cols-5 gap-2">
{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 }} />
<div className="text-sm font-medium text-nofx-text">
{t(value)}
{ts(coinSource[value as keyof typeof coinSource], language)}
</div>
<div className="text-xs mt-1 text-nofx-text-muted">
{t(`${value}Desc`)}
{ts(coinSource[`${value}Desc` as keyof typeof coinSource], language)}
</div>
</button>
))}
@@ -223,7 +178,7 @@ export function CoinSourceEditor({
{config.source_type === 'static' && (
<div>
<label className="block text-sm font-medium mb-3 text-nofx-text">
{t('staticCoins')}
{ts(coinSource.staticCoins, language)}
</label>
<div className="flex flex-wrap gap-2 mb-3">
{(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"
>
<Plus className="w-4 h-4" />
{t('addCoin')}
{ts(coinSource.addCoin, language)}
</button>
</div>
)}
@@ -270,11 +225,11 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2 mb-3">
<Ban className="w-4 h-4 text-nofx-danger" />
<label className="text-sm font-medium text-nofx-text">
{t('excludedCoins')}
{ts(coinSource.excludedCoins, language)}
</label>
</div>
<p className="text-xs mb-3 text-nofx-text-muted">
{t('excludedCoinsDesc')}
{ts(coinSource.excludedCoinsDesc, language)}
</p>
<div className="flex flex-wrap gap-2 mb-3">
{(config.excluded_coins || []).map((coin) => (
@@ -295,7 +250,7 @@ export function CoinSourceEditor({
))}
{(config.excluded_coins || []).length === 0 && (
<span className="text-xs italic text-nofx-text-muted">
{language === 'zh' ? '无' : 'None'}
{ts(coinSource.excludedNone, language)}
</span>
)}
</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"
>
<Ban className="w-4 h-4" />
{t('addExcludedCoin')}
{ts(coinSource.addExcludedCoin, language)}
</button>
</div>
)}
@@ -329,7 +284,7 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-nofx-gold" />
<span className="text-sm font-medium text-nofx-text">
AI500 {t('dataSourceConfig')}
AI500 {ts(coinSource.dataSourceConfig, language)}
</span>
<NofxOSBadge />
</div>
@@ -346,13 +301,13 @@ export function CoinSourceEditor({
disabled={disabled}
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>
{config.use_ai500 && (
<div className="flex items-center gap-3 pl-8">
<span className="text-sm text-nofx-text-muted">
{t('ai500Limit')}:
{ts(coinSource.ai500Limit, language)}:
</span>
<select
value={config.ai500_limit || 10}
@@ -371,7 +326,7 @@ export function CoinSourceEditor({
)}
<p className="text-xs pl-8 text-nofx-text-muted">
{t('nofxosNote')}
{ts(coinSource.nofxosNote, language)}
</p>
</div>
</div>
@@ -386,7 +341,7 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-nofx-success" />
<span className="text-sm font-medium text-nofx-text">
OI {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig')}
{ts(coinSource.oiIncreaseTitle, language)} {ts(coinSource.dataSourceConfig, language)}
</span>
<NofxOSBadge />
</div>
@@ -403,13 +358,13 @@ export function CoinSourceEditor({
disabled={disabled}
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>
{config.use_oi_top && (
<div className="flex items-center gap-3 pl-8">
<span className="text-sm text-nofx-text-muted">
{t('oiTopLimit')}:
{ts(coinSource.oiTopLimit, language)}:
</span>
<select
value={config.oi_top_limit || 10}
@@ -428,7 +383,7 @@ export function CoinSourceEditor({
)}
<p className="text-xs pl-8 text-nofx-text-muted">
{t('nofxosNote')}
{ts(coinSource.nofxosNote, language)}
</p>
</div>
</div>
@@ -443,7 +398,7 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-nofx-danger" />
<span className="text-sm font-medium text-nofx-text">
OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig')}
{ts(coinSource.oiDecreaseTitle, language)} {ts(coinSource.dataSourceConfig, language)}
</span>
<NofxOSBadge />
</div>
@@ -460,13 +415,13 @@ export function CoinSourceEditor({
disabled={disabled}
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>
{config.use_oi_low && (
<div className="flex items-center gap-3 pl-8">
<span className="text-sm text-nofx-text-muted">
{t('oiLowLimit')}:
{ts(coinSource.oiLowLimit, language)}:
</span>
<select
value={config.oi_low_limit || 10}
@@ -485,7 +440,7 @@ export function CoinSourceEditor({
)}
<p className="text-xs pl-8 text-nofx-text-muted">
{t('nofxosNote')}
{ts(coinSource.nofxosNote, language)}
</p>
</div>
</div>
@@ -497,7 +452,7 @@ export function CoinSourceEditor({
<div className="flex items-center gap-2 mb-4">
<Shuffle className="w-4 h-4 text-blue-400" />
<span className="text-sm font-medium text-nofx-text">
{t('mixedConfig')}
{ts(coinSource.mixedConfig, language)}
</span>
</div>
@@ -566,11 +521,11 @@ export function CoinSourceEditor({
/>
<TrendingUp className="w-4 h-4 text-nofx-success" />
<span className="text-sm font-medium text-nofx-text">
{language === 'zh' ? 'OI 增加' : 'OI Increase'}
{ts(coinSource.oiIncreaseLabel, language)}
</span>
</div>
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
{language === 'zh' ? '适合做多' : 'For long'}
{ts(coinSource.forLong, language)}
</p>
{config.use_oi_top && (
<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" />
<span className="text-sm font-medium text-nofx-text">
{language === 'zh' ? 'OI 减少' : 'OI Decrease'}
{ts(coinSource.oiDecreaseLabel, language)}
</span>
</div>
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
{language === 'zh' ? '适合做空' : 'For short'}
{ts(coinSource.forShort, language)}
</p>
{config.use_oi_low && (
<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">
<List className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium text-nofx-text">
{language === 'zh' ? '自定义' : 'Custom'}
{ts(coinSource.custom, language)}
</span>
{(config.static_coins || []).length > 0 && (
<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 (
<div className="p-2 rounded bg-nofx-bg border border-nofx-border">
<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">
{sources.join(' + ')}
</span>
</div>
<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>
)
+52 -116
View File
@@ -1,5 +1,6 @@
import { Grid, DollarSign, TrendingUp, Shield, Compass } from 'lucide-react'
import type { GridStrategyConfig } from '../../types'
import { gridConfig, ts } from '../../i18n/strategy-translations'
interface GridConfigEditorProps {
config: GridStrategyConfig
@@ -8,7 +9,7 @@ interface GridConfigEditorProps {
language: string
}
// Default grid config
// Default grid configuration
export const defaultGridConfig: GridStrategyConfig = {
symbol: 'BTCUSDT',
grid_count: 10,
@@ -33,71 +34,6 @@ export function GridConfigEditor({
disabled,
language,
}: 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>(
key: K,
value: GridStrategyConfig[K]
@@ -125,7 +61,7 @@ export function GridConfigEditor({
<div className="flex items-center gap-2 mb-4">
<DollarSign className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('tradingPair')}
{ts(gridConfig.tradingPair, language)}
</h3>
</div>
@@ -133,10 +69,10 @@ export function GridConfigEditor({
{/* Symbol */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('symbol')}
{ts(gridConfig.symbol, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('symbolDesc')}
{ts(gridConfig.symbolDesc, language)}
</p>
<select
value={config.symbol}
@@ -157,10 +93,10 @@ export function GridConfigEditor({
{/* Investment */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('totalInvestment')}
{ts(gridConfig.totalInvestment, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('totalInvestmentDesc')}
{ts(gridConfig.totalInvestmentDesc, language)}
</p>
<input
type="number"
@@ -177,10 +113,10 @@ export function GridConfigEditor({
{/* Leverage */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('leverage')}
{ts(gridConfig.leverage, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('leverageDesc')}
{ts(gridConfig.leverageDesc, language)}
</p>
<input
type="number"
@@ -201,7 +137,7 @@ export function GridConfigEditor({
<div className="flex items-center gap-2 mb-4">
<Grid className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('gridParameters')}
{ts(gridConfig.gridParameters, language)}
</h3>
</div>
@@ -209,10 +145,10 @@ export function GridConfigEditor({
{/* Grid Count */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('gridCount')}
{ts(gridConfig.gridCount, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('gridCountDesc')}
{ts(gridConfig.gridCountDesc, language)}
</p>
<input
type="number"
@@ -229,10 +165,10 @@ export function GridConfigEditor({
{/* Distribution */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('distribution')}
{ts(gridConfig.distribution, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('distributionDesc')}
{ts(gridConfig.distributionDesc, language)}
</p>
<select
value={config.distribution}
@@ -241,9 +177,9 @@ export function GridConfigEditor({
className="w-full px-3 py-2 rounded"
style={inputStyle}
>
<option value="uniform">{t('uniform')}</option>
<option value="gaussian">{t('gaussian')}</option>
<option value="pyramid">{t('pyramid')}</option>
<option value="uniform">{ts(gridConfig.uniform, language)}</option>
<option value="gaussian">{ts(gridConfig.gaussian, language)}</option>
<option value="pyramid">{ts(gridConfig.pyramid, language)}</option>
</select>
</div>
</div>
@@ -254,7 +190,7 @@ export function GridConfigEditor({
<div className="flex items-center gap-2 mb-4">
<TrendingUp className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('priceBounds')}
{ts(gridConfig.priceBounds, language)}
</h3>
</div>
@@ -263,10 +199,10 @@ export function GridConfigEditor({
<div className="flex items-center justify-between">
<div>
<label className="block text-sm" style={{ color: '#EAECEF' }}>
{t('useAtrBounds')}
{ts(gridConfig.useAtrBounds, language)}
</label>
<p className="text-xs" style={{ color: '#848E9C' }}>
{t('useAtrBoundsDesc')}
{ts(gridConfig.useAtrBoundsDesc, language)}
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
@@ -285,10 +221,10 @@ export function GridConfigEditor({
{config.use_atr_bounds ? (
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('atrMultiplier')}
{ts(gridConfig.atrMultiplier, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('atrMultiplierDesc')}
{ts(gridConfig.atrMultiplierDesc, language)}
</p>
<input
type="number"
@@ -306,10 +242,10 @@ export function GridConfigEditor({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('upperPrice')}
{ts(gridConfig.upperPrice, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('upperPriceDesc')}
{ts(gridConfig.upperPriceDesc, language)}
</p>
<input
type="number"
@@ -324,10 +260,10 @@ export function GridConfigEditor({
</div>
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('lowerPrice')}
{ts(gridConfig.lowerPrice, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('lowerPriceDesc')}
{ts(gridConfig.lowerPriceDesc, language)}
</p>
<input
type="number"
@@ -349,17 +285,17 @@ export function GridConfigEditor({
<div className="flex items-center gap-2 mb-4">
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('riskControl')}
{ts(gridConfig.riskControl, language)}
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxDrawdown')}
{ts(gridConfig.maxDrawdown, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxDrawdownDesc')}
{ts(gridConfig.maxDrawdownDesc, language)}
</p>
<input
type="number"
@@ -375,10 +311,10 @@ export function GridConfigEditor({
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('stopLoss')}
{ts(gridConfig.stopLoss, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('stopLossDesc')}
{ts(gridConfig.stopLossDesc, language)}
</p>
<input
type="number"
@@ -394,10 +330,10 @@ export function GridConfigEditor({
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('dailyLossLimit')}
{ts(gridConfig.dailyLossLimit, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('dailyLossLimitDesc')}
{ts(gridConfig.dailyLossLimitDesc, language)}
</p>
<input
type="number"
@@ -417,10 +353,10 @@ export function GridConfigEditor({
<div className="flex items-center justify-between">
<div>
<label className="block text-sm" style={{ color: '#EAECEF' }}>
{t('useMakerOnly')}
{ts(gridConfig.useMakerOnly, language)}
</label>
<p className="text-xs" style={{ color: '#848E9C' }}>
{t('useMakerOnlyDesc')}
{ts(gridConfig.useMakerOnlyDesc, language)}
</p>
</div>
<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">
<Compass className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('directionAdjust')}
{ts(gridConfig.directionAdjust, language)}
</h3>
</div>
@@ -451,10 +387,10 @@ export function GridConfigEditor({
<div className="flex items-center justify-between">
<div>
<label className="block text-sm" style={{ color: '#EAECEF' }}>
{t('enableDirectionAdjust')}
{ts(gridConfig.enableDirectionAdjust, language)}
</label>
<p className="text-xs" style={{ color: '#848E9C' }}>
{t('enableDirectionAdjustDesc')}
{ts(gridConfig.enableDirectionAdjustDesc, language)}
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
@@ -475,30 +411,30 @@ export function GridConfigEditor({
{/* Direction Modes Explanation */}
<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' }}>
📊 {t('directionModes')}
📊 {ts(gridConfig.directionModes, language)}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs" style={{ color: '#848E9C' }}>
<div> {t('modeNeutral')}</div>
<div> <span style={{ color: '#0ECB81' }}>{t('modeLongBias')}</span></div>
<div> <span style={{ color: '#0ECB81' }}>{t('modeLong')}</span></div>
<div> <span style={{ color: '#F6465D' }}>{t('modeShortBias')}</span></div>
<div> <span style={{ color: '#F6465D' }}>{t('modeShort')}</span></div>
<div> {ts(gridConfig.modeNeutral, language)}</div>
<div> <span style={{ color: '#0ECB81' }}>{ts(gridConfig.modeLongBias, language)}</span></div>
<div> <span style={{ color: '#0ECB81' }}>{ts(gridConfig.modeLong, language)}</span></div>
<div> <span style={{ color: '#F6465D' }}>{ts(gridConfig.modeShortBias, language)}</span></div>
<div> <span style={{ color: '#F6465D' }}>{ts(gridConfig.modeShort, language)}</span></div>
</div>
<p className="text-xs mt-3 pt-2 border-t border-zinc-700" style={{ color: '#848E9C' }}>
💡 {t('directionExplain')}
💡 {ts(gridConfig.directionExplain, language)}
</p>
</div>
{/* Bias Strength */}
<div className="p-4 rounded-lg" style={sectionStyle}>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('directionBiasRatio')} (X)
{ts(gridConfig.directionBiasRatio, language)} (X)
</label>
<p className="text-xs mb-1" style={{ color: '#848E9C' }}>
{t('directionBiasRatioDesc')}
{ts(gridConfig.directionBiasRatioDesc, language)}
</p>
<p className="text-xs mb-3" style={{ color: '#F0B90B' }}>
{t('directionBiasExplain')}
{ts(gridConfig.directionBiasExplain, language)}
</p>
<div className="flex items-center gap-3">
<input
@@ -518,12 +454,12 @@ export function GridConfigEditor({
</div>
<div className="mt-2 grid grid-cols-2 gap-2 text-xs">
<div className="p-2 rounded" style={{ background: '#0ECB8115', border: '1px solid #0ECB8130' }}>
<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: '#0ECB81' }}>Long Bias: </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 className="p-2 rounded" style={{ background: '#F6465D15', border: '1px solid #F6465D30' }}>
<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: '#F6465D' }}>Short Bias: </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>
+29 -88
View File
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { Shield, TrendingUp, AlertTriangle, Activity, Box, ChevronDown, ChevronUp } from 'lucide-react'
import type { GridRiskInfo } from '../../types'
import { gridRisk, ts } from '../../i18n/strategy-translations'
interface GridRiskPanelProps {
traderId: string
@@ -18,66 +19,6 @@ export function GridRiskPanel({
const [error, setError] = useState<string | null>(null)
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 () => {
try {
const token = localStorage.getItem('auth_token')
@@ -153,7 +94,7 @@ export function GridRiskPanel({
if (loading) {
return (
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
{t('loading')}
{ts(gridRisk.loading, language)}
</div>
)
}
@@ -161,7 +102,7 @@ export function GridRiskPanel({
if (error) {
return (
<div className="p-3 text-center text-xs" style={{ color: '#F6465D' }}>
{t('error')}: {error}
{ts(gridRisk.error, language)}: {error}
</div>
)
}
@@ -169,7 +110,7 @@ export function GridRiskPanel({
if (!riskInfo) {
return (
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
{t('noData')}
{ts(gridRisk.noData, language)}
</div>
)
}
@@ -184,7 +125,7 @@ export function GridRiskPanel({
<div className="flex items-center gap-2">
<Shield className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
{t('gridRisk')}
{ts(gridRisk.gridRisk, language)}
</span>
</div>
<div className="flex items-center gap-3">
@@ -194,7 +135,7 @@ export function GridRiskPanel({
className="px-2 py-0.5 rounded"
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 className="font-mono" style={{ color: '#EAECEF' }}>
{riskInfo.effective_leverage.toFixed(1)}x
@@ -223,19 +164,19 @@ export function GridRiskPanel({
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 mb-2">
<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 className="grid grid-cols-3 gap-1 text-xs">
<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>
<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>
<div>
<div style={{ color: '#5E6673' }}>{t('recommendedLeverage')}</div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.recommendedLeverage, language)}</div>
<div
className="font-mono"
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="flex items-center gap-1 mb-2">
<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 className="grid grid-cols-3 gap-1 text-xs">
<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>
<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>
<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) }}>
{riskInfo.position_percent.toFixed(1)}%
</div>
@@ -284,32 +225,32 @@ export function GridRiskPanel({
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 mb-2">
<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 className="grid grid-cols-2 gap-2 text-xs">
<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) }}>
{t(riskInfo.regime_level || 'standard')}
{ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
</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>
<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) }}>
{t(riskInfo.breakout_level || 'none')}
{ts(gridRisk[(riskInfo.breakout_level || 'none') as keyof typeof gridRisk], language)}
</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{t('breakoutDirection')}</div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutDirection, language)}</div>
<div
className="font-medium"
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>
@@ -319,17 +260,17 @@ export function GridRiskPanel({
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 mb-2">
<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 className="grid grid-cols-2 gap-2 text-xs">
<div>
<div style={{ color: '#5E6673' }}>{t('liquidationPrice')}</div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationPrice, language)}</div>
<div className="font-mono" style={{ color: '#F6465D' }}>
{riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'}
</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' }}>
{riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'}
</div>
@@ -342,23 +283,23 @@ export function GridRiskPanel({
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="flex items-center gap-1 mb-2">
<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 className="grid grid-cols-3 gap-2 text-xs">
<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' }}>
{formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)}
</span>
</div>
<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' }}>
{formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)}
</span>
</div>
<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' }}>
{formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)}
</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 type { IndicatorConfig } from '../../types'
import { indicator, ts } from '../../i18n/strategy-translations'
// Default NofxOS API Key
const DEFAULT_NOFXOS_API_KEY = 'cm_568c67eae410d912c54c'
@@ -11,7 +12,7 @@ interface IndicatorEditorProps {
language: string
}
// 所有可用时间周期
// All available timeframes
const allTimeframes = [
{ value: '1m', label: '1m', category: 'scalp' },
{ value: '3m', label: '3m', category: 'scalp' },
@@ -35,92 +36,10 @@ export function IndicatorEditor({
disabled,
language,
}: IndicatorEditorProps) {
const t = (key: string) => {
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
}
// 获取当前选中的时间周期
// Get currently selected timeframes
const selectedTimeframes = config.klines.selected_timeframes || [config.klines.primary_timeframe]
// 切换时间周期选择
// Toggle timeframe selection
const toggleTimeframe = (tf: string) => {
if (disabled) return
const current = [...selectedTimeframes]
@@ -153,7 +72,7 @@ export function IndicatorEditor({
}
}
// 设置主时间周期
// Set primary timeframe
const setPrimaryTimeframe = (tf: string) => {
if (disabled) return
onChange({
@@ -218,10 +137,10 @@ export function IndicatorEditor({
</div>
<div>
<h3 className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{t('nofxosTitle')}
{ts(indicator.nofxosTitle, language)}
</h3>
<span className="text-[10px]" style={{ color: '#848E9C' }}>
{t('nofxosFeatures')}
{ts(indicator.nofxosFeatures, language)}
</span>
</div>
</div>
@@ -231,12 +150,12 @@ export function IndicatorEditor({
{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' }}>
<Check className="w-3 h-3" />
{t('connected')}
{ts(indicator.connected, language)}
</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' }}>
<AlertCircle className="w-3 h-3" />
{t('notConfigured')}
{ts(indicator.notConfigured, language)}
</span>
)}
<a
@@ -250,7 +169,7 @@ export function IndicatorEditor({
}}
>
<ExternalLink className="w-3 h-3" />
{t('viewApiDocs')}
{ts(indicator.viewApiDocs, language)}
</a>
</div>
</div>
@@ -264,7 +183,7 @@ export function IndicatorEditor({
value={config.nofxos_api_key || ''}
onChange={(e) => !disabled && onChange({ ...config, nofxos_api_key: e.target.value })}
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"
style={{
background: 'rgba(30, 35, 41, 0.8)',
@@ -283,7 +202,7 @@ export function IndicatorEditor({
color: '#fff',
}}
>
{t('fillDefault')}
{ts(indicator.fillDefault, language)}
</button>
)}
</div>
@@ -291,7 +210,7 @@ export function IndicatorEditor({
{/* NofxOS Data Sources Grid */}
<div className="mt-4">
<div className="text-[10px] font-medium mb-2" style={{ color: '#848E9C' }}>
{t('nofxosDataSources')}
{ts(indicator.nofxosDataSources, language)}
</div>
<div className="grid grid-cols-2 gap-2">
{/* Quant Data */}
@@ -307,7 +226,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<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>
<input
type="checkbox"
@@ -317,7 +236,7 @@ export function IndicatorEditor({
className="w-3.5 h-3.5 rounded accent-blue-500"
/>
</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 && (
<div className="flex gap-3 mt-2">
<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 gap-2">
<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>
<input
type="checkbox"
@@ -377,7 +296,7 @@ export function IndicatorEditor({
className="w-3.5 h-3.5 rounded accent-green-500"
/>
</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 && (
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
<select
@@ -422,7 +341,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<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>
<input
type="checkbox"
@@ -437,7 +356,7 @@ export function IndicatorEditor({
className="w-3.5 h-3.5 rounded accent-amber-500"
/>
</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 && (
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
<select
@@ -482,7 +401,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<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>
<input
type="checkbox"
@@ -497,7 +416,7 @@ export function IndicatorEditor({
className="w-3.5 h-3.5 rounded accent-pink-500"
/>
</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 && (
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
<select
@@ -510,7 +429,7 @@ export function IndicatorEditor({
<option value="1h">1h</option>
<option value="4h">4h</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
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)' }}>
<AlertCircle className="w-4 h-4 flex-shrink-0" 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>
</div>
)}
@@ -545,8 +464,8 @@ export function IndicatorEditor({
<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' }}>
<BarChart2 className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('marketData')}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('marketDataDesc')}</span>
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.marketData, language)}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.marketDataDesc, language)}</span>
</div>
<div className="p-3 space-y-4">
@@ -558,13 +477,13 @@ export function IndicatorEditor({
</div>
<div>
<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' }}>
<Lock className="w-2.5 h-2.5" />
{t('required')}
{ts(indicator.required, language)}
</span>
</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>
<input
@@ -580,10 +499,10 @@ export function IndicatorEditor({
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<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 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
type="number"
value={config.klines.primary_count}
@@ -602,7 +521,7 @@ export function IndicatorEditor({
/>
</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 */}
<div className="space-y-1.5">
@@ -611,7 +530,7 @@ export function IndicatorEditor({
return (
<div key={category} className="flex items-center gap-2">
<span className="text-[10px] w-10 flex-shrink-0" style={{ color: categoryColors[category] }}>
{t(category)}
{ts(indicator[category], language)}
</span>
<div className="flex flex-wrap gap-1">
{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="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' }} />
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('technicalIndicators')}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('technicalIndicatorsDesc')}</span>
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.technicalIndicators, language)}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.technicalIndicatorsDesc, language)}</span>
</div>
<div className="p-3">
{/* Tip */}
<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' }} />
<p className="text-[10px]" style={{ color: '#848E9C' }}>{t('aiCanCalculate')}</p>
<p className="text-[10px]" style={{ color: '#848E9C' }}>{ts(indicator.aiCanCalculate, language)}</p>
</div>
{/* Indicator Grid */}
@@ -685,7 +604,7 @@ export function IndicatorEditor({
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<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>
<input
type="checkbox"
@@ -695,7 +614,7 @@ export function IndicatorEditor({
className="w-4 h-4 rounded accent-yellow-500"
/>
</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] && (
<input
type="text"
@@ -726,8 +645,8 @@ export function IndicatorEditor({
<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' }}>
<TrendingUp className="w-4 h-4" style={{ color: '#22c55e' }} />
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('marketSentiment')}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('marketSentimentDesc')}</span>
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.marketSentiment, language)}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.marketSentimentDesc, language)}</span>
</div>
<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 gap-2">
<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>
<input
type="checkbox"
@@ -758,7 +677,7 @@ export function IndicatorEditor({
className="w-4 h-4 rounded accent-yellow-500"
/>
</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>
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { ChevronDown, ChevronRight, RotateCcw, FileText } from 'lucide-react'
import type { PromptSectionsConfig } from '../../types'
import { promptSections as promptSectionsI18n, ts } from '../../i18n/strategy-translations'
interface PromptSectionsEditorProps {
config: PromptSectionsConfig | undefined
@@ -54,29 +55,11 @@ export function PromptSectionsEditor({
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 = [
{ key: 'role_definition', label: t('roleDefinition'), desc: t('roleDefinitionDesc') },
{ key: 'trading_frequency', label: t('tradingFrequency'), desc: t('tradingFrequencyDesc') },
{ key: 'entry_standards', label: t('entryStandards'), desc: t('entryStandardsDesc') },
{ key: 'decision_process', label: t('decisionProcess'), desc: t('decisionProcessDesc') },
{ key: 'role_definition', label: ts(promptSectionsI18n.roleDefinition, language), desc: ts(promptSectionsI18n.roleDefinitionDesc, language) },
{ key: 'trading_frequency', label: ts(promptSectionsI18n.tradingFrequency, language), desc: ts(promptSectionsI18n.tradingFrequencyDesc, language) },
{ key: 'entry_standards', label: ts(promptSectionsI18n.entryStandards, language), desc: ts(promptSectionsI18n.entryStandardsDesc, language) },
{ key: 'decision_process', label: ts(promptSectionsI18n.decisionProcess, language), desc: ts(promptSectionsI18n.decisionProcessDesc, language) },
]
const currentConfig = config || {}
@@ -107,10 +90,10 @@ export function PromptSectionsEditor({
<FileText className="w-5 h-5 mt-0.5" style={{ color: '#a855f7' }} />
<div>
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('promptSections')}
{ts(promptSectionsI18n.promptSections, language)}
</h3>
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('promptSectionsDesc')}
{ts(promptSectionsI18n.promptSectionsDesc, language)}
</p>
</div>
</div>
@@ -146,12 +129,12 @@ export function PromptSectionsEditor({
className="px-1.5 py-0.5 text-[10px] rounded"
style={{ background: 'rgba(168, 85, 247, 0.15)', color: '#a855f7' }}
>
{language === 'zh' ? '已修改' : 'Modified'}
{ts(promptSectionsI18n.modified, language)}
</span>
)}
</div>
<span className="text-[10px]" style={{ color: '#848E9C' }}>
{value.length} {t('chars')}
{value.length} {ts(promptSectionsI18n.chars, language)}
</span>
</button>
@@ -181,7 +164,7 @@ export function PromptSectionsEditor({
style={{ color: '#848E9C' }}
>
<RotateCcw className="w-3 h-3" />
{t('resetToDefault')}
{ts(promptSectionsI18n.resetToDefault, language)}
</button>
</div>
</div>
@@ -1,4 +1,5 @@
import { Globe, Lock, Eye, EyeOff } from 'lucide-react'
import { publishSettings, ts } from '../../i18n/strategy-translations'
interface PublishSettingsEditorProps {
isPublic: boolean
@@ -17,23 +18,9 @@ export function PublishSettingsEditor({
disabled = false,
language,
}: 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 (
<div className="space-y-3">
{/* 发布开关 */}
{/* Publish toggle */}
<div
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
style={{
@@ -73,10 +60,10 @@ export function PublishSettingsEditor({
</div>
<div>
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
{t('publishToMarket')}
{ts(publishSettings.publishToMarket, language)}
</div>
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
{t('publishDesc')}
{ts(publishSettings.publishDesc, language)}
</div>
</div>
</div>
@@ -87,7 +74,7 @@ export function PublishSettingsEditor({
className="text-[10px] font-mono font-bold tracking-wider"
style={{ color: isPublic ? '#0ECB81' : '#848E9C' }}
>
{isPublic ? t('public') : t('private')}
{isPublic ? ts(publishSettings.public, language) : ts(publishSettings.private, language)}
</span>
<div
className="relative w-12 h-6 rounded-full transition-all duration-300"
@@ -111,7 +98,7 @@ export function PublishSettingsEditor({
</div>
</div>
{/* 配置可见性开关 - 仅在公开时显示 */}
{/* Config visibility toggle - only shown when public */}
{isPublic && (
<div
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 className="text-sm font-medium" style={{ color: '#EAECEF' }}>
{t('showConfig')}
{ts(publishSettings.showConfig, language)}
</div>
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
{t('showConfigDesc')}
{ts(publishSettings.showConfigDesc, language)}
</div>
</div>
</div>
@@ -166,7 +153,7 @@ export function PublishSettingsEditor({
className="text-[10px] font-mono font-bold tracking-wider"
style={{ color: configVisible ? '#a855f7' : '#848E9C' }}
>
{configVisible ? t('visible') : t('hidden')}
{configVisible ? ts(publishSettings.visible, language) : ts(publishSettings.hidden, language)}
</span>
<div
className="relative w-12 h-6 rounded-full transition-all duration-300"
@@ -1,5 +1,6 @@
import { Shield, AlertTriangle } from 'lucide-react'
import type { RiskControlConfig } from '../../types'
import { riskControl, ts } from '../../i18n/strategy-translations'
interface RiskControlEditorProps {
config: RiskControlConfig
@@ -14,38 +15,6 @@ export function RiskControlEditor({
disabled,
language,
}: 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>(
key: K,
value: RiskControlConfig[K]
@@ -62,7 +31,7 @@ export function RiskControlEditor({
<div className="flex items-center gap-2 mb-4">
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('positionLimits')}
{ts(riskControl.positionLimits, language)}
</h3>
</div>
@@ -72,10 +41,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxPositions')}
{ts(riskControl.maxPositions, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxPositionsDesc')}
{ts(riskControl.maxPositionsDesc, language)}
</p>
<input
type="number"
@@ -99,7 +68,7 @@ export function RiskControlEditor({
{/* Trading Leverage (Exchange) */}
<div className="mb-2">
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
{t('tradingLeverage')}
{ts(riskControl.tradingLeverage, language)}
</p>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
@@ -108,10 +77,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('btcEthLeverage')}
{ts(riskControl.btcEthLeverage, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('btcEthLeverageDesc')}
{ts(riskControl.btcEthLeverageDesc, language)}
</p>
<div className="flex items-center gap-2">
<input
@@ -139,10 +108,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('altcoinLeverage')}
{ts(riskControl.altcoinLeverage, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('altcoinLeverageDesc')}
{ts(riskControl.altcoinLeverageDesc, language)}
</p>
<div className="flex items-center gap-2">
<input
@@ -169,10 +138,10 @@ export function RiskControlEditor({
{/* Position Value Ratio (Risk Control - CODE ENFORCED) */}
<div className="mb-2">
<p className="text-xs font-medium" style={{ color: '#0ECB81' }}>
{t('positionValueRatio')}
{ts(riskControl.positionValueRatio, language)}
</p>
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('positionValueRatioDesc')}
{ts(riskControl.positionValueRatioDesc, language)}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
@@ -181,10 +150,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('btcEthPositionValueRatio')}
{ts(riskControl.btcEthPositionValueRatio, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('btcEthPositionValueRatioDesc')}
{ts(riskControl.btcEthPositionValueRatioDesc, language)}
</p>
<div className="flex items-center gap-2">
<input
@@ -213,10 +182,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('altcoinPositionValueRatio')}
{ts(riskControl.altcoinPositionValueRatio, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('altcoinPositionValueRatioDesc')}
{ts(riskControl.altcoinPositionValueRatioDesc, language)}
</p>
<div className="flex items-center gap-2">
<input
@@ -247,7 +216,7 @@ export function RiskControlEditor({
<div className="flex items-center gap-2 mb-4">
<AlertTriangle className="w-5 h-5" style={{ color: '#F6465D' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('riskParameters')}
{ts(riskControl.riskParameters, language)}
</h3>
</div>
@@ -257,10 +226,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minRiskReward')}
{ts(riskControl.minRiskReward, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minRiskRewardDesc')}
{ts(riskControl.minRiskRewardDesc, language)}
</p>
<div className="flex items-center">
<span style={{ color: '#848E9C' }}>1:</span>
@@ -289,10 +258,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxMarginUsage')}
{ts(riskControl.maxMarginUsage, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxMarginUsageDesc')}
{ts(riskControl.maxMarginUsageDesc, language)}
</p>
<div className="flex items-center gap-2">
<input
@@ -319,7 +288,7 @@ export function RiskControlEditor({
<div className="flex items-center gap-2 mb-4">
<Shield className="w-5 h-5" style={{ color: '#0ECB81' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('entryRequirements')}
{ts(riskControl.entryRequirements, language)}
</h3>
</div>
@@ -329,10 +298,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minPositionSize')}
{ts(riskControl.minPositionSize, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minPositionSizeDesc')}
{ts(riskControl.minPositionSizeDesc, language)}
</p>
<div className="flex items-center">
<input
@@ -362,10 +331,10 @@ export function RiskControlEditor({
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minConfidence')}
{ts(riskControl.minConfidence, language)}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minConfidenceDesc')}
{ts(riskControl.minConfidenceDesc, language)}
</p>
<div className="flex items-center gap-2">
<input
+33 -33
View File
@@ -212,9 +212,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
await toast.promise(api.createTrader(data), {
loading: '正在创建…',
success: '创建成功',
error: '创建失败',
loading: t('aiTradersToast.creating', language),
success: t('aiTradersToast.created', language),
error: t('aiTradersToast.createFailed', language),
})
setShowCreateModal(false)
await mutateTraders()
@@ -269,9 +269,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
console.log('🔥 handleSaveEditTrader - request:', request)
await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
loading: '正在保存…',
success: '保存成功',
error: '保存失败',
loading: t('aiTradersToast.saving', language),
success: t('aiTradersToast.saved', language),
error: t('aiTradersToast.saveFailed', language),
})
setShowEditModal(false)
setEditingTrader(null)
@@ -290,9 +290,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
try {
await toast.promise(api.deleteTrader(traderId), {
loading: '正在删除…',
success: '删除成功',
error: '删除失败',
loading: t('aiTradersToast.deleting', language),
success: t('aiTradersToast.deleted', language),
error: t('aiTradersToast.deleteFailed', language),
})
await mutateTraders()
@@ -306,15 +306,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
try {
if (running) {
await toast.promise(api.stopTrader(traderId), {
loading: '正在停止…',
success: '已停止',
error: '停止失败',
loading: t('aiTradersToast.stopping', language),
success: t('aiTradersToast.stopped', language),
error: t('aiTradersToast.stopFailed', language),
})
} else {
await toast.promise(api.startTrader(traderId), {
loading: '正在启动…',
success: '已启动',
error: '启动失败',
loading: t('aiTradersToast.starting', language),
success: t('aiTradersToast.started', language),
error: t('aiTradersToast.startFailed', language),
})
}
@@ -329,9 +329,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
try {
const newValue = !currentShowInCompetition
await toast.promise(api.toggleCompetition(traderId, newValue), {
loading: '正在更新…',
success: newValue ? '已在竞技场显示' : '已在竞技场隐藏',
error: '更新失败',
loading: t('aiTradersToast.updating', language),
success: newValue ? t('aiTradersToast.showInCompetition', language) : t('aiTradersToast.hideInCompetition', language),
error: t('aiTradersToast.updateFailed', language),
})
await mutateTraders()
@@ -393,9 +393,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const request = config.buildRequest(updatedItems)
await toast.promise(config.updateApi(request), {
loading: '正在更新配置…',
success: '配置已更新',
error: '更新配置失败',
loading: t('aiTradersToast.updatingConfig', language),
success: t('aiTradersToast.configUpdated', language),
error: t('aiTradersToast.configUpdateFailed', language),
})
const refreshedItems = await config.refreshApi()
@@ -506,9 +506,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
await toast.promise(api.updateModelConfigs(request), {
loading: '正在更新模型配置…',
success: '模型配置已更新',
error: '更新模型配置失败',
loading: t('aiTradersToast.updatingModelConfig', language),
success: t('aiTradersToast.modelConfigUpdated', language),
error: t('aiTradersToast.modelConfigUpdateFailed', language),
})
const refreshedModels = await api.getModelConfigs()
@@ -536,9 +536,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
try {
await toast.promise(api.deleteExchange(exchangeId), {
loading: language === 'zh' ? '正在删除交易所账户…' : 'Deleting exchange account...',
success: language === 'zh' ? '交易所账户已删除' : 'Exchange account deleted',
error: language === 'zh' ? '删除交易所账户失败' : 'Failed to delete exchange account',
loading: t('aiTradersToast.deletingExchange', language),
success: t('aiTradersToast.exchangeDeleted', language),
error: t('aiTradersToast.exchangeDeleteFailed', language),
})
const refreshedExchanges = await api.getExchangeConfigs()
@@ -598,9 +598,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
await toast.promise(api.updateExchangeConfigsEncrypted(request), {
loading: language === 'zh' ? '正在更新交易所配置…' : 'Updating exchange config...',
success: language === 'zh' ? '交易所配置已更新' : 'Exchange config updated',
error: language === 'zh' ? '更新交易所配置失败' : 'Failed to update exchange config',
loading: t('aiTradersToast.updatingExchangeConfig', language),
success: t('aiTradersToast.exchangeConfigUpdated', language),
error: t('aiTradersToast.exchangeConfigUpdateFailed', language),
})
} else {
const createRequest = {
@@ -622,9 +622,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
await toast.promise(api.createExchangeEncrypted(createRequest), {
loading: language === 'zh' ? '正在创建交易所账户…' : 'Creating exchange account...',
success: language === 'zh' ? '交易所账户已创建' : 'Exchange account created',
error: language === 'zh' ? '创建交易所账户失败' : 'Failed to create exchange account',
loading: t('aiTradersToast.creatingExchange', language),
success: t('aiTradersToast.exchangeCreated', language),
error: t('aiTradersToast.exchangeCreateFailed', language),
})
}
@@ -258,7 +258,7 @@ export function ExchangeConfigModal({
toast.success(t('ipCopied', language))
}
} catch {
toast.error(t('copyIPFailed', language) || `复制失败: ${ip}`)
toast.error(t('copyIPFailed', language))
}
}
@@ -305,7 +305,7 @@ export function ExchangeConfigModal({
const trimmedAccountName = accountName.trim()
if (!trimmedAccountName) {
toast.error(language === 'zh' ? '请输入账户名称' : 'Please enter account name')
toast.error(t('exchangeConfig.pleaseEnterAccountName', language))
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 dexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'dex')
@@ -412,13 +412,13 @@ export function ExchangeConfigModal({
{/* Exchange Grid */}
<div className="space-y-4">
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '选择您的交易所' : 'Choose Your Exchange'}
{t('exchangeConfig.chooseExchange', language)}
</div>
{/* CEX */}
<div className="space-y-3">
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#F0B90B' }}>
{language === 'zh' ? '中心化交易所 (CEX)' : 'Centralized Exchanges'}
{t('exchangeConfig.centralizedExchanges', language)}
</div>
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
{cexExchanges.map((template) => (
@@ -436,7 +436,7 @@ export function ExchangeConfigModal({
{/* DEX */}
<div className="space-y-3">
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#A78BFA' }}>
{language === 'zh' ? '去中心化交易所 (DEX)' : 'Decentralized Exchanges'}
{t('exchangeConfig.decentralizedExchanges', language)}
</div>
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
{dexExchanges.map((template) => (
@@ -477,11 +477,11 @@ export function ExchangeConfigModal({
>
<UserPlus className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium" style={{ color: '#F0B90B' }}>
{language === 'zh' ? '注册' : 'Register'}
{t('exchangeConfig.register', language)}
</span>
{exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && (
<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>
)}
</a>
@@ -491,13 +491,13 @@ export function ExchangeConfigModal({
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<Key className="w-4 h-4" style={{ color: '#F0B90B' }} />
{language === 'zh' ? '账户名称' : 'Account Name'} *
{t('exchangeConfig.accountName', language)} *
</label>
<input
type="text"
value={accountName}
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"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
@@ -517,7 +517,7 @@ export function ExchangeConfigModal({
<div className="flex items-center gap-2">
<span style={{ color: '#58a6ff' }}></span>
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '币安用户必读:使用「现货与合约交易」API' : 'Use "Spot & Futures Trading" API'}
{t('exchangeConfig.useBinanceFuturesApi', language)}
</span>
</div>
<span style={{ color: '#8b949e' }}>{showBinanceGuide ? '▲' : '▼'}</span>
@@ -532,7 +532,7 @@ export function ExchangeConfigModal({
style={{ color: '#58a6ff' }}
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>
</div>
)}
@@ -696,10 +696,10 @@ export function ExchangeConfigModal({
<span style={{ fontSize: '16px' }}>🔐</span>
<div>
<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 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>
@@ -717,8 +717,8 @@ export function ExchangeConfigModal({
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
{language === 'zh' ? 'API Key 索引' : 'API Key Index'}
<Tooltip content={language === 'zh' ? 'API Key 索引从0开始' : 'API Key index starts from 0'}>
{t('exchangeConfig.apiKeyIndex', language)}
<Tooltip content={t('exchangeConfig.apiKeyIndexTooltip', language)}>
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#3B82F6' }} />
</Tooltip>
</label>
@@ -730,7 +730,7 @@ export function ExchangeConfigModal({
{/* Buttons */}
<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' }}>
{editingExchangeId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
{editingExchangeId ? t('cancel', language) : t('exchangeConfig.back', language)}
</button>
<button
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"
style={{ background: '#F0B90B', color: '#000' }}
>
{isSaving ? (t('saving', language) || '保存中...') : (
{isSaving ? t('saving', language) : (
<>{t('saveConfig', language)} <ArrowRight className="w-4 h-4" /></>
)}
</button>
+26 -40
View File
@@ -79,7 +79,7 @@ export function ModelConfigModal({
const availableModels = allModels || []
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 (
<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 (
<div className="space-y-4">
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '选择 AI 模型提供商' : 'Choose Your AI Provider'}
{t('modelConfig.chooseProvider', language)}
</div>
{/* 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>
</div>
<div className="text-xs mt-0.5" style={{ color: '#A0AEC0' }}>
{language === 'zh'
? 'USDC 按次付费 · 支持全部 AI 模型 · 无需 API Key'
: 'Pay-per-call USDC · All AI Models · No API Key'}
{t('modelConfig.payPerCall', language)}
</div>
</div>
</div>
@@ -228,7 +226,7 @@ function ModelSelectionStep({
<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' }}>
{language === 'zh' ? '🔥 推荐' : '🔥 Best'}
{'🔥 ' + t('modelConfig.recommended', language)}
</div>
</div>
</div>
@@ -256,7 +254,7 @@ function ModelSelectionStep({
<div className="flex items-center gap-3 pt-2">
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
<span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}>
{language === 'zh' ? '通过钱包支付' : 'Via BlockRun Wallet'}
{t('modelConfig.viaBlockrunWallet', language)}
</span>
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
</div>
@@ -274,7 +272,7 @@ function ModelSelectionStep({
</>
)}
<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>
)
@@ -310,9 +308,7 @@ function Claw402ConfigForm({
Claw402 <span className="text-xs font-normal" style={{ color: '#60A5FA' }}></span>
</a>
<div className="text-sm mt-1" style={{ color: '#A0AEC0' }}>
{language === 'zh'
? '用 USDC 按次付费,支持所有主流 AI 模型'
: 'Pay-per-call with USDC — supports all major AI models'}
{t('modelConfig.allModelsClaw', language)}
</div>
<div className="flex items-center justify-center gap-3 mt-3 flex-wrap">
{['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => (
@@ -327,12 +323,10 @@ function Claw402ConfigForm({
<div className="space-y-3">
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
<Brain className="w-4 h-4" style={{ color: '#2563EB' }} />
{language === 'zh' ? '① 选择 AI 模型' : '① Choose AI Model'}
{t('modelConfig.selectAiModel', language)}
</label>
<div className="text-xs mb-2" style={{ color: '#848E9C' }}>
{language === 'zh'
? '所有模型通过 Claw402 统一调用,创建后可随时切换'
: 'All models unified via Claw402. Switch anytime after setup.'}
{t('modelConfig.allModelsUnified', language)}
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{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">
<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>
{language === 'zh' ? '② 设置钱包' : '② Setup Wallet'}
{t('modelConfig.setupWallet', language)}
</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="text-xs mb-2" style={{ color: '#A0AEC0' }}>
{language === 'zh'
? '💡 Claw402 使用 Base 链上的 USDC 付费,你需要一个 EVM 钱包'
: '💡 Claw402 uses USDC on Base chain. You need an EVM wallet.'}
{t('modelConfig.walletInfo', language)}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div className="flex items-center gap-1.5">
<span style={{ color: '#00E096' }}></span>
{language === 'zh'
? '可以用 MetaMask、Rabby 等钱包导出私钥'
: 'Export private key from MetaMask, Rabby, etc.'}
{t('modelConfig.exportKey', language)}
</div>
<div className="flex items-center gap-1.5">
<span style={{ color: '#00E096' }}></span>
{language === 'zh'
? '建议新建一个专用钱包,充入少量 USDC 即可'
: 'Recommended: create a dedicated wallet with a small USDC balance'}
{t('modelConfig.dedicatedWallet', language)}
</div>
</div>
</div>
<div className="space-y-1.5">
<div className="text-xs font-medium" style={{ color: '#A0AEC0' }}>
{language === 'zh' ? '钱包私钥(Base 链 EVM' : 'Wallet Private Key (Base Chain EVM)'}
{t('modelConfig.walletPrivateKey', language)}
</div>
<input
type="password"
@@ -413,9 +401,7 @@ function Claw402ConfigForm({
<div className="flex items-start gap-1.5 text-[11px]" style={{ color: '#848E9C' }}>
<span className="mt-px">🔒</span>
<span>
{language === 'zh'
? '私钥仅在本地签名使用,不会上传或发送交易。无需 ETH,无 Gas 费用。'
: 'Private key is only used locally for signing. Never uploaded. No ETH or gas needed.'}
{t('modelConfig.privateKeyNote', language)}
</span>
</div>
</div>
@@ -424,20 +410,20 @@ function Claw402ConfigForm({
{/* 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="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 className="text-xs space-y-1.5" style={{ color: '#848E9C' }}>
<div className="flex items-start gap-2">
<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 className="flex items-start gap-2">
<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 className="flex items-start gap-2">
<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>
@@ -445,7 +431,7 @@ function Claw402ConfigForm({
{/* Buttons */}
<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' }}>
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
{editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
</button>
<button
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"
style={{ background: apiKey.trim() ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
>
{language === 'zh' ? '🚀 开始交易' : '🚀 Start Trading'}
{'🚀 ' + t('modelConfig.startTrading', language)}
</button>
</div>
</form>
@@ -513,8 +499,8 @@ function StandardProviderConfigForm({
<ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} />
<span className="text-sm font-medium" style={{ color: '#A78BFA' }}>
{selectedModel.provider?.startsWith('blockrun')
? (language === 'zh' ? '开始使用' : 'Get Started')
: (language === 'zh' ? '获取 API Key' : 'Get API Key')}
? t('modelConfig.getStarted', language)
: t('modelConfig.getApiKey', language)}
</span>
</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" />
</svg>
{selectedModel.provider?.startsWith('blockrun')
? (language === 'zh' ? '钱包私钥 *' : 'Wallet Private Key *')
? t('modelConfig.walletPrivateKeyLabel', language)
: 'API Key *'}
</label>
<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">
<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>
{language === 'zh' ? '选择模型' : 'Select Model'}
{t('modelConfig.selectModelLabel', language)}
</label>
<div className="grid grid-cols-2 gap-2">
{BLOCKRUN_MODELS.map((m) => {
@@ -655,7 +641,7 @@ function StandardProviderConfigForm({
{/* Buttons */}
<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' }}>
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
{editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
</button>
<button
type="submit"
@@ -3,7 +3,7 @@ import { Check, ChevronLeft, ExternalLink, MessageCircle, Unlink, ArrowRight } f
import { toast } from 'sonner'
import { api } from '../../lib/api'
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)
function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) {
@@ -55,8 +55,6 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
const [isLoading, setIsLoading] = useState(true)
const [isUnbinding, setIsUnbinding] = useState(false)
const zh = language === 'zh'
// Load current config and available models
useEffect(() => {
Promise.all([
@@ -84,20 +82,20 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
// Basic format validation: looks like "123456789:ABCdef..."
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
}
setIsSaving(true)
try {
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()
setConfig(updated)
setToken('')
setStep(1)
} catch (err) {
toast.error(zh ? '保存失败,请检查 Token 是否正确' : 'Save failed, please verify the token')
toast.error(t('telegram.saveFailed', language))
} finally {
setIsSaving(false)
}
@@ -108,33 +106,31 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
setIsUnbinding(true)
try {
await api.unbindTelegram()
toast.success(zh ? '已解绑 Telegram 账号' : 'Telegram account unbound')
toast.success(t('telegram.unbound', language))
const updated = await api.getTelegramConfig()
setConfig(updated)
setStep(updated.token_masked ? 1 : 0)
} catch {
toast.error(zh ? '解绑失败' : 'Unbind failed')
toast.error(t('telegram.unbindFailed', language))
} finally {
setIsUnbinding(false)
}
}
const stepLabels = zh
? ['创建 Bot', '绑定账号', '完成']
: ['Create Bot', 'Bind Account', 'Done']
const stepLabels = [t('telegram.createBot', language), t('telegram.bindAccount', language), t('telegram.done', language)]
// Model selector shared between steps
const ModelSelector = () => (
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{zh ? '选择 AI 模型(可选)' : 'Select AI Model (optional)'}
{t('telegram.selectAiModel', language)}
</label>
{models.length === 0 ? (
<div
className="px-4 py-3 rounded-xl text-xs"
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>
) : (
<select
@@ -147,7 +143,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
color: selectedModelId ? '#EAECEF' : '#848E9C',
}}
>
<option value="">{zh ? '— 自动选择(推荐)' : '— Auto-select (recommended)'}</option>
<option value="">{t('telegram.autoSelect', language)}</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name} ({m.provider}{m.customModelName ? ` · ${m.customModelName}` : ''})
@@ -156,9 +152,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
</select>
)}
<div className="text-xs" style={{ color: '#848E9C' }}>
{zh
? '不选则自动使用已启用的模型'
: 'Leave blank to auto-use any enabled model'}
{t('telegram.autoUseEnabled', language)}
</div>
</div>
)
@@ -184,7 +178,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="flex items-center gap-2">
<MessageCircle className="w-6 h-6" style={{ color: '#2AABEE' }} />
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{zh ? 'Telegram Bot 配置' : 'Telegram Bot Setup'}
{t('telegram.botSetup', language)}
</h3>
</div>
</div>
@@ -207,7 +201,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<div className="px-6 pb-6 space-y-5">
{isLoading ? (
<div className="text-center py-8 text-zinc-500 text-sm font-mono">
{zh ? '加载中...' : 'Loading...'}
{t('telegram.loading', language)}
</div>
) : (
<>
@@ -222,13 +216,13 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<span className="text-2xl">🤖</span>
<div>
<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 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>2. {zh ? '发送' : 'Send'} <code className="text-blue-400">/newbot</code> {zh ? '命令' : 'command'}</div>
<div>3. {zh ? '按提示输入 Bot 名称和用户名' : 'Follow prompts to set bot name and username'}</div>
<div>4. {zh ? 'BotFather 会返回一个 Token,复制它' : 'BotFather will return a Token, copy it'}</div>
<div>1. {t('telegram.step1Desc1', language)} <code className="text-blue-400">@BotFather</code></div>
<div>2. {t('telegram.step1Desc2', language)} <code className="text-blue-400">/newbot</code> {t('telegram.step1Desc2Suffix', language)}</div>
<div>3. {t('telegram.step1Desc3', language)}</div>
<div>4. {t('telegram.step1Desc4', language)}</div>
</div>
</div>
</div>
@@ -242,12 +236,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: '#2AABEE', color: '#000' }}
>
<ExternalLink className="w-4 h-4" />
{zh ? '打开 @BotFather' : 'Open @BotFather'}
{t('telegram.openBotFather', language)}
</a>
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{zh ? '粘贴 Bot Token' : 'Paste Bot Token'}
{t('telegram.pasteToken', language)}
</label>
<input
type="password"
@@ -258,7 +252,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/>
<div className="text-xs" style={{ color: '#848E9C' }}>
{zh ? 'Token 格式:数字:字母数字串,如 123456789:ABCdef...' : 'Format: numbers:alphanumeric, e.g. 123456789:ABCdef...'}
{t('telegram.tokenFormat', language)}
</div>
</div>
@@ -271,8 +265,8 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: '#2AABEE', color: '#000' }}
>
{isSaving
? (zh ? '保存中...' : 'Saving...')
: (<>{zh ? '保存并继续' : 'Save & Continue'} <ArrowRight className="w-4 h-4" /></>)
? t('telegram.savingToken', language)
: (<>{t('telegram.saveAndContinue', language)} <ArrowRight className="w-4 h-4" /></>)
}
</button>
</div>
@@ -289,12 +283,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
<span className="text-2xl">📱</span>
<div>
<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 className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div>1. {zh ? '在 Telegram 中搜索你刚创建的 Bot' : 'Search for your newly created Bot in Telegram'}</div>
<div>2. {zh ? '点击 Start 或发送' : 'Click Start or send'} <code className="text-green-400">/start</code></div>
<div>3. {zh ? 'Bot 会自动绑定到你的账号' : 'Bot will automatically bind to your account'}</div>
<div>1. {t('telegram.step2Desc1', language)}</div>
<div>2. {t('telegram.step2Desc2', language)} <code className="text-green-400">/start</code></div>
<div>3. {t('telegram.step2Desc3', language)}</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>
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
{zh ? '当前 Token' : 'Current Token'}
{t('telegram.currentToken', language)}
</div>
<div className="text-sm font-mono" style={{ color: '#EAECEF' }}>
{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)' }}
>
<div className="text-xs" style={{ color: '#F0B90B' }}>
{zh
? '⏳ 等待你发送 /start... 发送后刷新页面查看状态'
: '⏳ Waiting for you to send /start... Refresh page after sending'}
{t('telegram.waitingForStart', language)}
</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"
style={{ background: '#2B3139', color: '#848E9C' }}
>
{zh ? '重新配置 Token' : 'Reconfigure Token'}
{t('telegram.reconfigureToken', language)}
</button>
<button
onClick={async () => {
@@ -343,19 +335,19 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
setConfig(updated)
if (updated.is_bound) {
setStep(2)
toast.success(zh ? '绑定成功!' : 'Bound successfully!')
toast.success(t('telegram.bindSuccess', language))
} 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 {
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]"
style={{ background: '#0ECB81', color: '#000' }}
>
<Check className="w-4 h-4" />
{zh ? '检查绑定状态' : 'Check Status'}
{t('telegram.checkStatus', language)}
</button>
</div>
</div>
@@ -370,12 +362,10 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
>
<div className="text-4xl">🎉</div>
<div className="font-bold text-lg" style={{ color: '#0ECB81' }}>
{zh ? 'Telegram Bot 已绑定!' : 'Telegram Bot is Active!'}
{t('telegram.botActive', language)}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{zh
? '你现在可以通过 Telegram 用自然语言控制交易系统'
: 'You can now control the trading system via natural language in Telegram'}
{t('telegram.botActiveDesc', language)}
</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="min-w-0">
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
{zh ? 'Bot Token' : 'Bot Token'}
Bot Token
</div>
<div className="text-sm font-mono truncate" style={{ color: '#EAECEF' }}>
{config.token_masked}
@@ -398,7 +388,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
{/* AI Model selector — works on active bot */}
<BoundModelSelector
zh={zh}
language={language}
models={models}
currentModelId={config?.model_id ?? ''}
onSaved={(modelId) => {
@@ -412,14 +402,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#848E9C' }}>
{zh ? '支持的命令' : 'Supported Commands'}
{t('telegram.supportedCommands', language)}
</div>
{[
{ cmd: '/help', desc: zh ? '查看所有命令' : 'Show all commands' },
{ cmd: zh ? '查看交易员状态' : 'Show trader status', desc: zh ? '自然语言查询' : 'Natural language' },
{ cmd: zh ? '启动/停止交易员' : 'Start/stop trader', desc: zh ? '自然语言控制' : 'Natural language control' },
{ cmd: zh ? '查看持仓' : 'View positions', desc: zh ? '实时持仓查询' : 'Real-time position query' },
{ cmd: zh ? '配置策略' : 'Configure strategy', desc: zh ? '修改交易策略' : 'Modify trading strategy' },
{ cmd: '/help', desc: t('telegram.cmdHelp', language) },
{ cmd: t('telegram.cmdStatus', language), desc: t('telegram.cmdNaturalLang', language) },
{ cmd: t('telegram.cmdStartStop', language), desc: t('telegram.cmdControl', language) },
{ cmd: t('telegram.cmdPositions', language), desc: t('telegram.cmdPositionsDesc', language) },
{ cmd: t('telegram.cmdStrategy', language), desc: t('telegram.cmdStrategyDesc', language) },
].map((item, i) => (
<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' }}>
@@ -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)' }}
>
<Unlink className="w-4 h-4" />
{isUnbinding ? (zh ? '解绑中...' : 'Unbinding...') : (zh ? '解绑账号' : 'Unbind Account')}
{isUnbinding ? t('telegram.unbinding', language) : t('telegram.unbindAccount', language)}
</button>
<button
onClick={onClose}
className="flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
style={{ background: '#2AABEE', color: '#000' }}
>
{zh ? '完成' : 'Done'}
{t('telegram.done', language)}
</button>
</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.
// It updates the model_id without requiring re-entry of the bot token.
function BoundModelSelector({
zh,
language,
models,
currentModelId,
onSaved,
}: {
zh: boolean
language: Language
models: AIModel[]
currentModelId: string
onSaved: (modelId: string) => void
@@ -483,9 +473,9 @@ function BoundModelSelector({
// POST /api/telegram/model — lightweight endpoint for model-only update
await api.updateTelegramModel(modelId)
onSaved(modelId)
toast.success(zh ? 'AI 模型已更新' : 'AI model updated')
toast.success(t('telegram.modelUpdated', language))
} catch {
toast.error(zh ? '更新失败' : 'Update failed')
toast.error(t('telegram.modelUpdateFailed', language))
} finally {
setIsSaving(false)
}
@@ -496,7 +486,7 @@ function BoundModelSelector({
return (
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{zh ? 'AI 模型(用于自然语言解析)' : 'AI Model (for natural language)'}
{t('telegram.aiModelLabel', language)}
</label>
<div className="flex gap-2">
<select
@@ -509,7 +499,7 @@ function BoundModelSelector({
color: modelId ? '#EAECEF' : '#848E9C',
}}
>
<option value="">{zh ? '— 自动选择' : '— Auto-select'}</option>
<option value="">{t('telegram.aiModelAutoSelect', language)}</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{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"
style={{ background: '#F0B90B', color: '#000', whiteSpace: 'nowrap' }}
>
{isSaving ? '...' : (zh ? '保存' : 'Save')}
{isSaving ? '...' : t('telegram.save', language)}
</button>
</div>
</div>
@@ -1,7 +1,9 @@
import type { TraderConfigData } from '../../types'
import { t } from '../../i18n/translations'
import { useLanguage } from '../../contexts/LanguageContext'
import { PunkAvatar, getTraderAvatar } from '../common/PunkAvatar'
// 提取下划线后面的名称部分
// Extract the name part after the last underscore
function getShortName(fullName: string): string {
const parts = fullName.split('_')
return parts.length > 1 ? parts[parts.length - 1] : fullName
@@ -18,6 +20,7 @@ export function TraderConfigViewModal({
onClose,
traderData,
}: TraderConfigViewModalProps) {
const { language } = useLanguage()
if (!isOpen || !traderData) return null
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">
<span className="text-sm text-[#848E9C] font-medium">{label}</span>
<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>
</div>
)
@@ -50,9 +53,9 @@ export function TraderConfigViewModal({
className="rounded-lg"
/>
<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">
{traderData.trader_name}
{t('traderConfigView.configInfo', language, { name: traderData.trader_name })}
</p>
</div>
</div>
@@ -67,7 +70,7 @@ export function TraderConfigViewModal({
}
>
<span>{traderData.is_running ? '●' : '○'}</span>
{traderData.is_running ? '运行中' : '已停止'}
{traderData.is_running ? t('traderConfigView.running', language) : t('traderConfigView.stopped', language)}
</div>
<button
onClick={onClose}
@@ -83,32 +86,32 @@ export function TraderConfigViewModal({
{/* Basic Info */}
<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">
🤖
{'🤖 ' + t('traderConfigView.basicInfo', language)}
</h3>
<div className="space-y-3">
<InfoRow
label="交易员名称"
label={t('traderConfigView.traderName', language)}
value={traderData.trader_name}
/>
<InfoRow
label="AI模型"
label={t('traderConfigView.aiModel', language)}
value={getShortName(traderData.ai_model).toUpperCase()}
/>
<InfoRow
label="交易所"
label={t('traderConfigView.exchange', language)}
value={getShortName(traderData.exchange_id).toUpperCase()}
/>
<InfoRow
label="初始余额"
label={t('traderConfigView.initialBalance', language)}
value={`$${traderData.initial_balance.toLocaleString()}`}
/>
<InfoRow
label="保证金模式"
value={traderData.is_cross_margin ? '全仓' : '逐仓'}
label={t('traderConfigView.marginMode', language)}
value={traderData.is_cross_margin ? t('traderConfigView.crossMargin', language) : t('traderConfigView.isolatedMargin', language)}
/>
<InfoRow
label="扫描间隔"
value={`${traderData.scan_interval_minutes || 3} 分钟`}
label={t('traderConfigView.scanIntervalLabel', language)}
value={t('traderConfigView.scanInterval', language, { minutes: traderData.scan_interval_minutes || 3 })}
/>
</div>
</div>
@@ -117,11 +120,11 @@ export function TraderConfigViewModal({
{traderData.strategy_id && (
<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">
📋 使
{'📋 ' + t('traderConfigView.strategyUsed', language)}
</h3>
<div className="space-y-3">
<InfoRow
label="策略名称"
label={t('traderConfigView.strategyName', language)}
value={traderData.strategy_name || traderData.strategy_id}
/>
</div>
@@ -135,7 +138,7 @@ export function TraderConfigViewModal({
onClick={onClose}
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>
</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
reset401Flag()
// 先检查是否为管理员模式(使用带缓存的系统配置获取)
// Check if admin mode is active (uses cached system config)
getSystemConfig()
.then(() => {
// 不再在管理员模式下模拟登录;统一检查本地存储
// No longer simulate login in admin mode; check local storage uniformly
const savedToken = localStorage.getItem('auth_token')
const savedUser = localStorage.getItem('auth_user')
if (savedToken && savedUser) {
@@ -60,7 +60,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
})
.catch((err) => {
console.error('Failed to fetch system config:', err)
// 发生错误时,继续检查本地存储
// On error, continue checking local storage
const savedToken = localStorage.getItem('auth_token')
const savedUser = localStorage.getItem('auth_user')
@@ -119,7 +119,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
window.history.pushState({}, '', returnUrl)
window.dispatchEvent(new PopStateEvent('popstate'))
} else {
// 跳转到配置页面
// Redirect to config page
window.history.pushState({}, '', '/traders')
window.dispatchEvent(new PopStateEvent('popstate'))
}
@@ -128,7 +128,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
// Unexpected success response
return { success: false, message: data.message || '登录响应异常' }
return { success: false, message: data.message || 'Unexpected login response' }
} else {
return {
success: false,
@@ -136,7 +136,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
}
} 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.dispatchEvent(new PopStateEvent('popstate'))
} else {
// 跳转到仪表盘
// Redirect to dashboard
window.history.pushState({}, '', '/dashboard')
window.dispatchEvent(new PopStateEvent('popstate'))
}
return { success: true }
} else {
return { success: false, message: data.error || '登录失败' }
return { success: false, message: data.error || 'Login failed' }
}
} 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.dispatchEvent(new PopStateEvent('popstate'))
} else {
// 跳转到配置页面
// Redirect to config page
window.history.pushState({}, '', '/traders')
window.dispatchEvent(new PopStateEvent('popstate'))
}
@@ -269,7 +269,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return { success: false, message: data.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' },
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' },
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' },
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' },
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' },
resetToDefault: { zh: '重置为默认', en: 'Reset to Default', es: 'Restablecer' },
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' },
notConfigured: { zh: '未配置', en: 'Not Configured', es: 'No Configurado' },
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' },
};
// ============================================================================
// 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
// ============================================================================
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -183,13 +183,13 @@ export const backtestApi = {
try {
const data = text ? JSON.parse(text) : null
throw new Error(
data?.error || data?.message || text || '导出失败,请稍后再试'
data?.error || data?.message || text || 'Export failed, please try again later'
)
} catch (err) {
if (err instanceof Error && err.message) {
throw err
}
throw new Error(text || '导出失败,请稍后再试')
throw new Error(text || 'Export failed, please try again later')
}
}
return res.blob()
+36 -36
View File
@@ -10,7 +10,7 @@ import { API_BASE, httpClient, CryptoService } from './helpers'
export const configApi = {
async getModelConfigs(): Promise<AIModel[]> {
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 : []
},
@@ -18,13 +18,13 @@ export const configApi = {
const result = await httpClient.get<AIModel[]>(
`${API_BASE}/supported-models`
)
if (!result.success) throw new Error('获取支持的模型失败')
if (!result.success) throw new Error('Failed to fetch supported models')
return result.data!
},
async getPromptTemplates(): Promise<string[]> {
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()
if (Array.isArray(data.templates)) {
return data.templates.map((item: { name: string }) => item.name)
@@ -33,41 +33,41 @@ export const configApi = {
},
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
// 检查是否启用了传输加密
// Check if transport encryption is enabled
const config = await CryptoService.fetchCryptoConfig()
if (!config.transport_encryption) {
// 传输加密禁用时,直接发送明文
// Transport encryption disabled, send plaintext
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
}
// 获取RSA公钥
// Fetch RSA public key
const publicKey = await CryptoService.fetchPublicKey()
// 初始化加密服务
// Initialize crypto service
await CryptoService.initialize(publicKey)
// 获取用户信息(从localStorage或其他地方)
// Get user info from localStorage
const userId = localStorage.getItem('user_id') || ''
const sessionId = sessionStorage.getItem('session_id') || ''
// 加密敏感数据
// Encrypt sensitive data
const encryptedPayload = await CryptoService.encryptSensitiveData(
JSON.stringify(request),
userId,
sessionId
)
// 发送加密数据
// Send encrypted data
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[]> {
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!
},
@@ -75,7 +75,7 @@ export const configApi = {
const result = await httpClient.get<Exchange[]>(
`${API_BASE}/supported-exchanges`
)
if (!result.success) throw new Error('获取支持的交易所失败')
if (!result.success) throw new Error('Failed to fetch supported exchanges')
return result.data!
},
@@ -83,93 +83,93 @@ export const configApi = {
request: UpdateExchangeConfigRequest
): Promise<void> {
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 }> {
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!
},
async createExchangeEncrypted(request: CreateExchangeRequest): Promise<{ id: string }> {
// 检查是否启用了传输加密
// Check if transport encryption is enabled
const config = await CryptoService.fetchCryptoConfig()
if (!config.transport_encryption) {
// 传输加密禁用时,直接发送明文
// Transport encryption disabled, send plaintext
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!
}
// 获取RSA公钥
// Fetch RSA public key
const publicKey = await CryptoService.fetchPublicKey()
// 初始化加密服务
// Initialize crypto service
await CryptoService.initialize(publicKey)
// 获取用户信息
// Get user info
const userId = localStorage.getItem('user_id') || ''
const sessionId = sessionStorage.getItem('session_id') || ''
// 加密敏感数据
// Encrypt sensitive data
const encryptedPayload = await CryptoService.encryptSensitiveData(
JSON.stringify(request),
userId,
sessionId
)
// 发送加密数据
// Send encrypted data
const result = await httpClient.post<{ id: string }>(
`${API_BASE}/exchanges`,
encryptedPayload
)
if (!result.success) throw new Error('创建交易所账户失败')
if (!result.success) throw new Error('Failed to create exchange account')
return result.data!
},
async deleteExchange(exchangeId: string): Promise<void> {
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(
request: UpdateExchangeConfigRequest
): Promise<void> {
// 检查是否启用了传输加密
// Check if transport encryption is enabled
const config = await CryptoService.fetchCryptoConfig()
if (!config.transport_encryption) {
// 传输加密禁用时,直接发送明文
// Transport encryption disabled, send plaintext
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
}
// 获取RSA公钥
// Fetch RSA public key
const publicKey = await CryptoService.fetchPublicKey()
// 初始化加密服务
// Initialize crypto service
await CryptoService.initialize(publicKey)
// 获取用户信息(从localStorage或其他地方)
// Get user info from localStorage
const userId = localStorage.getItem('user_id') || ''
const sessionId = sessionStorage.getItem('session_id') || ''
// 加密敏感数据
// Encrypt sensitive data
const encryptedPayload = await CryptoService.encryptSensitiveData(
JSON.stringify(request),
userId,
sessionId
)
// 发送加密数据
// Send encrypted data
const result = await httpClient.put(
`${API_BASE}/exchanges`,
encryptedPayload
)
if (!result.success) throw new Error('更新交易所配置失败')
if (!result.success) throw new Error('Failed to update exchange configs')
},
async getServerIP(): Promise<{
@@ -180,7 +180,7 @@ export const configApi = {
public_ip: string
message: string
}>(`${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!
},
}
+12 -13
View File
@@ -15,7 +15,7 @@ export const dataApi = {
? `${API_BASE}/status?trader_id=${traderId}`
: `${API_BASE}/status`
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!
},
@@ -24,8 +24,7 @@ export const dataApi = {
? `${API_BASE}/account?trader_id=${traderId}`
: `${API_BASE}/account`
const result = await httpClient.get<AccountInfo>(url)
if (!result.success) throw new Error('获取账户信息失败')
console.log('Account data fetched:', result.data)
if (!result.success) throw new Error('Failed to fetch account info')
return result.data!
},
@@ -34,7 +33,7 @@ export const dataApi = {
? `${API_BASE}/positions?trader_id=${traderId}`
: `${API_BASE}/positions`
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!
},
@@ -43,7 +42,7 @@ export const dataApi = {
? `${API_BASE}/decisions?trader_id=${traderId}`
: `${API_BASE}/decisions`
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!
},
@@ -60,7 +59,7 @@ export const dataApi = {
const result = await httpClient.get<DecisionRecord[]>(
`${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!
},
@@ -69,7 +68,7 @@ export const dataApi = {
? `${API_BASE}/statistics?trader_id=${traderId}`
: `${API_BASE}/statistics`
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!
},
@@ -78,7 +77,7 @@ export const dataApi = {
? `${API_BASE}/equity-history?trader_id=${traderId}`
: `${API_BASE}/equity-history`
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!
},
@@ -87,13 +86,13 @@ export const dataApi = {
`${API_BASE}/equity-history-batch`,
{ 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!
},
async getTopTraders(): Promise<any[]> {
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!
},
@@ -101,7 +100,7 @@ export const dataApi = {
const result = await httpClient.get<any>(
`${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!
},
@@ -109,7 +108,7 @@ export const dataApi = {
const result = await httpClient.get<CompetitionData>(
`${API_BASE}/competition`
)
if (!result.success) throw new Error('获取竞赛数据失败')
if (!result.success) throw new Error('Failed to fetch competition data')
return result.data!
},
@@ -117,7 +116,7 @@ export const dataApi = {
const result = await httpClient.get<PositionHistoryResponse>(
`${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!
},
}
+1 -1
View File
@@ -31,7 +31,7 @@ export async function handleJSONResponse<T>(res: Response): Promise<T> {
} catch {
/* ignore JSON parse errors */
}
throw new Error(message || '请求失败')
throw new Error(message || 'Request failed')
}
if (!text) {
return {} as T
+9 -9
View File
@@ -7,26 +7,26 @@ import { API_BASE, httpClient } from './helpers'
export const strategyApi = {
async getStrategies(): Promise<Strategy[]> {
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
return Array.isArray(strategies) ? strategies : []
},
async getStrategy(strategyId: string): Promise<Strategy> {
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!
},
async getActiveStrategy(): Promise<Strategy> {
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!
},
async getDefaultStrategyConfig(): Promise<StrategyConfig> {
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!
},
@@ -36,7 +36,7 @@ export const strategyApi = {
config: StrategyConfig
}): Promise<Strategy> {
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!
},
@@ -49,24 +49,24 @@ export const strategyApi = {
}
): Promise<Strategy> {
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!
},
async deleteStrategy(strategyId: string): Promise<void> {
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> {
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!
},
async duplicateStrategy(strategyId: string): Promise<Strategy> {
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!
},
}
+4 -4
View File
@@ -4,22 +4,22 @@ import { API_BASE, httpClient } from './helpers'
export const telegramApi = {
async getTelegramConfig(): Promise<TelegramConfig> {
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!
},
async updateTelegramConfig(token: string, modelId?: string): Promise<void> {
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> {
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> {
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 = {
async getTraders(): Promise<TraderInfo[]> {
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 : []
},
async getPublicTraders(): Promise<any[]> {
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!
},
@@ -23,25 +23,25 @@ export const traderApi = {
`${API_BASE}/traders`,
request
)
if (!result.success) throw new Error('创建交易员失败')
if (!result.success) throw new Error('Failed to create trader')
return result.data!
},
async deleteTrader(traderId: string): Promise<void> {
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> {
const result = await httpClient.post(
`${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> {
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> {
@@ -49,7 +49,7 @@ export const traderApi = {
`${API_BASE}/traders/${traderId}/competition`,
{ 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 }> {
@@ -57,7 +57,7 @@ export const traderApi = {
`${API_BASE}/traders/${traderId}/close-position`,
{ symbol, side }
)
if (!result.success) throw new Error('平仓失败')
if (!result.success) throw new Error('Failed to close position')
return result.data!
},
@@ -69,14 +69,14 @@ export const traderApi = {
`${API_BASE}/traders/${traderId}/prompt`,
{ 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> {
const result = await httpClient.get<TraderConfigData>(
`${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!
},
@@ -88,7 +88,7 @@ export const traderApi = {
`${API_BASE}/traders/${traderId}`,
request
)
if (!result.success) throw new Error('更新交易员失败')
if (!result.success) throw new Error('Failed to update trader')
return result.data!
},
}
+4 -4
View File
@@ -1,14 +1,14 @@
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 {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
} else {
// 兼容降级:创建临时文本域执行复制
// Fallback: create temporary textarea for copy
const el = document.createElement('textarea')
el.value = text
el.style.position = 'fixed'
@@ -22,7 +22,7 @@ export async function copyWithToast(text: string, successMsg = '已复制') {
return true
} catch (err) {
console.error('Clipboard copy failed:', err)
notify.error('复制失败')
notify.error('Copy failed')
return false
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
export function DataPage() {
const { language } = useLanguage()
@@ -7,7 +8,7 @@ export function DataPage() {
<div className="w-full h-[calc(100vh-64px)]">
<iframe
src="https://nofxos.ai/dashboard"
title={language === 'zh' ? '数据中心' : 'Data Center'}
title={t('dataCenter', language)}
className="w-full h-full border-0"
allow="fullscreen"
/>
+19 -99
View File
@@ -20,6 +20,7 @@ import {
import { useLanguage } from '../contexts/LanguageContext'
import { useAuth } from '../contexts/AuthContext'
import { toast } from 'sonner'
import { t } from '../i18n/translations'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
interface PublicStrategy {
@@ -106,88 +107,7 @@ export function StrategyMarketPage() {
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [copiedId, setCopiedId] = useState<string | null>(null)
const texts = {
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]
const tr = (key: string) => t(`strategyMarket.${key}`, language)
// Fetch public strategies
const { data: strategies, isLoading } = useSWR<PublicStrategy[]>(
@@ -218,7 +138,7 @@ export function StrategyMarketPage() {
try {
await navigator.clipboard.writeText(JSON.stringify(strategy.config, null, 2))
setCopiedId(strategy.id)
toast.success(t.copied)
toast.success(tr('copied'))
setTimeout(() => setCopiedId(null), 2000)
} catch (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" />
</div>
<div>
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={t.title}>
{t.title}
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={tr('title')}>
{tr('title')}
</h1>
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
// {t.subtitle}
// {tr('subtitle')}
</p>
</div>
</div>
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
{t.description}
{tr('description')}
</p>
</div>
@@ -295,7 +215,7 @@ export function StrategyMarketPage() {
</div>
<input
type="text"
placeholder={t.search}
placeholder={tr('search')}
value={searchQuery}
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"
@@ -324,7 +244,7 @@ export function StrategyMarketPage() {
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>
))}
</div>
@@ -340,7 +260,7 @@ export function StrategyMarketPage() {
<Cpu size={24} className="text-nofx-gold/50" />
</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="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>
@@ -357,9 +277,9 @@ export function StrategyMarketPage() {
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
</div>
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
[{t.noStrategies}]
[{tr('noStrategies')}]
</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>
)}
@@ -423,11 +343,11 @@ export function StrategyMarketPage() {
{/* Meta Data */}
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
<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>
</div>
<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>
</div>
</div>
@@ -468,7 +388,7 @@ export function StrategyMarketPage() {
) : (
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
<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>
@@ -483,19 +403,19 @@ export function StrategyMarketPage() {
{copiedId === strategy.id ? (
<>
<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" />
{t.copyConfig}
{tr('copyConfig')}
</>
)}
</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">
<Shield size={12} />
{t.hideConfig}
{tr('hideConfig')}
</button>
)}
</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">
<Hexagon className="text-nofx-gold animate-spin-slow" size={24} />
<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>
<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 { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
import { t } from '../i18n/translations'
const API_BASE = import.meta.env.VITE_API_BASE || ''
@@ -108,7 +109,7 @@ export function StrategyStudioPage() {
})
if (response.ok) {
const data = await response.json()
// 后端返回的是数组,不是 { models: [] }
// Backend returns an array, not { models: [] }
const allModels = Array.isArray(data) ? data : (data.models || [])
const enabledModels = allModels.filter((m: AIModel) => m.enabled)
setAiModels(enabledModels)
@@ -209,7 +210,7 @@ export function StrategyStudioPage() {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: language === 'zh' ? '新策略' : 'New Strategy',
name: tr('newStrategyName'),
description: '',
config: defaultConfig,
}),
@@ -222,7 +223,7 @@ export function StrategyStudioPage() {
const now = new Date().toISOString()
const newStrategy = {
id: result.id,
name: language === 'zh' ? '新策略' : 'New Strategy',
name: tr('newStrategyName'),
description: '',
is_active: false,
is_default: false,
@@ -246,11 +247,11 @@ export function StrategyStudioPage() {
if (!token) return
const confirmed = await confirmToast(
language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?',
tr('confirmDeleteStrategy'),
{
title: language === 'zh' ? '确认删除' : 'Confirm Delete',
okText: language === 'zh' ? '删除' : 'Delete',
cancelText: language === 'zh' ? '取消' : 'Cancel',
title: tr('confirmDelete'),
okText: tr('delete'),
cancelText: tr('cancel'),
}
)
if (!confirmed) return
@@ -261,7 +262,7 @@ export function StrategyStudioPage() {
headers: { Authorization: `Bearer ${token}` },
})
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
if (selectedStrategy?.id === id) {
setSelectedStrategy(null)
@@ -287,7 +288,7 @@ export function StrategyStudioPage() {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: language === 'zh' ? '策略副本' : 'Strategy Copy',
name: tr('strategyCopy'),
}),
})
if (!response.ok) throw new Error('Failed to duplicate strategy')
@@ -330,7 +331,7 @@ export function StrategyStudioPage() {
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
notify.success(language === 'zh' ? '策略已导出' : 'Strategy exported')
notify.success(tr('strategyExported'))
}
// Import strategy from JSON file
@@ -344,7 +345,7 @@ export function StrategyStudioPage() {
// Validate imported data
if (!importData.config || !importData.name) {
throw new Error(language === 'zh' ? '无效的策略文件' : 'Invalid strategy file')
throw new Error(tr('invalidStrategyFile'))
}
// Create new strategy with imported config
@@ -355,14 +356,14 @@ export function StrategyStudioPage() {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: `${importData.name} (${language === 'zh' ? '导入' : 'Imported'})`,
name: `${importData.name} (${tr('imported')})`,
description: importData.description || '',
config: importData.config,
}),
})
if (!response.ok) throw new Error('Failed to import strategy')
notify.success(language === 'zh' ? '策略已导入' : 'Strategy imported')
notify.success(tr('strategyImported'))
await fetchStrategies()
} catch (err) {
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')
setHasChanges(false)
notify.success(language === 'zh' ? '策略已保存' : 'Strategy saved')
notify.success(tr('strategySaved'))
await fetchStrategies()
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
@@ -482,51 +483,7 @@ export function StrategyStudioPage() {
}
}
const t = (key: string) => {
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
}
const tr = (key: string) => t(`strategyStudio.${key}`, language)
if (isLoading) {
return (
@@ -550,7 +507,7 @@ export function StrategyStudioPage() {
key: 'gridConfig' as const,
icon: Activity,
color: '#0ECB81',
title: t('gridConfig'),
title: tr('gridConfig'),
forStrategyType: 'grid_trading' as const,
content: editingConfig?.grid_config && (
<GridConfigEditor
@@ -566,7 +523,7 @@ export function StrategyStudioPage() {
key: 'coinSource' as const,
icon: Target,
color: '#F0B90B',
title: t('coinSource'),
title: tr('coinSource'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<CoinSourceEditor
@@ -581,7 +538,7 @@ export function StrategyStudioPage() {
key: 'indicators' as const,
icon: BarChart3,
color: '#0ECB81',
title: t('indicators'),
title: tr('indicators'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<IndicatorEditor
@@ -596,7 +553,7 @@ export function StrategyStudioPage() {
key: 'riskControl' as const,
icon: Shield,
color: '#F6465D',
title: t('riskControl'),
title: tr('riskControl'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<RiskControlEditor
@@ -611,7 +568,7 @@ export function StrategyStudioPage() {
key: 'promptSections' as const,
icon: FileText,
color: '#a855f7',
title: t('promptSections'),
title: tr('promptSections'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<PromptSectionsEditor
@@ -626,18 +583,18 @@ export function StrategyStudioPage() {
key: 'customPrompt' as const,
icon: Settings,
color: '#60a5fa',
title: t('customPrompt'),
title: tr('customPrompt'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
<div>
<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>
<textarea
value={editingConfig.custom_prompt || ''}
onChange={(e) => updateConfig('custom_prompt', e.target.value)}
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"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/>
@@ -648,7 +605,7 @@ export function StrategyStudioPage() {
key: 'publishSettings' as const,
icon: Globe,
color: '#0ECB81',
title: t('publishSettings'),
title: tr('publishSettings'),
forStrategyType: 'both' as const,
content: selectedStrategy && (
<PublishSettingsEditor
@@ -683,8 +640,8 @@ export function StrategyStudioPage() {
<Sparkles className="w-5 h-5 text-black" />
</div>
<div>
<h1 className="text-lg font-bold text-nofx-text">{t('strategyStudio')}</h1>
<p className="text-xs text-nofx-text-muted">{t('subtitle')}</p>
<h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
</div>
</div>
{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="p-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">
{/* 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" />
<input
type="file"
@@ -717,7 +674,7 @@ export function StrategyStudioPage() {
<button
onClick={handleCreateStrategy}
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" />
</button>
@@ -745,7 +702,7 @@ export function StrategyStudioPage() {
<button
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
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" />
</button>
@@ -754,14 +711,14 @@ export function StrategyStudioPage() {
<button
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
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" />
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
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" />
</button>
@@ -772,18 +729,18 @@ export function StrategyStudioPage() {
<div className="flex items-center gap-1 mt-1 flex-wrap">
{strategy.is_active && (
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-success/15 text-nofx-success">
{t('active')}
{tr('active')}
</span>
)}
{strategy.is_default && (
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-gold/15 text-nofx-gold">
{t('default')}
{tr('default')}
</span>
)}
{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">
<Globe className="w-2.5 h-2.5" />
{language === 'zh' ? '公开' : 'Public'}
{tr('public')}
</span>
)}
</div>
@@ -818,11 +775,11 @@ export function StrategyStudioPage() {
setHasChanges(true)
}}
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"
/>
{hasChanges && (
<span className="text-xs text-nofx-gold"> {language === 'zh' ? '未保存' : 'Unsaved'}</span>
<span className="text-xs text-nofx-gold"> {tr('unsaved')}</span>
)}
</div>
<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"
>
<Check className="w-3 h-3" />
{t('activate')}
{tr('activate')}
</button>
)}
{!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'}`}
>
<Save className="w-3 h-3" />
{isSaving ? t('saving') : t('save')}
{isSaving ? tr('saving') : tr('save')}
</button>
)}
</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="flex items-center gap-2 mb-3">
<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 className="grid grid-cols-2 gap-3">
<button
@@ -874,9 +831,9 @@ export function StrategyStudioPage() {
>
<div className="flex items-center gap-2 mb-1">
<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>
<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
onClick={() => {
@@ -897,9 +854,9 @@ export function StrategyStudioPage() {
>
<div className="flex items-center gap-2 mb-1">
<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>
<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>
</div>
</div>
@@ -940,7 +897,7 @@ export function StrategyStudioPage() {
<div className="text-center">
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" />
<p className="text-sm text-nofx-text-muted">
{language === 'zh' ? '选择或创建策略' : 'Select or create a strategy'}
{tr('selectOrCreate')}
</p>
</div>
</div>
@@ -957,7 +914,7 @@ export function StrategyStudioPage() {
}`}
>
<Eye className="w-4 h-4" />
{t('promptPreview')}
{tr('promptPreview')}
</button>
<button
onClick={() => setActiveRightTab('test')}
@@ -965,7 +922,7 @@ export function StrategyStudioPage() {
}`}
>
<Play className="w-4 h-4" />
{t('aiTestRun')}
{tr('aiTestRun')}
</button>
</div>
@@ -981,9 +938,9 @@ export function StrategyStudioPage() {
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"
>
<option value="balanced">{t('balanced')}</option>
<option value="aggressive">{t('aggressive')}</option>
<option value="conservative">{t('conservative')}</option>
<option value="balanced">{tr('balanced')}</option>
<option value="aggressive">{tr('aggressive')}</option>
<option value="conservative">{tr('conservative')}</option>
</select>
<button
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"
>
{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>
</div>
@@ -1018,7 +975,7 @@ export function StrategyStudioPage() {
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5">
<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>
<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
@@ -1035,7 +992,7 @@ export function StrategyStudioPage() {
) : (
<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" />
<p className="text-sm">{language === 'zh' ? '点击生成 Prompt 预览' : 'Click to generate prompt preview'}</p>
<p className="text-sm">{tr('generatePromptPreview')}</p>
</div>
)}
</div>
@@ -1046,7 +1003,7 @@ export function StrategyStudioPage() {
<div className="space-y-2">
<div className="flex items-center gap-2">
<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>
{aiModels.length > 0 ? (
<select
@@ -1062,7 +1019,7 @@ export function StrategyStudioPage() {
</select>
) : (
<div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger">
{t('noModel')}
{tr('noModel')}
</div>
)}
@@ -1072,9 +1029,9 @@ export function StrategyStudioPage() {
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"
>
<option value="balanced">{t('balanced')}</option>
<option value="aggressive">{t('aggressive')}</option>
<option value="conservative">{t('conservative')}</option>
<option value="balanced">{tr('balanced')}</option>
<option value="aggressive">{tr('aggressive')}</option>
<option value="conservative">{tr('conservative')}</option>
</select>
<button
onClick={runAiTest}
@@ -1084,17 +1041,17 @@ export function StrategyStudioPage() {
{isRunningAiTest ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
{t('running')}
{tr('running')}
</>
) : (
<>
<Send className="w-4 h-4" />
{t('runTest')}
{tr('runTest')}
</>
)}
</button>
</div>
<p className="text-[10px] text-nofx-text-muted">{t('testNote')}</p>
<p className="text-[10px] text-nofx-text-muted">{tr('testNote')}</p>
</div>
{/* Test Results */}
@@ -1110,7 +1067,7 @@ export function StrategyStudioPage() {
<div className="flex items-center gap-2">
<Clock className="w-3 h-3 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>
</div>
)}
@@ -1120,7 +1077,7 @@ export function StrategyStudioPage() {
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<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>
<pre
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 className="flex items-center gap-1.5 mb-1.5">
<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>
<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"
@@ -1152,7 +1109,7 @@ export function StrategyStudioPage() {
<div>
<div className="flex items-center gap-1.5 mb-1.5">
<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>
<pre
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 className="flex items-center gap-1.5 mb-1.5">
<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>
<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"
@@ -1184,7 +1141,7 @@ export function StrategyStudioPage() {
) : (
<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" />
<p className="text-sm">{language === 'zh' ? '点击运行 AI 测试' : 'Click to run AI test'}</p>
<p className="text-sm">{tr('runAiTestHint')}</p>
</div>
)}
</div>
+29 -43
View File
@@ -23,7 +23,7 @@ import type {
// --- Helper Functions ---
// 获取友好的AI模型名称
// Get friendly AI model display name
function getModelDisplayName(modelId: string): string {
switch (modelId.toLowerCase()) {
case 'deepseek':
@@ -189,19 +189,17 @@ export function TraderDashboardPage({
}, 100)
}
// 平仓操作
// Close position handler
const handleClosePosition = async (symbol: string, side: string) => {
if (!selectedTraderId) return
const confirmMsg =
language === 'zh'
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
const sideLabel = side === 'LONG' ? 'LONG' : 'SHORT'
const confirmMsg = t('traderDashboard.confirmClosePosition', language, { symbol, side: sideLabel })
const confirmed = await confirmToast(confirmMsg, {
title: language === 'zh' ? '确认平仓' : 'Confirm Close',
okText: language === 'zh' ? '确认' : 'Confirm',
cancelText: language === 'zh' ? '取消' : 'Cancel',
title: t('traderDashboard.confirmClose', language),
okText: t('traderDashboard.confirm', language),
cancelText: t('traderDashboard.cancel', language),
})
if (!confirmed) return
@@ -209,10 +207,8 @@ export function TraderDashboardPage({
setClosingPosition(symbol)
try {
await api.closePosition(selectedTraderId, symbol, side)
notify.success(
language === 'zh' ? '平仓成功' : 'Position closed successfully'
)
// 使用 SWR mutate 刷新数据而非重新加载页面
notify.success(t('traderDashboard.positionClosed', language))
// Use SWR mutate to refresh data instead of reloading page
await Promise.all([
mutate(`positions-${selectedTraderId}`),
mutate(`account-${selectedTraderId}`),
@@ -221,9 +217,7 @@ export function TraderDashboardPage({
const errorMsg =
err instanceof Error
? err.message
: language === 'zh'
? '平仓失败'
: 'Failed to close position'
: t('traderDashboard.closeFailed', language)
notify.error(errorMsg)
} finally {
setClosingPosition(null)
@@ -257,18 +251,16 @@ export function TraderDashboardPage({
</svg>
</div>
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
{language === 'zh' ? '无法连接到服务器' : 'Connection Failed'}
{t('traderDashboard.connectionFailed', language)}
</h2>
<p className="text-base mb-6 text-nofx-text-muted">
{language === 'zh'
? '请确认后端服务已启动。'
: 'Please check if the backend service is running.'}
{t('traderDashboard.connectionFailedDesc', language)}
</p>
<button
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"
>
{language === 'zh' ? '重试' : 'Retry'}
{t('traderDashboard.retry', language)}
</button>
</div>
</div>
@@ -414,12 +406,8 @@ export function TraderDashboardPage({
className="p-1 rounded hover:bg-white/10 transition-colors"
title={
showWalletAddress
? language === 'zh'
? '隐藏地址'
: 'Hide address'
: language === 'zh'
? '显示完整地址'
: 'Show full address'
? t('traderDashboard.hideAddress', language)
: t('traderDashboard.showFullAddress', language)
}
>
{showWalletAddress ? (
@@ -432,7 +420,7 @@ export function TraderDashboardPage({
type="button"
onClick={handleCopyAddress}
className="p-1 rounded hover:bg-white/10 transition-colors"
title={language === 'zh' ? '复制地址' : 'Copy address'}
title={t('traderDashboard.copyAddress', language)}
>
{copiedAddress ? (
<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">
{language === 'zh' ? '未配置地址' : 'No address configured'}
{t('traderDashboard.noAddressConfigured', language)}
</span>
)}
</div>
@@ -599,14 +587,14 @@ export function TraderDashboardPage({
<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-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-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('markPrice', language)}>{language === 'zh' ? '标记价' : 'Mark'}</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 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-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-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 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-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)}>{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)}>{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)}>{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)}>{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)}>{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)}>{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)}>{t('traderDashboard.liq', language)}</th>
</tr>
</thead>
<tbody>
@@ -644,14 +632,14 @@ export function TraderDashboardPage({
}}
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"
title={language === 'zh' ? '平仓' : 'Close Position'}
title={t('traderDashboard.closePosition', language)}
>
{closingPosition === pos.symbol ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<LogOut className="w-3 h-3" />
)}
{language === 'zh' ? '平仓' : 'Close'}
{t('traderDashboard.close', language)}
</button>
</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 && (
<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>
{language === 'zh'
? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓`
: `Showing ${paginatedPositions.length} of ${totalPositions} positions`}
{t('traderDashboard.showingPositions', language, { shown: paginatedPositions.length, total: totalPositions })}
</span>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span>{language === 'zh' ? '每页' : 'Per page'}:</span>
<span>{t('traderDashboard.perPage', language)}:</span>
<select
value={positionsPageSize}
onChange={(e) => setPositionsPageSize(Number(e.target.value))}