mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
refactor: optimize codebase encoding
This commit is contained in:
+1
-1
@@ -61,6 +61,6 @@ DB_NAME=nofx
|
|||||||
DB_SSLMODE=disable
|
DB_SSLMODE=disable
|
||||||
|
|
||||||
|
|
||||||
# 数据库配置 - SQLite(默认)
|
# Database configuration - SQLite (default)
|
||||||
DB_TYPE=sqlite
|
DB_TYPE=sqlite
|
||||||
DB_PATH=data/data.db
|
DB_PATH=data/data.db
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
name: PR Docker Build Check
|
name: PR Docker Build Check
|
||||||
|
|
||||||
# PR 时只做轻量级构建检查,不推送镜像
|
# Lightweight build check on PR only, no image push
|
||||||
# 策略: 快速验证 amd64 + 抽样检查 arm64 (backend only)
|
# Strategy: Quick verify amd64 + spot check arm64 (backend only)
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
@@ -18,7 +18,7 @@ on:
|
|||||||
- '.github/workflows/pr-docker-check.yml'
|
- '.github/workflows/pr-docker-check.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# 快速检查: 所有镜像的 amd64 版本
|
# Quick check: amd64 builds for all images
|
||||||
docker-build-amd64:
|
docker-build-amd64:
|
||||||
name: Build Check (amd64)
|
name: Build Check (amd64)
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- name: backend
|
- name: backend
|
||||||
dockerfile: ./docker/Dockerfile.backend
|
dockerfile: ./docker/Dockerfile.backend
|
||||||
test_run: true # 需要测试运行
|
test_run: true # Needs test run
|
||||||
- name: frontend
|
- name: frontend
|
||||||
dockerfile: ./docker/Dockerfile.frontend
|
dockerfile: ./docker/Dockerfile.frontend
|
||||||
test_run: true
|
test_run: true
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
file: ${{ matrix.dockerfile }}
|
file: ${{ matrix.dockerfile }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
load: true # 加载到本地 Docker,用于测试运行
|
load: true # Load into local Docker for test run
|
||||||
tags: nofx-${{ matrix.name }}:pr-test
|
tags: nofx-${{ matrix.name }}:pr-test
|
||||||
cache-from: type=gha,scope=${{ matrix.name }}-amd64
|
cache-from: type=gha,scope=${{ matrix.name }}-amd64
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.name }}-amd64
|
cache-to: type=gha,mode=max,scope=${{ matrix.name }}-amd64
|
||||||
@@ -66,12 +66,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "🧪 Testing container startup..."
|
echo "🧪 Testing container startup..."
|
||||||
|
|
||||||
# 启动容器
|
# Start container
|
||||||
docker run -d --name test-${{ matrix.name }} \
|
docker run -d --name test-${{ matrix.name }} \
|
||||||
--health-cmd="exit 0" \
|
--health-cmd="exit 0" \
|
||||||
nofx-${{ matrix.name }}:pr-test
|
nofx-${{ matrix.name }}:pr-test
|
||||||
|
|
||||||
# 等待容器启动 (最多 30 秒)
|
# Wait for container to start (up to 30 seconds)
|
||||||
for i in {1..30}; do
|
for i in {1..30}; do
|
||||||
if docker ps | grep -q test-${{ matrix.name }}; then
|
if docker ps | grep -q test-${{ matrix.name }}; then
|
||||||
echo "✅ Container started successfully"
|
echo "✅ Container started successfully"
|
||||||
@@ -93,7 +93,7 @@ jobs:
|
|||||||
|
|
||||||
echo "📦 Image size: ${SIZE_MB} MB"
|
echo "📦 Image size: ${SIZE_MB} MB"
|
||||||
|
|
||||||
# 警告阈值
|
# Warning thresholds
|
||||||
if [ "${{ matrix.name }}" = "backend" ] && [ $SIZE_MB -gt 500 ]; then
|
if [ "${{ matrix.name }}" = "backend" ] && [ $SIZE_MB -gt 500 ]; then
|
||||||
echo "⚠️ Warning: Backend image is larger than 500MB"
|
echo "⚠️ Warning: Backend image is larger than 500MB"
|
||||||
elif [ "${{ matrix.name }}" = "frontend" ] && [ $SIZE_MB -gt 200 ]; then
|
elif [ "${{ matrix.name }}" = "frontend" ] && [ $SIZE_MB -gt 200 ]; then
|
||||||
@@ -102,10 +102,10 @@ jobs:
|
|||||||
echo "✅ Image size is reasonable"
|
echo "✅ Image size is reasonable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ARM64 原生构建检查: 使用 GitHub 原生 ARM64 runner (快速!)
|
# ARM64 native build check: Uses GitHub native ARM64 runner (fast!)
|
||||||
docker-build-arm64-native:
|
docker-build-arm64-native:
|
||||||
name: Build Check (arm64 native - backend)
|
name: Build Check (arm64 native - backend)
|
||||||
runs-on: ubuntu-22.04-arm # 原生 ARM64 runner
|
runs-on: ubuntu-22.04-arm # Native ARM64 runner
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@@ -113,19 +113,19 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# 原生 ARM64 不需要 QEMU,直接构建
|
# Native ARM64 does not need QEMU, builds directly
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build backend image (arm64 native)
|
- name: Build backend image (arm64 native)
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
timeout-minutes: 15 # 原生构建更快!
|
timeout-minutes: 15 # Native builds are faster!
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile.backend
|
file: ./docker/Dockerfile.backend
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
push: false
|
push: false
|
||||||
load: true # 加载到本地,用于测试
|
load: true # Load locally for testing
|
||||||
tags: nofx-backend:pr-test-arm64
|
tags: nofx-backend:pr-test-arm64
|
||||||
cache-from: type=gha,scope=backend-arm64
|
cache-from: type=gha,scope=backend-arm64
|
||||||
cache-to: type=gha,mode=max,scope=backend-arm64
|
cache-to: type=gha,mode=max,scope=backend-arm64
|
||||||
@@ -139,12 +139,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "🧪 Testing ARM64 container startup..."
|
echo "🧪 Testing ARM64 container startup..."
|
||||||
|
|
||||||
# 启动容器
|
# Start container
|
||||||
docker run -d --name test-backend-arm64 \
|
docker run -d --name test-backend-arm64 \
|
||||||
--health-cmd="exit 0" \
|
--health-cmd="exit 0" \
|
||||||
nofx-backend:pr-test-arm64
|
nofx-backend:pr-test-arm64
|
||||||
|
|
||||||
# 等待启动
|
# Wait for startup
|
||||||
for i in {1..30}; do
|
for i in {1..30}; do
|
||||||
if docker ps | grep -q test-backend-arm64; then
|
if docker ps | grep -q test-backend-arm64; then
|
||||||
echo "✅ ARM64 container started successfully"
|
echo "✅ ARM64 container started successfully"
|
||||||
@@ -165,14 +165,14 @@ jobs:
|
|||||||
echo "Using GitHub native ARM64 runner - no QEMU needed!"
|
echo "Using GitHub native ARM64 runner - no QEMU needed!"
|
||||||
echo "Build time is ~3x faster than emulation"
|
echo "Build time is ~3x faster than emulation"
|
||||||
|
|
||||||
# 汇总检查结果
|
# Aggregate check results
|
||||||
check-summary:
|
check-summary:
|
||||||
name: Docker Build Summary
|
name: Docker Build Summary
|
||||||
needs: [docker-build-amd64, docker-build-arm64-native]
|
needs: [docker-build-amd64, docker-build-arm64-native]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
if: always()
|
if: always()
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write # 用于发布评论
|
pull-requests: write # For posting comments
|
||||||
steps:
|
steps:
|
||||||
- name: Check build results
|
- name: Check build results
|
||||||
id: check
|
id: check
|
||||||
@@ -180,7 +180,7 @@ jobs:
|
|||||||
echo "## 🐳 Docker Build Check Results" >> $GITHUB_STEP_SUMMARY
|
echo "## 🐳 Docker Build Check Results" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# 检查 amd64 构建
|
# Check amd64 build
|
||||||
if [[ "${{ needs.docker-build-amd64.result }}" == "success" ]]; then
|
if [[ "${{ needs.docker-build-amd64.result }}" == "success" ]]; then
|
||||||
echo "✅ **AMD64 builds**: All passed" >> $GITHUB_STEP_SUMMARY
|
echo "✅ **AMD64 builds**: All passed" >> $GITHUB_STEP_SUMMARY
|
||||||
AMD64_OK=true
|
AMD64_OK=true
|
||||||
@@ -189,7 +189,7 @@ jobs:
|
|||||||
AMD64_OK=false
|
AMD64_OK=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查 arm64 构建
|
# Check arm64 build
|
||||||
if [[ "${{ needs.docker-build-arm64-native.result }}" == "success" ]]; then
|
if [[ "${{ needs.docker-build-arm64-native.result }}" == "success" ]]; then
|
||||||
echo "✅ **ARM64 build** (native): Backend passed (frontend will be verified after merge)" >> $GITHUB_STEP_SUMMARY
|
echo "✅ **ARM64 build** (native): Backend passed (frontend will be verified after merge)" >> $GITHUB_STEP_SUMMARY
|
||||||
ARM64_OK=true
|
ARM64_OK=true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: PR Docker Compose Healthcheck
|
name: PR Docker Compose Healthcheck
|
||||||
|
|
||||||
# 驗證 docker-compose.yml 的 healthcheck 配置在 Alpine 容器中正常工作
|
# Verify docker-compose.yml healthcheck config works correctly in Alpine containers
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
|
|||||||
+10
-10
@@ -1,37 +1,37 @@
|
|||||||
# Railway All-in-One: 复用现有 GHCR 镜像
|
# Railway All-in-One: Reuse existing GHCR images
|
||||||
# 从现有镜像提取内容,合并到一个容器
|
# Extract content from existing images and merge into a single container
|
||||||
|
|
||||||
# 从后端镜像提取二进制
|
# Extract binary from backend image
|
||||||
FROM ghcr.io/nofxaios/nofx/nofx-backend:latest AS backend
|
FROM ghcr.io/nofxaios/nofx/nofx-backend:latest AS backend
|
||||||
|
|
||||||
# 从前端镜像提取静态文件
|
# Extract static files from frontend image
|
||||||
FROM ghcr.io/nofxaios/nofx/nofx-frontend:latest AS frontend
|
FROM ghcr.io/nofxaios/nofx/nofx-frontend:latest AS frontend
|
||||||
|
|
||||||
# 最终镜像
|
# Final image
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates tzdata sqlite nginx openssl gettext
|
RUN apk add --no-cache ca-certificates tzdata sqlite nginx openssl gettext
|
||||||
|
|
||||||
# 复制后端二进制
|
# Copy backend binary
|
||||||
COPY --from=backend /app/nofx /app/nofx
|
COPY --from=backend /app/nofx /app/nofx
|
||||||
|
|
||||||
# 复制 TA-Lib 库
|
# Copy TA-Lib libraries
|
||||||
COPY --from=backend /usr/local/lib/libta_lib* /usr/local/lib/
|
COPY --from=backend /usr/local/lib/libta_lib* /usr/local/lib/
|
||||||
RUN ldconfig /usr/local/lib 2>/dev/null || true
|
RUN ldconfig /usr/local/lib 2>/dev/null || true
|
||||||
|
|
||||||
# 复制前端静态文件
|
# Copy frontend static files
|
||||||
COPY --from=frontend /usr/share/nginx/html /usr/share/nginx/html
|
COPY --from=frontend /usr/share/nginx/html /usr/share/nginx/html
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN mkdir -p /app/data
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
# 启动脚本(包含 nginx 配置生成)
|
# Startup script (includes nginx config generation)
|
||||||
COPY railway/start.sh /app/start.sh
|
COPY railway/start.sh /app/start.sh
|
||||||
RUN chmod +x /app/start.sh
|
RUN chmod +x /app/start.sh
|
||||||
|
|
||||||
ENV DB_PATH=/app/data/data.db
|
ENV DB_PATH=/app/data/data.db
|
||||||
|
|
||||||
# Railway 会自动设置 PORT 环境变量
|
# Railway automatically sets the PORT environment variable
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert coinank kline format to market.Kline format
|
// Convert coinank kline format to market.Kline format
|
||||||
// Coinank: Volume = BTC 数量, Quantity = USDT 成交额
|
// Coinank: Volume = BTC quantity, Quantity = USDT turnover
|
||||||
klines := make([]market.Kline, len(coinankKlines))
|
klines := make([]market.Kline, len(coinankKlines))
|
||||||
for i, ck := range coinankKlines {
|
for i, ck := range coinankKlines {
|
||||||
klines[i] = market.Kline{
|
klines[i] = market.Kline{
|
||||||
@@ -196,8 +196,8 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i
|
|||||||
High: ck.High,
|
High: ck.High,
|
||||||
Low: ck.Low,
|
Low: ck.Low,
|
||||||
Close: ck.Close,
|
Close: ck.Close,
|
||||||
Volume: ck.Volume, // BTC 数量
|
Volume: ck.Volume, // BTC quantity
|
||||||
QuoteVolume: ck.Quantity, // USDT 成交额
|
QuoteVolume: ck.Quantity, // USDT turnover
|
||||||
CloseTime: ck.EndTime,
|
CloseTime: ck.EndTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,8 +229,8 @@ func (s *Server) getKlinesFromAlpaca(symbol, interval string, limit int) ([]mark
|
|||||||
High: bar.High,
|
High: bar.High,
|
||||||
Low: bar.Low,
|
Low: bar.Low,
|
||||||
Close: bar.Close,
|
Close: bar.Close,
|
||||||
Volume: float64(bar.Volume), // 股数
|
Volume: float64(bar.Volume), // share count
|
||||||
QuoteVolume: float64(bar.Volume) * bar.Close, // 成交额 = 股数 * 收盘价 (USD)
|
QuoteVolume: float64(bar.Volume) * bar.Close, // turnover = shares * close price (USD)
|
||||||
CloseTime: bar.Timestamp.UnixMilli(),
|
CloseTime: bar.Timestamp.UnixMilli(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,8 +311,8 @@ func (s *Server) getKlinesFromHyperliquid(symbol, interval string, limit int) ([
|
|||||||
High: high,
|
High: high,
|
||||||
Low: low,
|
Low: low,
|
||||||
Close: close,
|
Close: close,
|
||||||
Volume: volume, // 合约数量
|
Volume: volume, // contract quantity
|
||||||
QuoteVolume: volume * close, // 成交额 (USD)
|
QuoteVolume: volume * close, // turnover (USD)
|
||||||
CloseTime: candle.CloseTime,
|
CloseTime: candle.CloseTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -195,7 +195,7 @@ func (cfg *BacktestConfig) ToStrategyConfig() *store.StrategyConfig {
|
|||||||
if cfg.loadedStrategy != nil {
|
if cfg.loadedStrategy != nil {
|
||||||
result := *cfg.loadedStrategy // Make a copy
|
result := *cfg.loadedStrategy // Make a copy
|
||||||
|
|
||||||
// Override coin source with backtest symbols (回测指定的币对优先)
|
// Override coin source with backtest symbols (backtest-specified pairs take priority)
|
||||||
if len(cfg.Symbols) > 0 {
|
if len(cfg.Symbols) > 0 {
|
||||||
result.CoinSource.SourceType = "static"
|
result.CoinSource.SourceType = "static"
|
||||||
result.CoinSource.StaticCoins = cfg.Symbols
|
result.CoinSource.StaticCoins = cfg.Symbols
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ services:
|
|||||||
dockerfile: ./docker/Dockerfile.backend
|
dockerfile: ./docker/Dockerfile.backend
|
||||||
container_name: nofx-trading
|
container_name: nofx-trading
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
stop_grace_period: 30s # 允许应用有 30 秒时间优雅关闭
|
stop_grace_period: 30s # Allow the app 30 seconds for graceful shutdown
|
||||||
ports:
|
ports:
|
||||||
- "${NOFX_BACKEND_PORT:-8080}:8080"
|
- "${NOFX_BACKEND_PORT:-8080}:8080"
|
||||||
- "6060:6060" # pprof profiling
|
- "6060:6060" # pprof profiling
|
||||||
|
|||||||
+6
-6
@@ -247,7 +247,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
return e.filterExcludedCoins(candidates), nil
|
return e.filterExcludedCoins(candidates), nil
|
||||||
|
|
||||||
case "ai500":
|
case "ai500":
|
||||||
// 检查 use_ai500 标志,如果为 false 则回退到静态币种
|
// Check use_ai500 flag; if false, fall back to static coins
|
||||||
if !coinSource.UseAI500 {
|
if !coinSource.UseAI500 {
|
||||||
logger.Infof("⚠️ source_type is 'ai500' but use_ai500 is false, falling back to static coins")
|
logger.Infof("⚠️ source_type is 'ai500' but use_ai500 is false, falling back to static coins")
|
||||||
for _, symbol := range coinSource.StaticCoins {
|
for _, symbol := range coinSource.StaticCoins {
|
||||||
@@ -263,11 +263,11 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// 空列表是正常情况,直接返回
|
// Empty list is a normal condition, return directly
|
||||||
return e.filterExcludedCoins(coins), nil
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
case "oi_top":
|
case "oi_top":
|
||||||
// 检查 use_oi_top 标志,如果为 false 则回退到静态币种
|
// Check use_oi_top flag; if false, fall back to static coins
|
||||||
if !coinSource.UseOITop {
|
if !coinSource.UseOITop {
|
||||||
logger.Infof("⚠️ source_type is 'oi_top' but use_oi_top is false, falling back to static coins")
|
logger.Infof("⚠️ source_type is 'oi_top' but use_oi_top is false, falling back to static coins")
|
||||||
for _, symbol := range coinSource.StaticCoins {
|
for _, symbol := range coinSource.StaticCoins {
|
||||||
@@ -283,11 +283,11 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// 空列表是正常情况,直接返回
|
// Empty list is a normal condition, return directly
|
||||||
return e.filterExcludedCoins(coins), nil
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
case "oi_low":
|
case "oi_low":
|
||||||
// 持仓减少榜,适合做空
|
// OI decrease ranking, suitable for short positions
|
||||||
if !coinSource.UseOILow {
|
if !coinSource.UseOILow {
|
||||||
logger.Infof("⚠️ source_type is 'oi_low' but use_oi_low is false, falling back to static coins")
|
logger.Infof("⚠️ source_type is 'oi_low' but use_oi_low is false, falling back to static coins")
|
||||||
for _, symbol := range coinSource.StaticCoins {
|
for _, symbol := range coinSource.StaticCoins {
|
||||||
@@ -303,7 +303,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// 空列表是正常情况,直接返回
|
// Empty list is a normal condition, return directly
|
||||||
return e.filterExcludedCoins(coins), nil
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
case "hyper_all":
|
case "hyper_all":
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ func (e *StrategyEngine) formatPositionInfo(index int, pos PositionInfo, ctx *Co
|
|||||||
|
|
||||||
func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
|
func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
|
||||||
if len(sources) > 1 {
|
if len(sources) > 1 {
|
||||||
// 多信号源组合
|
// Multiple signal source combination
|
||||||
hasAI500 := false
|
hasAI500 := false
|
||||||
hasOITop := false
|
hasOITop := false
|
||||||
hasOILow := false
|
hasOILow := false
|
||||||
@@ -482,9 +482,9 @@ func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
|
|||||||
case "ai500":
|
case "ai500":
|
||||||
return " (AI500)"
|
return " (AI500)"
|
||||||
case "oi_top":
|
case "oi_top":
|
||||||
return " (OI_Top 持仓增加)"
|
return " (OI_Top OI increase)"
|
||||||
case "oi_low":
|
case "oi_low":
|
||||||
return " (OI_Low 持仓减少)"
|
return " (OI_Low OI decrease)"
|
||||||
case "static":
|
case "static":
|
||||||
return " (Manual selection)"
|
return " (Manual selection)"
|
||||||
case "hyper_all":
|
case "hyper_all":
|
||||||
@@ -504,7 +504,7 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string {
|
|||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
indicators := e.config.Indicators
|
indicators := e.config.Indicators
|
||||||
|
|
||||||
// 明确标注币种
|
// Clearly label the coin symbol
|
||||||
sb.WriteString(fmt.Sprintf("=== %s Market Data ===\n\n", data.Symbol))
|
sb.WriteString(fmt.Sprintf("=== %s Market Data ===\n\n", data.Symbol))
|
||||||
sb.WriteString(fmt.Sprintf("current_price = %.4f", data.CurrentPrice))
|
sb.WriteString(fmt.Sprintf("current_price = %.4f", data.CurrentPrice))
|
||||||
|
|
||||||
|
|||||||
+49
-48
@@ -10,49 +10,50 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// AI Data Formatter - AI数据格式化器
|
// AI Data Formatter
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 将交易上下文转换为AI友好的格式,确保AI能够100%理解数据
|
// Converts trading context into AI-friendly format, ensuring AI fully
|
||||||
|
// understands the data regardless of language.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// FormatContextForAI 将交易上下文格式化为AI可理解的文本(包含Schema)
|
// FormatContextForAI formats trading context into AI-readable text (including schema)
|
||||||
func FormatContextForAI(ctx *Context, lang Language) string {
|
func FormatContextForAI(ctx *Context, lang Language) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
// 1. 添加Schema说明(让AI理解数据格式)
|
// 1. Add schema description (so AI understands data format)
|
||||||
sb.WriteString(GetSchemaPrompt(lang))
|
sb.WriteString(GetSchemaPrompt(lang))
|
||||||
sb.WriteString("\n---\n\n")
|
sb.WriteString("\n---\n\n")
|
||||||
|
|
||||||
// 2. 当前状态概览
|
// 2. Current state overview
|
||||||
sb.WriteString(formatContextData(ctx, lang))
|
sb.WriteString(formatContextData(ctx, lang))
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatContextDataOnly 仅格式化上下文数据,不包含Schema(用于已有Schema的场景)
|
// FormatContextDataOnly formats context data only, without schema (for use when schema is already present)
|
||||||
func FormatContextDataOnly(ctx *Context, lang Language) string {
|
func FormatContextDataOnly(ctx *Context, lang Language) string {
|
||||||
return formatContextData(ctx, lang)
|
return formatContextData(ctx, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatContextData 格式化核心数据部分
|
// formatContextData formats the core data section
|
||||||
func formatContextData(ctx *Context, lang Language) string {
|
func formatContextData(ctx *Context, lang Language) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
// 1. 当前状态概览
|
// 1. Current state overview
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
sb.WriteString(formatHeaderZH(ctx))
|
sb.WriteString(formatHeaderZH(ctx))
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString(formatHeaderEN(ctx))
|
sb.WriteString(formatHeaderEN(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 账户信息
|
// 3. Account information
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
sb.WriteString(formatAccountZH(ctx))
|
sb.WriteString(formatAccountZH(ctx))
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString(formatAccountEN(ctx))
|
sb.WriteString(formatAccountEN(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 历史交易统计
|
// 4. Historical trading statistics
|
||||||
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
|
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
sb.WriteString(formatTradingStatsZH(ctx.TradingStats))
|
sb.WriteString(formatTradingStatsZH(ctx.TradingStats))
|
||||||
@@ -61,7 +62,7 @@ func formatContextData(ctx *Context, lang Language) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 最近交易记录
|
// 5. Recent trade records
|
||||||
if len(ctx.RecentOrders) > 0 {
|
if len(ctx.RecentOrders) > 0 {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
sb.WriteString(formatRecentTradesZH(ctx.RecentOrders))
|
sb.WriteString(formatRecentTradesZH(ctx.RecentOrders))
|
||||||
@@ -70,7 +71,7 @@ func formatContextData(ctx *Context, lang Language) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 当前持仓
|
// 5. Current positions
|
||||||
if len(ctx.Positions) > 0 {
|
if len(ctx.Positions) > 0 {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
sb.WriteString(formatCurrentPositionsZH(ctx))
|
sb.WriteString(formatCurrentPositionsZH(ctx))
|
||||||
@@ -79,7 +80,7 @@ func formatContextData(ctx *Context, lang Language) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 候选币种(带市场数据)
|
// 6. Candidate coins (with market data)
|
||||||
if len(ctx.CandidateCoins) > 0 {
|
if len(ctx.CandidateCoins) > 0 {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
sb.WriteString(formatCandidateCoinsZH(ctx))
|
sb.WriteString(formatCandidateCoinsZH(ctx))
|
||||||
@@ -88,7 +89,7 @@ func formatContextData(ctx *Context, lang Language) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. OI排名数据(如果有)
|
// 7. OI ranking data (if available)
|
||||||
if ctx.OIRankingData != nil {
|
if ctx.OIRankingData != nil {
|
||||||
nofxosLang := nofxos.LangEnglish
|
nofxosLang := nofxos.LangEnglish
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
@@ -100,15 +101,15 @@ func formatContextData(ctx *Context, lang Language) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 中文格式化函数 ==========
|
// ========== Chinese Formatting Functions ==========
|
||||||
|
|
||||||
// formatHeaderZH 格式化头部信息(中文)
|
// formatHeaderZH formats header information (Chinese)
|
||||||
func formatHeaderZH(ctx *Context) string {
|
func formatHeaderZH(ctx *Context) string {
|
||||||
return fmt.Sprintf("# 📊 交易决策请求\n\n时间: %s | 周期: #%d | 运行时长: %d 分钟\n\n",
|
return fmt.Sprintf("# 📊 交易决策请求\n\n时间: %s | 周期: #%d | 运行时长: %d 分钟\n\n",
|
||||||
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
|
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatAccountZH 格式化账户信息(中文)
|
// formatAccountZH formats account information (Chinese)
|
||||||
func formatAccountZH(ctx *Context) string {
|
func formatAccountZH(ctx *Context) string {
|
||||||
acc := ctx.Account
|
acc := ctx.Account
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
@@ -120,7 +121,7 @@ func formatAccountZH(ctx *Context) string {
|
|||||||
sb.WriteString(fmt.Sprintf("保证金使用率: %.1f%% | ", acc.MarginUsedPct))
|
sb.WriteString(fmt.Sprintf("保证金使用率: %.1f%% | ", acc.MarginUsedPct))
|
||||||
sb.WriteString(fmt.Sprintf("持仓数: %d\n\n", acc.PositionCount))
|
sb.WriteString(fmt.Sprintf("持仓数: %d\n\n", acc.PositionCount))
|
||||||
|
|
||||||
// 添加风险提示
|
// Add risk warnings
|
||||||
if acc.MarginUsedPct > 70 {
|
if acc.MarginUsedPct > 70 {
|
||||||
sb.WriteString("⚠️ **风险警告**: 保证金使用率 > 70%,处于高风险状态!\n\n")
|
sb.WriteString("⚠️ **风险警告**: 保证金使用率 > 70%,处于高风险状态!\n\n")
|
||||||
} else if acc.MarginUsedPct > 50 {
|
} else if acc.MarginUsedPct > 50 {
|
||||||
@@ -130,25 +131,25 @@ func formatAccountZH(ctx *Context) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatTradingStatsZH 格式化历史交易统计(中文)
|
// formatTradingStatsZH formats historical trading statistics (Chinese)
|
||||||
func formatTradingStatsZH(stats *TradingStats) string {
|
func formatTradingStatsZH(stats *TradingStats) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## 历史交易统计\n\n")
|
sb.WriteString("## 历史交易统计\n\n")
|
||||||
|
|
||||||
// 盈亏比计算
|
// Win/loss ratio calculation
|
||||||
var winLossRatio float64
|
var winLossRatio float64
|
||||||
if stats.AvgLoss > 0 {
|
if stats.AvgLoss > 0 {
|
||||||
winLossRatio = stats.AvgWin / stats.AvgLoss
|
winLossRatio = stats.AvgWin / stats.AvgLoss
|
||||||
}
|
}
|
||||||
|
|
||||||
// 指标定义说明(去掉胜率,聚焦核心指标)
|
// Metric definitions (focusing on core metrics, excluding win rate)
|
||||||
sb.WriteString("**指标说明**:\n")
|
sb.WriteString("**指标说明**:\n")
|
||||||
sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利,>1.5为良好,>2为优秀)\n")
|
sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利,>1.5为良好,>2为优秀)\n")
|
||||||
sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好,>2优秀)\n")
|
sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好,>2优秀)\n")
|
||||||
sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀)\n")
|
sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀)\n")
|
||||||
sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n")
|
sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n")
|
||||||
|
|
||||||
// 数据值
|
// Data values
|
||||||
sb.WriteString("**当前数据**:\n")
|
sb.WriteString("**当前数据**:\n")
|
||||||
sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades))
|
sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades))
|
||||||
sb.WriteString(fmt.Sprintf("- 盈利因子: %.2f\n", stats.ProfitFactor))
|
sb.WriteString(fmt.Sprintf("- 盈利因子: %.2f\n", stats.ProfitFactor))
|
||||||
@@ -159,10 +160,10 @@ func formatTradingStatsZH(stats *TradingStats) string {
|
|||||||
sb.WriteString(fmt.Sprintf("- 平均亏损: -%.2f USDT\n", stats.AvgLoss))
|
sb.WriteString(fmt.Sprintf("- 平均亏损: -%.2f USDT\n", stats.AvgLoss))
|
||||||
sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct))
|
sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct))
|
||||||
|
|
||||||
// 综合分析和决策建议
|
// Comprehensive analysis and decision guidance
|
||||||
sb.WriteString("**决策参考**:\n")
|
sb.WriteString("**决策参考**:\n")
|
||||||
|
|
||||||
// 根据统计数据给出具体建议
|
// Provide specific recommendations based on statistics
|
||||||
if stats.TotalTrades < 10 {
|
if stats.TotalTrades < 10 {
|
||||||
sb.WriteString("- 样本量较小(<10笔),统计结果参考意义有限\n")
|
sb.WriteString("- 样本量较小(<10笔),统计结果参考意义有限\n")
|
||||||
}
|
}
|
||||||
@@ -191,13 +192,13 @@ func formatTradingStatsZH(stats *TradingStats) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatRecentTradesZH 格式化最近交易(中文)
|
// formatRecentTradesZH formats recent trades (Chinese)
|
||||||
func formatRecentTradesZH(orders []RecentOrder) string {
|
func formatRecentTradesZH(orders []RecentOrder) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## 最近完成的交易\n\n")
|
sb.WriteString("## 最近完成的交易\n\n")
|
||||||
|
|
||||||
for i, order := range orders {
|
for i, order := range orders {
|
||||||
// 判断盈亏
|
// Determine profit or loss
|
||||||
profitOrLoss := "盈利"
|
profitOrLoss := "盈利"
|
||||||
if order.RealizedPnL < 0 {
|
if order.RealizedPnL < 0 {
|
||||||
profitOrLoss = "亏损"
|
profitOrLoss = "亏损"
|
||||||
@@ -222,13 +223,13 @@ func formatRecentTradesZH(orders []RecentOrder) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatCurrentPositionsZH 格式化当前持仓(中文)
|
// formatCurrentPositionsZH formats current positions (Chinese)
|
||||||
func formatCurrentPositionsZH(ctx *Context) string {
|
func formatCurrentPositionsZH(ctx *Context) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## 当前持仓\n\n")
|
sb.WriteString("## 当前持仓\n\n")
|
||||||
|
|
||||||
for i, pos := range ctx.Positions {
|
for i, pos := range ctx.Positions {
|
||||||
// 计算回撤
|
// Calculate drawdown
|
||||||
drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct
|
drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct
|
||||||
|
|
||||||
sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
|
sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
|
||||||
@@ -242,7 +243,7 @@ func formatCurrentPositionsZH(ctx *Context) string {
|
|||||||
sb.WriteString(fmt.Sprintf("保证金 %.0f USDT | ", pos.MarginUsed))
|
sb.WriteString(fmt.Sprintf("保证金 %.0f USDT | ", pos.MarginUsed))
|
||||||
sb.WriteString(fmt.Sprintf("强平价 %.4f\n", pos.LiquidationPrice))
|
sb.WriteString(fmt.Sprintf("强平价 %.4f\n", pos.LiquidationPrice))
|
||||||
|
|
||||||
// 添加分析提示
|
// Add analysis hints
|
||||||
if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 {
|
if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 {
|
||||||
sb.WriteString(fmt.Sprintf(" ⚠️ **止盈提示**: 当前盈亏从峰值 %.2f%% 回撤到 %.2f%%,回撤幅度 %.2f%%,建议考虑止盈\n",
|
sb.WriteString(fmt.Sprintf(" ⚠️ **止盈提示**: 当前盈亏从峰值 %.2f%% 回撤到 %.2f%%,回撤幅度 %.2f%%,建议考虑止盈\n",
|
||||||
pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100))
|
pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100))
|
||||||
@@ -252,7 +253,7 @@ func formatCurrentPositionsZH(ctx *Context) string {
|
|||||||
sb.WriteString(" ⚠️ **止损提示**: 亏损接近-5%止损线,建议考虑止损\n")
|
sb.WriteString(" ⚠️ **止损提示**: 亏损接近-5%止损线,建议考虑止损\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示当前价格(如果有市场数据)
|
// Show current price (if market data available)
|
||||||
if ctx.MarketDataMap != nil {
|
if ctx.MarketDataMap != nil {
|
||||||
if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok {
|
if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok {
|
||||||
sb.WriteString(fmt.Sprintf(" 📈 当前价格: %.4f\n", mdata.CurrentPrice))
|
sb.WriteString(fmt.Sprintf(" 📈 当前价格: %.4f\n", mdata.CurrentPrice))
|
||||||
@@ -265,7 +266,7 @@ func formatCurrentPositionsZH(ctx *Context) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatCandidateCoinsZH 格式化候选币种(中文)
|
// formatCandidateCoinsZH formats candidate coins (Chinese)
|
||||||
func formatCandidateCoinsZH(ctx *Context) string {
|
func formatCandidateCoinsZH(ctx *Context) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## 候选币种\n\n")
|
sb.WriteString("## 候选币种\n\n")
|
||||||
@@ -273,19 +274,19 @@ func formatCandidateCoinsZH(ctx *Context) string {
|
|||||||
for i, coin := range ctx.CandidateCoins {
|
for i, coin := range ctx.CandidateCoins {
|
||||||
sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol))
|
sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol))
|
||||||
|
|
||||||
// 当前价格
|
// Current price
|
||||||
if ctx.MarketDataMap != nil {
|
if ctx.MarketDataMap != nil {
|
||||||
if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok {
|
if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok {
|
||||||
sb.WriteString(fmt.Sprintf("当前价格: %.4f\n\n", mdata.CurrentPrice))
|
sb.WriteString(fmt.Sprintf("当前价格: %.4f\n\n", mdata.CurrentPrice))
|
||||||
|
|
||||||
// K线数据(多时间框架)
|
// Kline data (multi-timeframe)
|
||||||
if mdata.TimeframeData != nil {
|
if mdata.TimeframeData != nil {
|
||||||
sb.WriteString(formatKlineDataZH(coin.Symbol, mdata.TimeframeData, ctx.Timeframes))
|
sb.WriteString(formatKlineDataZH(coin.Symbol, mdata.TimeframeData, ctx.Timeframes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OI数据(如果有)
|
// OI data (if available)
|
||||||
if ctx.OITopDataMap != nil {
|
if ctx.OITopDataMap != nil {
|
||||||
if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok {
|
if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok {
|
||||||
sb.WriteString(fmt.Sprintf("**持仓量变化**: OI排名 #%d | 变化 %+.2f%% (%+.2fM USDT) | 价格变化 %+.2f%%\n\n",
|
sb.WriteString(fmt.Sprintf("**持仓量变化**: OI排名 #%d | 变化 %+.2f%% (%+.2fM USDT) | 价格变化 %+.2f%%\n\n",
|
||||||
@@ -295,7 +296,7 @@ func formatCandidateCoinsZH(ctx *Context) string {
|
|||||||
oiData.PriceDeltaPercent,
|
oiData.PriceDeltaPercent,
|
||||||
))
|
))
|
||||||
|
|
||||||
// OI解读
|
// OI interpretation
|
||||||
oiChange := "增加"
|
oiChange := "增加"
|
||||||
if oiData.OIDeltaPercent < 0 {
|
if oiData.OIDeltaPercent < 0 {
|
||||||
oiChange = "减少"
|
oiChange = "减少"
|
||||||
@@ -314,7 +315,7 @@ func formatCandidateCoinsZH(ctx *Context) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatKlineDataZH 格式化K线数据(中文)
|
// formatKlineDataZH formats kline data (Chinese)
|
||||||
func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
|
func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
@@ -324,7 +325,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD
|
|||||||
sb.WriteString("```\n")
|
sb.WriteString("```\n")
|
||||||
sb.WriteString("时间(UTC) 开盘 最高 最低 收盘 成交量\n")
|
sb.WriteString("时间(UTC) 开盘 最高 最低 收盘 成交量\n")
|
||||||
|
|
||||||
// 只显示最近30根K线
|
// Only show the latest 30 klines
|
||||||
startIdx := 0
|
startIdx := 0
|
||||||
if len(data.Klines) > 30 {
|
if len(data.Klines) > 30 {
|
||||||
startIdx = len(data.Klines) - 30
|
startIdx = len(data.Klines) - 30
|
||||||
@@ -343,7 +344,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记最后一根K线
|
// Mark the last kline
|
||||||
if len(data.Klines) > 0 {
|
if len(data.Klines) > 0 {
|
||||||
sb.WriteString(" <- 当前\n")
|
sb.WriteString(" <- 当前\n")
|
||||||
}
|
}
|
||||||
@@ -356,7 +357,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// getOIInterpretationZH 获取OI变化解读(中文)
|
// getOIInterpretationZH returns OI change interpretation (Chinese)
|
||||||
func getOIInterpretationZH(oiChange, priceChange string) string {
|
func getOIInterpretationZH(oiChange, priceChange string) string {
|
||||||
if oiChange == "增加" && priceChange == "上涨" {
|
if oiChange == "增加" && priceChange == "上涨" {
|
||||||
return OIInterpretation.OIUp_PriceUp.ZH
|
return OIInterpretation.OIUp_PriceUp.ZH
|
||||||
@@ -369,15 +370,15 @@ func getOIInterpretationZH(oiChange, priceChange string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 英文格式化函数 ==========
|
// ========== English Formatting Functions ==========
|
||||||
|
|
||||||
// formatHeaderEN 格式化头部信息(英文)
|
// formatHeaderEN formats header information (English)
|
||||||
func formatHeaderEN(ctx *Context) string {
|
func formatHeaderEN(ctx *Context) string {
|
||||||
return fmt.Sprintf("# 📊 Trading Decision Request\n\nTime: %s | Period: #%d | Runtime: %d minutes\n\n",
|
return fmt.Sprintf("# 📊 Trading Decision Request\n\nTime: %s | Period: #%d | Runtime: %d minutes\n\n",
|
||||||
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
|
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatAccountEN 格式化账户信息(英文)
|
// formatAccountEN formats account information (English)
|
||||||
func formatAccountEN(ctx *Context) string {
|
func formatAccountEN(ctx *Context) string {
|
||||||
acc := ctx.Account
|
acc := ctx.Account
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
@@ -399,7 +400,7 @@ func formatAccountEN(ctx *Context) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatTradingStatsEN 格式化历史交易统计(英文)
|
// formatTradingStatsEN formats historical trading statistics (English)
|
||||||
func formatTradingStatsEN(stats *TradingStats) string {
|
func formatTradingStatsEN(stats *TradingStats) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## Historical Trading Statistics\n\n")
|
sb.WriteString("## Historical Trading Statistics\n\n")
|
||||||
@@ -460,7 +461,7 @@ func formatTradingStatsEN(stats *TradingStats) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatRecentTradesEN 格式化最近交易(英文)
|
// formatRecentTradesEN formats recent trades (English)
|
||||||
func formatRecentTradesEN(orders []RecentOrder) string {
|
func formatRecentTradesEN(orders []RecentOrder) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## Recent Completed Trades\n\n")
|
sb.WriteString("## Recent Completed Trades\n\n")
|
||||||
@@ -490,7 +491,7 @@ func formatRecentTradesEN(orders []RecentOrder) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatCurrentPositionsEN 格式化当前持仓(英文)
|
// formatCurrentPositionsEN formats current positions (English)
|
||||||
func formatCurrentPositionsEN(ctx *Context) string {
|
func formatCurrentPositionsEN(ctx *Context) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## Current Positions\n\n")
|
sb.WriteString("## Current Positions\n\n")
|
||||||
@@ -531,7 +532,7 @@ func formatCurrentPositionsEN(ctx *Context) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatCandidateCoinsEN 格式化候选币种(英文)
|
// formatCandidateCoinsEN formats candidate coins (English)
|
||||||
func formatCandidateCoinsEN(ctx *Context) string {
|
func formatCandidateCoinsEN(ctx *Context) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("## Candidate Coins\n\n")
|
sb.WriteString("## Candidate Coins\n\n")
|
||||||
@@ -576,7 +577,7 @@ func formatCandidateCoinsEN(ctx *Context) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatKlineDataEN 格式化K线数据(英文)
|
// formatKlineDataEN formats kline data (English)
|
||||||
func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
|
func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
@@ -621,7 +622,7 @@ func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesD
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// getOIInterpretationEN 获取OI变化解读(英文)
|
// getOIInterpretationEN returns OI change interpretation (English)
|
||||||
func getOIInterpretationEN(oiChange, priceChange string) string {
|
func getOIInterpretationEN(oiChange, priceChange string) string {
|
||||||
if oiChange == "increase" && priceChange == "up" {
|
if oiChange == "increase" && priceChange == "up" {
|
||||||
return OIInterpretation.OIUp_PriceUp.EN
|
return OIInterpretation.OIUp_PriceUp.EN
|
||||||
|
|||||||
+24
-24
@@ -6,22 +6,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// AI Prompt Builder - AI提示词构建器
|
// AI Prompt Builder
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 构建完整的AI提示词,包括系统提示词和用户提示词
|
// Builds complete AI prompts including system prompts and user prompts.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// PromptBuilder 提示词构建器
|
// PromptBuilder builds AI prompts in the configured language
|
||||||
type PromptBuilder struct {
|
type PromptBuilder struct {
|
||||||
lang Language
|
lang Language
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPromptBuilder 创建提示词构建器
|
// NewPromptBuilder creates a new prompt builder for the given language
|
||||||
func NewPromptBuilder(lang Language) *PromptBuilder {
|
func NewPromptBuilder(lang Language) *PromptBuilder {
|
||||||
return &PromptBuilder{lang: lang}
|
return &PromptBuilder{lang: lang}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildSystemPrompt 构建系统提示词
|
// BuildSystemPrompt builds the system prompt
|
||||||
func (pb *PromptBuilder) BuildSystemPrompt() string {
|
func (pb *PromptBuilder) BuildSystemPrompt() string {
|
||||||
if pb.lang == LangChinese {
|
if pb.lang == LangChinese {
|
||||||
return pb.buildSystemPromptZH()
|
return pb.buildSystemPromptZH()
|
||||||
@@ -29,19 +29,19 @@ func (pb *PromptBuilder) BuildSystemPrompt() string {
|
|||||||
return pb.buildSystemPromptEN()
|
return pb.buildSystemPromptEN()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildUserPrompt 构建用户提示词(包含完整的交易上下文)
|
// BuildUserPrompt builds the user prompt with full trading context
|
||||||
func (pb *PromptBuilder) BuildUserPrompt(ctx *Context) string {
|
func (pb *PromptBuilder) BuildUserPrompt(ctx *Context) string {
|
||||||
// 使用Formatter格式化交易上下文
|
// Use Formatter to format the trading context
|
||||||
formattedData := FormatContextForAI(ctx, pb.lang)
|
formattedData := FormatContextForAI(ctx, pb.lang)
|
||||||
|
|
||||||
// 添加决策要求
|
// Append decision requirements
|
||||||
if pb.lang == LangChinese {
|
if pb.lang == LangChinese {
|
||||||
return formattedData + pb.getDecisionRequirementsZH()
|
return formattedData + pb.getDecisionRequirementsZH()
|
||||||
}
|
}
|
||||||
return formattedData + pb.getDecisionRequirementsEN()
|
return formattedData + pb.getDecisionRequirementsEN()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 中文提示词 ==========
|
// ========== Chinese Prompts ==========
|
||||||
|
|
||||||
func (pb *PromptBuilder) buildSystemPromptZH() string {
|
func (pb *PromptBuilder) buildSystemPromptZH() string {
|
||||||
return `你是一个专业的量化交易AI助手,负责分析市场数据并做出交易决策。
|
return `你是一个专业的量化交易AI助手,负责分析市场数据并做出交易决策。
|
||||||
@@ -176,7 +176,7 @@ func (pb *PromptBuilder) getDecisionRequirementsZH() string {
|
|||||||
**请立即输出你的决策(JSON格式)**:`
|
**请立即输出你的决策(JSON格式)**:`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 英文提示词 ==========
|
// ========== English Prompts ==========
|
||||||
|
|
||||||
func (pb *PromptBuilder) buildSystemPromptEN() string {
|
func (pb *PromptBuilder) buildSystemPromptEN() string {
|
||||||
return `You are a professional quantitative trading AI assistant responsible for analyzing market data and making trading decisions.
|
return `You are a professional quantitative trading AI assistant responsible for analyzing market data and making trading decisions.
|
||||||
@@ -311,9 +311,9 @@ func (pb *PromptBuilder) getDecisionRequirementsEN() string {
|
|||||||
**Please output your decision (JSON format) immediately**:`
|
**Please output your decision (JSON format) immediately**:`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 辅助函数 ==========
|
// ========== Helper Functions ==========
|
||||||
|
|
||||||
// FormatDecisionExample 格式化决策示例(用于文档)
|
// FormatDecisionExample formats a decision example (for documentation)
|
||||||
func FormatDecisionExample(lang Language) string {
|
func FormatDecisionExample(lang Language) string {
|
||||||
example := Decision{
|
example := Decision{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
@@ -323,32 +323,32 @@ func FormatDecisionExample(lang Language) string {
|
|||||||
StopLoss: 42000,
|
StopLoss: 42000,
|
||||||
TakeProfit: 48000,
|
TakeProfit: 48000,
|
||||||
Confidence: 85,
|
Confidence: 85,
|
||||||
Reasoning: "详细的推理过程...",
|
Reasoning: "Detailed reasoning process...",
|
||||||
}
|
}
|
||||||
|
|
||||||
data, _ := json.MarshalIndent([]Decision{example}, "", " ")
|
data, _ := json.MarshalIndent([]Decision{example}, "", " ")
|
||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateDecisionFormat 验证决策格式是否正确
|
// ValidateDecisionFormat validates that the decision format is correct
|
||||||
func ValidateDecisionFormat(decisions []Decision) error {
|
func ValidateDecisionFormat(decisions []Decision) error {
|
||||||
if len(decisions) == 0 {
|
if len(decisions) == 0 {
|
||||||
return fmt.Errorf("决策列表不能为空")
|
return fmt.Errorf("decision list cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, d := range decisions {
|
for i, d := range decisions {
|
||||||
// 必需字段检查
|
// Required field checks
|
||||||
if d.Symbol == "" {
|
if d.Symbol == "" {
|
||||||
return fmt.Errorf("决策#%d: symbol不能为空", i+1)
|
return fmt.Errorf("decision #%d: symbol cannot be empty", i+1)
|
||||||
}
|
}
|
||||||
if d.Action == "" {
|
if d.Action == "" {
|
||||||
return fmt.Errorf("决策#%d: action不能为空", i+1)
|
return fmt.Errorf("decision #%d: action cannot be empty", i+1)
|
||||||
}
|
}
|
||||||
if d.Reasoning == "" {
|
if d.Reasoning == "" {
|
||||||
return fmt.Errorf("决策#%d: reasoning不能为空", i+1)
|
return fmt.Errorf("decision #%d: reasoning cannot be empty", i+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动作类型检查
|
// Action type validation
|
||||||
validActions := map[string]bool{
|
validActions := map[string]bool{
|
||||||
"HOLD": true,
|
"HOLD": true,
|
||||||
"PARTIAL_CLOSE": true,
|
"PARTIAL_CLOSE": true,
|
||||||
@@ -358,16 +358,16 @@ func ValidateDecisionFormat(decisions []Decision) error {
|
|||||||
"WAIT": true,
|
"WAIT": true,
|
||||||
}
|
}
|
||||||
if !validActions[d.Action] {
|
if !validActions[d.Action] {
|
||||||
return fmt.Errorf("决策#%d: 无效的action类型: %s", i+1, d.Action)
|
return fmt.Errorf("decision #%d: invalid action type: %s", i+1, d.Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开新仓位的必需参数检查
|
// Required parameters for opening new positions
|
||||||
if d.Action == "OPEN_NEW" {
|
if d.Action == "OPEN_NEW" {
|
||||||
if d.Leverage == 0 {
|
if d.Leverage == 0 {
|
||||||
return fmt.Errorf("决策#%d: OPEN_NEW动作需要提供leverage", i+1)
|
return fmt.Errorf("decision #%d: OPEN_NEW action requires leverage", i+1)
|
||||||
}
|
}
|
||||||
if d.PositionSizeUSD == 0 {
|
if d.PositionSizeUSD == 0 {
|
||||||
return fmt.Errorf("决策#%d: OPEN_NEW动作需要提供position_size_usd", i+1)
|
return fmt.Errorf("decision #%d: OPEN_NEW action requires position_size_usd", i+1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,8 +170,8 @@ func TestValidateDecisionFormat(t *testing.T) {
|
|||||||
t.Error("Empty decisions should return error")
|
t.Error("Empty decisions should return error")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(err.Error(), "不能为空") {
|
if !strings.Contains(err.Error(), "cannot be empty") {
|
||||||
t.Errorf("Error message should mention '不能为空', got: %v", err)
|
t.Errorf("Error message should mention 'cannot be empty', got: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -238,8 +238,8 @@ func TestValidateDecisionFormat(t *testing.T) {
|
|||||||
t.Error("Invalid action should return error")
|
t.Error("Invalid action should return error")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(err.Error(), "无效的action") {
|
if !strings.Contains(err.Error(), "invalid action") {
|
||||||
t.Errorf("Error should mention '无效的action', got: %v", err)
|
t.Errorf("Error should mention 'invalid action', got: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+39
-39
@@ -1,17 +1,17 @@
|
|||||||
package kernel
|
package kernel
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Trading Data Schema - 交易数据字典
|
// Trading Data Schema
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 双语数据字典,支持中文和英文
|
// Bilingual data dictionary supporting Chinese and English.
|
||||||
// 确保AI能够100%理解数据格式,无论使用哪种语言
|
// Ensures AI can fully understand data formats regardless of language.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SchemaVersion = "1.0.0"
|
SchemaVersion = "1.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Language 语言类型
|
// Language represents the language type
|
||||||
type Language string
|
type Language string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -19,20 +19,20 @@ const (
|
|||||||
LangEnglish Language = "en-US"
|
LangEnglish Language = "en-US"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ========== 双语字段定义 ==========
|
// ========== Bilingual Field Definitions ==========
|
||||||
|
|
||||||
// BilingualFieldDef 双语字段定义
|
// BilingualFieldDef defines a field with bilingual name, formula, and description
|
||||||
type BilingualFieldDef struct {
|
type BilingualFieldDef struct {
|
||||||
NameZH string // 中文名称
|
NameZH string // Chinese name
|
||||||
NameEN string // English name
|
NameEN string // English name
|
||||||
Unit string // 单位
|
Unit string // unit of measurement
|
||||||
FormulaZH string // 中文公式
|
FormulaZH string // Chinese formula
|
||||||
FormulaEN string // English formula
|
FormulaEN string // English formula
|
||||||
DescZH string // 中文描述
|
DescZH string // Chinese description
|
||||||
DescEN string // English description
|
DescEN string // English description
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetName 获取字段名称(根据语言)
|
// GetName returns the field name based on language
|
||||||
func (d BilingualFieldDef) GetName(lang Language) string {
|
func (d BilingualFieldDef) GetName(lang Language) string {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
return d.NameZH
|
return d.NameZH
|
||||||
@@ -40,7 +40,7 @@ func (d BilingualFieldDef) GetName(lang Language) string {
|
|||||||
return d.NameEN
|
return d.NameEN
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFormula 获取公式(根据语言)
|
// GetFormula returns the formula based on language
|
||||||
func (d BilingualFieldDef) GetFormula(lang Language) string {
|
func (d BilingualFieldDef) GetFormula(lang Language) string {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
return d.FormulaZH
|
return d.FormulaZH
|
||||||
@@ -48,7 +48,7 @@ func (d BilingualFieldDef) GetFormula(lang Language) string {
|
|||||||
return d.FormulaEN
|
return d.FormulaEN
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDesc 获取描述(根据语言)
|
// GetDesc returns the description based on language
|
||||||
func (d BilingualFieldDef) GetDesc(lang Language) string {
|
func (d BilingualFieldDef) GetDesc(lang Language) string {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
return d.DescZH
|
return d.DescZH
|
||||||
@@ -56,9 +56,9 @@ func (d BilingualFieldDef) GetDesc(lang Language) string {
|
|||||||
return d.DescEN
|
return d.DescEN
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 数据字典 ==========
|
// ========== Data Dictionary ==========
|
||||||
|
|
||||||
// DataDictionary 数据字典:定义所有字段的含义
|
// DataDictionary defines the meaning of all fields
|
||||||
var DataDictionary = map[string]map[string]BilingualFieldDef{
|
var DataDictionary = map[string]map[string]BilingualFieldDef{
|
||||||
"AccountMetrics": {
|
"AccountMetrics": {
|
||||||
"Equity": {
|
"Equity": {
|
||||||
@@ -217,18 +217,18 @@ var DataDictionary = map[string]map[string]BilingualFieldDef{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 双语规则定义 ==========
|
// ========== Bilingual Rule Definitions ==========
|
||||||
|
|
||||||
// BilingualRuleDef 双语规则定义
|
// BilingualRuleDef defines a trading rule with bilingual description and reason
|
||||||
type BilingualRuleDef struct {
|
type BilingualRuleDef struct {
|
||||||
Value interface{} // 规则值
|
Value interface{} // rule value
|
||||||
DescZH string // 中文描述
|
DescZH string // Chinese description
|
||||||
DescEN string // English description
|
DescEN string // English description
|
||||||
ReasonZH string // 中文原因
|
ReasonZH string // Chinese reason
|
||||||
ReasonEN string // English reason
|
ReasonEN string // English reason
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDesc 获取描述(根据语言)
|
// GetDesc returns the description based on language
|
||||||
func (d BilingualRuleDef) GetDesc(lang Language) string {
|
func (d BilingualRuleDef) GetDesc(lang Language) string {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
return d.DescZH
|
return d.DescZH
|
||||||
@@ -236,7 +236,7 @@ func (d BilingualRuleDef) GetDesc(lang Language) string {
|
|||||||
return d.DescEN
|
return d.DescEN
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReason 获取原因(根据语言)
|
// GetReason returns the reason based on language
|
||||||
func (d BilingualRuleDef) GetReason(lang Language) string {
|
func (d BilingualRuleDef) GetReason(lang Language) string {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
return d.ReasonZH
|
return d.ReasonZH
|
||||||
@@ -244,9 +244,9 @@ func (d BilingualRuleDef) GetReason(lang Language) string {
|
|||||||
return d.ReasonEN
|
return d.ReasonEN
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 交易规则 ==========
|
// ========== Trading Rules ==========
|
||||||
|
|
||||||
// TradingRules 交易规则定义
|
// TradingRules defines the trading rules
|
||||||
var TradingRules = struct {
|
var TradingRules = struct {
|
||||||
RiskManagement map[string]BilingualRuleDef
|
RiskManagement map[string]BilingualRuleDef
|
||||||
EntrySignals map[string]BilingualRuleDef
|
EntrySignals map[string]BilingualRuleDef
|
||||||
@@ -340,9 +340,9 @@ var TradingRules = struct {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== OI解读 ==========
|
// ========== OI Interpretation ==========
|
||||||
|
|
||||||
// OIInterpretation OI变化的市场解读(双语)
|
// OIInterpretation defines bilingual market interpretations for OI changes
|
||||||
type OIInterpretationType struct {
|
type OIInterpretationType struct {
|
||||||
OIUp_PriceUp struct {
|
OIUp_PriceUp struct {
|
||||||
ZH string
|
ZH string
|
||||||
@@ -393,9 +393,9 @@ var OIInterpretation = OIInterpretationType{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 常见错误 ==========
|
// ========== Common Mistakes ==========
|
||||||
|
|
||||||
// CommonMistake 常见错误定义
|
// CommonMistake defines a common mistake with bilingual fields
|
||||||
type CommonMistake struct {
|
type CommonMistake struct {
|
||||||
ErrorZH string
|
ErrorZH string
|
||||||
ErrorEN string
|
ErrorEN string
|
||||||
@@ -440,9 +440,9 @@ var CommonMistakes = []CommonMistake{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Prompt生成函数 ==========
|
// ========== Prompt Generation Functions ==========
|
||||||
|
|
||||||
// GetSchemaPrompt 生成Schema说明文本,用于AI Prompt
|
// GetSchemaPrompt generates schema description text for AI prompts
|
||||||
func GetSchemaPrompt(lang Language) string {
|
func GetSchemaPrompt(lang Language) string {
|
||||||
if lang == LangChinese {
|
if lang == LangChinese {
|
||||||
return getSchemaPromptZH()
|
return getSchemaPromptZH()
|
||||||
@@ -450,36 +450,36 @@ func GetSchemaPrompt(lang Language) string {
|
|||||||
return getSchemaPromptEN()
|
return getSchemaPromptEN()
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSchemaPromptZH 生成中文Prompt
|
// getSchemaPromptZH generates the Chinese prompt
|
||||||
func getSchemaPromptZH() string {
|
func getSchemaPromptZH() string {
|
||||||
prompt := "# 📖 数据字典与交易规则\n\n"
|
prompt := "# 📖 数据字典与交易规则\n\n"
|
||||||
prompt += "## 📊 字段含义说明\n\n"
|
prompt += "## 📊 字段含义说明\n\n"
|
||||||
|
|
||||||
// 账户指标
|
// Account metrics
|
||||||
prompt += "### 账户指标\n"
|
prompt += "### 账户指标\n"
|
||||||
for key, field := range DataDictionary["AccountMetrics"] {
|
for key, field := range DataDictionary["AccountMetrics"] {
|
||||||
prompt += formatFieldDefZH(key, field)
|
prompt += formatFieldDefZH(key, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 交易指标
|
// Trade metrics
|
||||||
prompt += "\n### 交易指标\n"
|
prompt += "\n### 交易指标\n"
|
||||||
for key, field := range DataDictionary["TradeMetrics"] {
|
for key, field := range DataDictionary["TradeMetrics"] {
|
||||||
prompt += formatFieldDefZH(key, field)
|
prompt += formatFieldDefZH(key, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 持仓指标
|
// Position metrics
|
||||||
prompt += "\n### 持仓指标\n"
|
prompt += "\n### 持仓指标\n"
|
||||||
for key, field := range DataDictionary["PositionMetrics"] {
|
for key, field := range DataDictionary["PositionMetrics"] {
|
||||||
prompt += formatFieldDefZH(key, field)
|
prompt += formatFieldDefZH(key, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 市场数据
|
// Market data
|
||||||
prompt += "\n### 市场数据\n"
|
prompt += "\n### 市场数据\n"
|
||||||
for key, field := range DataDictionary["MarketData"] {
|
for key, field := range DataDictionary["MarketData"] {
|
||||||
prompt += formatFieldDefZH(key, field)
|
prompt += formatFieldDefZH(key, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OI解读
|
// OI interpretation
|
||||||
prompt += "\n## 💹 持仓量(OI)变化解读\n\n"
|
prompt += "\n## 💹 持仓量(OI)变化解读\n\n"
|
||||||
prompt += "- **OI增加 + 价格上涨**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n"
|
prompt += "- **OI增加 + 价格上涨**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n"
|
||||||
prompt += "- **OI增加 + 价格下跌**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n"
|
prompt += "- **OI增加 + 价格下跌**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n"
|
||||||
@@ -489,7 +489,7 @@ func getSchemaPromptZH() string {
|
|||||||
return prompt
|
return prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSchemaPromptEN 生成英文Prompt
|
// getSchemaPromptEN generates the English prompt
|
||||||
func getSchemaPromptEN() string {
|
func getSchemaPromptEN() string {
|
||||||
prompt := "# 📖 Data Dictionary & Trading Rules\n\n"
|
prompt := "# 📖 Data Dictionary & Trading Rules\n\n"
|
||||||
prompt += "## 📊 Field Definitions\n\n"
|
prompt += "## 📊 Field Definitions\n\n"
|
||||||
@@ -528,7 +528,7 @@ func getSchemaPromptEN() string {
|
|||||||
return prompt
|
return prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatFieldDefZH 格式化中文字段定义
|
// formatFieldDefZH formats a field definition in Chinese
|
||||||
func formatFieldDefZH(key string, field BilingualFieldDef) string {
|
func formatFieldDefZH(key string, field BilingualFieldDef) string {
|
||||||
result := "- **" + key + "**(" + field.NameZH + "): " + field.DescZH
|
result := "- **" + key + "**(" + field.NameZH + "): " + field.DescZH
|
||||||
if field.FormulaZH != "" {
|
if field.FormulaZH != "" {
|
||||||
@@ -541,7 +541,7 @@ func formatFieldDefZH(key string, field BilingualFieldDef) string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatFieldDefEN 格式化英文字段定义
|
// formatFieldDefEN formats a field definition in English
|
||||||
func formatFieldDefEN(key string, field BilingualFieldDef) string {
|
func formatFieldDefEN(key string, field BilingualFieldDef) string {
|
||||||
result := "- **" + key + "** (" + field.NameEN + "): " + field.DescEN
|
result := "- **" + key + "** (" + field.NameEN + "): " + field.DescEN
|
||||||
if field.FormulaEN != "" {
|
if field.FormulaEN != "" {
|
||||||
|
|||||||
+5
-5
@@ -210,11 +210,11 @@ type BoxData struct {
|
|||||||
type RegimeLevel string
|
type RegimeLevel string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RegimeLevelNarrow RegimeLevel = "narrow" // 窄幅震荡
|
RegimeLevelNarrow RegimeLevel = "narrow" // narrow range oscillation
|
||||||
RegimeLevelStandard RegimeLevel = "standard" // 标准震荡
|
RegimeLevelStandard RegimeLevel = "standard" // standard oscillation
|
||||||
RegimeLevelWide RegimeLevel = "wide" // 宽幅震荡
|
RegimeLevelWide RegimeLevel = "wide" // wide range oscillation
|
||||||
RegimeLevelVolatile RegimeLevel = "volatile" // 剧烈震荡
|
RegimeLevelVolatile RegimeLevel = "volatile" // extreme volatility
|
||||||
RegimeLevelTrending RegimeLevel = "trending" // 趋势
|
RegimeLevelTrending RegimeLevel = "trending" // trending
|
||||||
)
|
)
|
||||||
|
|
||||||
// BreakoutLevel represents which box level has been broken
|
// BreakoutLevel represents which box level has been broken
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func (c *Client) fetchAI500() ([]CoinData, error) {
|
|||||||
return nil, fmt.Errorf("API returned failure status")
|
return nil, fmt.Errorf("API returned failure status")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 空列表是正常情况,不是错误
|
// Empty list is a normal condition, not an error
|
||||||
if len(response.Data.Coins) == 0 {
|
if len(response.Data.Coins) == 0 {
|
||||||
log.Printf("ℹ️ AI500 returned empty coin list (no coins meet criteria currently)")
|
log.Printf("ℹ️ AI500 returned empty coin list (no coins meet criteria currently)")
|
||||||
return []CoinData{}, nil
|
return []CoinData{}, nil
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type NetFlowResponse struct {
|
|||||||
Netflows []NetFlowPosition `json:"netflows"`
|
Netflows []NetFlowPosition `json:"netflows"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Type string `json:"type"` // institution or personal
|
Type string `json:"type"` // institution or personal
|
||||||
Trade string `json:"trade"` // 合约 or 现货
|
Trade string `json:"trade"` // futures or spot
|
||||||
TimeRange string `json:"time_range"`
|
TimeRange string `json:"time_range"`
|
||||||
RankType string `json:"rank_type"` // top or low
|
RankType string `json:"rank_type"` // top or low
|
||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
|
|||||||
+6
-6
@@ -1,11 +1,11 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Railway 会设置 PORT 环境变量
|
# Railway sets the PORT environment variable
|
||||||
export PORT=${PORT:-8080}
|
export PORT=${PORT:-8080}
|
||||||
echo "🚀 Starting NOFX on port $PORT..."
|
echo "🚀 Starting NOFX on port $PORT..."
|
||||||
|
|
||||||
# 生成加密密钥(如果没有设置)
|
# Generate encryption keys (if not already set)
|
||||||
if [ -z "$RSA_PRIVATE_KEY" ]; then
|
if [ -z "$RSA_PRIVATE_KEY" ]; then
|
||||||
export RSA_PRIVATE_KEY=$(openssl genrsa 2048 2>/dev/null)
|
export RSA_PRIVATE_KEY=$(openssl genrsa 2048 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
@@ -13,7 +13,7 @@ if [ -z "$DATA_ENCRYPTION_KEY" ]; then
|
|||||||
export DATA_ENCRYPTION_KEY=$(openssl rand -base64 32)
|
export DATA_ENCRYPTION_KEY=$(openssl rand -base64 32)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 生成 nginx 配置
|
# Generate nginx config
|
||||||
cat > /etc/nginx/http.d/default.conf << NGINX_EOF
|
cat > /etc/nginx/http.d/default.conf << NGINX_EOF
|
||||||
server {
|
server {
|
||||||
listen $PORT;
|
listen $PORT;
|
||||||
@@ -44,14 +44,14 @@ server {
|
|||||||
}
|
}
|
||||||
NGINX_EOF
|
NGINX_EOF
|
||||||
|
|
||||||
# 启动后端(端口 8081)
|
# Start backend (port 8081)
|
||||||
API_SERVER_PORT=8081 /app/nofx &
|
API_SERVER_PORT=8081 /app/nofx &
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# 启动 nginx(后台)
|
# Start nginx (background)
|
||||||
nginx
|
nginx
|
||||||
|
|
||||||
echo "✅ NOFX started successfully"
|
echo "✅ NOFX started successfully"
|
||||||
|
|
||||||
# 保持容器运行
|
# Keep the container running
|
||||||
tail -f /dev/null
|
tail -f /dev/null
|
||||||
|
|||||||
+2
-2
@@ -111,11 +111,11 @@ type CoinSourceConfig struct {
|
|||||||
UseAI500 bool `json:"use_ai500"`
|
UseAI500 bool `json:"use_ai500"`
|
||||||
// AI500 coin pool maximum count
|
// AI500 coin pool maximum count
|
||||||
AI500Limit int `json:"ai500_limit,omitempty"`
|
AI500Limit int `json:"ai500_limit,omitempty"`
|
||||||
// whether to use OI Top (持仓增加榜,适合做多)
|
// whether to use OI Top (OI increase ranking, suitable for long positions)
|
||||||
UseOITop bool `json:"use_oi_top"`
|
UseOITop bool `json:"use_oi_top"`
|
||||||
// OI Top maximum count
|
// OI Top maximum count
|
||||||
OITopLimit int `json:"oi_top_limit,omitempty"`
|
OITopLimit int `json:"oi_top_limit,omitempty"`
|
||||||
// whether to use OI Low (持仓减少榜,适合做空)
|
// whether to use OI Low (OI decrease ranking, suitable for short positions)
|
||||||
UseOILow bool `json:"use_oi_low"`
|
UseOILow bool `json:"use_oi_low"`
|
||||||
// OI Low maximum count
|
// OI Low maximum count
|
||||||
OILowLimit int `json:"oi_low_limit,omitempty"`
|
OILowLimit int `json:"oi_low_limit,omitempty"`
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ func (a *Agent) Run(userMessage string, onChunk func(string)) string {
|
|||||||
|
|
||||||
// Safety: max iterations reached.
|
// Safety: max iterations reached.
|
||||||
logger.Warnf("Agent: max iterations (%d) reached for message: %q", maxIterations, userMessage)
|
logger.Warnf("Agent: max iterations (%d) reached for message: %q", maxIterations, userMessage)
|
||||||
reply := "操作已完成,请检查您的账户查看最新状态。"
|
reply := "Operation completed. Please check your account for the latest status. / 操作已完成,请检查您的账户查看最新状态。"
|
||||||
a.memory.Add("user", userMessage)
|
a.memory.Add("user", userMessage)
|
||||||
a.memory.Add("assistant", reply)
|
a.memory.Add("assistant", reply)
|
||||||
return reply
|
return reply
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (m *Manager) Run(chatID int64, userMessage string, onChunk func(string)) st
|
|||||||
case lane <- struct{}{}:
|
case lane <- struct{}{}:
|
||||||
case <-time.After(60 * time.Second):
|
case <-time.After(60 * time.Second):
|
||||||
logger.Warnf("Agent: lane wait timeout for chat %d — previous message still processing", chatID)
|
logger.Warnf("Agent: lane wait timeout for chat %d — previous message still processing", chatID)
|
||||||
return "上一条消息仍在处理中,请稍等片刻后再试。"
|
return "Previous message is still being processed. Please wait a moment and try again. / 上一条消息仍在处理中,请稍等片刻后再试。"
|
||||||
}
|
}
|
||||||
defer func() { <-lane }()
|
defer func() { <-lane }()
|
||||||
return a.Run(userMessage, onChunk)
|
return a.Run(userMessage, onChunk)
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ func (at *AutoTrader) runCycle() error {
|
|||||||
// NOTE: Must be called BEFORE candidate coins check to ensure equity is always recorded
|
// NOTE: Must be called BEFORE candidate coins check to ensure equity is always recorded
|
||||||
at.saveEquitySnapshot(ctx)
|
at.saveEquitySnapshot(ctx)
|
||||||
|
|
||||||
// 如果没有候选币种,记录但不报错
|
// If no candidate coins available, log but do not error
|
||||||
if len(ctx.CandidateCoins) == 0 {
|
if len(ctx.CandidateCoins) == 0 {
|
||||||
logger.Infof("ℹ️ No candidate coins available, skipping this cycle")
|
logger.Infof("ℹ️ No candidate coins available, skipping this cycle")
|
||||||
record.Success = true // 不是错误,只是没有候选币
|
record.Success = true // Not an error, just no candidate coins
|
||||||
record.ExecutionLog = append(record.ExecutionLog, "No candidate coins available, cycle skipped")
|
record.ExecutionLog = append(record.ExecutionLog, "No candidate coins available, cycle skipped")
|
||||||
record.AccountState = store.AccountSnapshot{
|
record.AccountState = store.AccountSnapshot{
|
||||||
TotalBalance: ctx.Account.TotalEquity,
|
TotalBalance: ctx.Account.TotalEquity,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[str
|
|||||||
|
|
||||||
resp, err := t.client.Do(req)
|
resp, err := t.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// ✅ 正确做法:查询失败返回错误,而不是假设成交
|
// Correct approach: return error on query failure, do not assume order is filled
|
||||||
return nil, fmt.Errorf("failed to query order status: %w", err)
|
return nil, fmt.Errorf("failed to query order status: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { motion, AnimatePresence } from 'framer-motion'
|
|||||||
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
|
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
|
||||||
import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
||||||
import { useLanguage } from '../../contexts/LanguageContext'
|
import { useLanguage } from '../../contexts/LanguageContext'
|
||||||
|
import { t } from '../../i18n/translations'
|
||||||
|
|
||||||
interface LoginRequiredOverlayProps {
|
interface LoginRequiredOverlayProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -12,52 +13,19 @@ interface LoginRequiredOverlayProps {
|
|||||||
export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequiredOverlayProps) {
|
export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequiredOverlayProps) {
|
||||||
const { language } = useLanguage()
|
const { language } = useLanguage()
|
||||||
|
|
||||||
const texts = {
|
const tr = (key: string, params?: Record<string, string | number>) =>
|
||||||
zh: {
|
t(`loginRequired.${key}`, language, params)
|
||||||
title: '系统访问受限',
|
|
||||||
subtitle: featureName ? `访问「${featureName}」需要更高权限` : '此模块需要授权访问',
|
|
||||||
description: '初始化身份验证协议以解锁完整系统功能:AI 交易员配置、策略市场数据流、回测模拟核心。',
|
|
||||||
benefits: [
|
|
||||||
'AI 交易员控制权',
|
|
||||||
'高频策略核心市场',
|
|
||||||
'历史数据回测引擎',
|
|
||||||
'全系统数据可视化'
|
|
||||||
],
|
|
||||||
login: '执行登录指令',
|
|
||||||
register: '注册新用户 ID',
|
|
||||||
later: '中止操作'
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
title: 'SYSTEM ACCESS DENIED',
|
|
||||||
subtitle: featureName ? `Module "${featureName}" requires elevated privileges` : 'Authorization required for this module',
|
|
||||||
description: 'Initialize authentication protocol to unlock full system capabilities: AI Trader configuration, Strategy Market data streams, and Backtest Simulation core.',
|
|
||||||
benefits: [
|
|
||||||
'AI Trader Control',
|
|
||||||
'HFT Strategy Market',
|
|
||||||
'Historical Backtest Engine',
|
|
||||||
'Full System Visualization'
|
|
||||||
],
|
|
||||||
login: 'EXECUTE LOGIN',
|
|
||||||
register: 'REGISTER NEW ID',
|
|
||||||
later: 'ABORT'
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
title: 'AKSES SISTEM DITOLAK',
|
|
||||||
subtitle: featureName ? `Modul "${featureName}" memerlukan hak akses lebih tinggi` : 'Otorisasi diperlukan untuk modul ini',
|
|
||||||
description: 'Inisialisasi protokol autentikasi untuk membuka kemampuan sistem penuh: konfigurasi Trader AI, aliran data Pasar Strategi, dan inti Simulasi Backtest.',
|
|
||||||
benefits: [
|
|
||||||
'Kontrol Trader AI',
|
|
||||||
'Pasar Strategi HFT',
|
|
||||||
'Mesin Backtest Historis',
|
|
||||||
'Visualisasi Sistem Penuh'
|
|
||||||
],
|
|
||||||
login: 'JALANKAN LOGIN',
|
|
||||||
register: 'DAFTAR ID BARU',
|
|
||||||
later: 'BATALKAN'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = texts[language]
|
const subtitle = featureName
|
||||||
|
? tr('subtitleWithFeature', { featureName })
|
||||||
|
: tr('subtitleDefault')
|
||||||
|
|
||||||
|
const benefits = [
|
||||||
|
tr('benefit1'),
|
||||||
|
tr('benefit2'),
|
||||||
|
tr('benefit3'),
|
||||||
|
tr('benefit4'),
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -108,7 +76,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
|||||||
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
|
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
|
||||||
<div className="bg-nofx-bg border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
|
<div className="bg-nofx-bg border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
|
||||||
<AlertTriangle size={18} className="animate-pulse" />
|
<AlertTriangle size={18} className="animate-pulse" />
|
||||||
<span className="font-bold tracking-widest text-sm uppercase">{language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'}</span>
|
<span className="font-bold tracking-widest text-sm uppercase">{tr('accessDenied')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,19 +84,19 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
|||||||
{/* Terminal Text */}
|
{/* Terminal Text */}
|
||||||
<div className="space-y-4 mb-8">
|
<div className="space-y-4 mb-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{t.title}</h2>
|
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{tr('title')}</h2>
|
||||||
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{t.subtitle}</p>
|
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4">
|
<div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4">
|
||||||
<p className="text-xs text-nofx-text-muted leading-relaxed font-mono">
|
<p className="text-xs text-nofx-text-muted leading-relaxed font-mono">
|
||||||
<span className="text-green-500 mr-2">$</span>
|
<span className="text-green-500 mr-2">$</span>
|
||||||
{t.description}
|
{tr('description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{t.benefits.map((benefit, i) => (
|
{benefits.map((benefit, i) => (
|
||||||
<div key={i} className="flex items-center gap-2 text-[10px] text-nofx-text-muted uppercase tracking-wide">
|
<div key={i} className="flex items-center gap-2 text-[10px] text-nofx-text-muted uppercase tracking-wide">
|
||||||
<span className="text-nofx-gold">✓</span> {benefit}
|
<span className="text-nofx-gold">✓</span> {benefit}
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +111,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
|||||||
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-black font-bold text-xs uppercase tracking-widest hover:bg-yellow-400 transition-all shadow-neon hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group"
|
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-black font-bold text-xs uppercase tracking-widest hover:bg-yellow-400 transition-all shadow-neon hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group"
|
||||||
>
|
>
|
||||||
<LogIn size={14} />
|
<LogIn size={14} />
|
||||||
<span>{t.login}</span>
|
<span>{tr('loginButton')}</span>
|
||||||
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-></span>
|
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -152,7 +120,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
|||||||
className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-nofx-gold/20 text-nofx-text-muted hover:text-white hover:border-nofx-gold font-bold text-xs uppercase tracking-widest transition-all hover:bg-nofx-gold/10"
|
className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-nofx-gold/20 text-nofx-text-muted hover:text-white hover:border-nofx-gold font-bold text-xs uppercase tracking-widest transition-all hover:bg-nofx-gold/10"
|
||||||
>
|
>
|
||||||
<UserPlus size={14} />
|
<UserPlus size={14} />
|
||||||
<span>{t.register}</span>
|
<span>{tr('registerButton')}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -161,7 +129,7 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
|
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
|
||||||
>
|
>
|
||||||
[ {t.later} ]
|
[ {tr('abort')} ]
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
CandlestickChart as CandlestickIcon,
|
CandlestickChart as CandlestickIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { api } from '../../lib/api'
|
import { api } from '../../lib/api'
|
||||||
|
import { t, type Language } from '../../i18n/translations'
|
||||||
import type {
|
import type {
|
||||||
BacktestEquityPoint,
|
BacktestEquityPoint,
|
||||||
BacktestTradeEvent,
|
BacktestTradeEvent,
|
||||||
@@ -136,7 +137,7 @@ export function EquityChart({ equity, trades }: EquityChartProps) {
|
|||||||
interface CandlestickChartProps {
|
interface CandlestickChartProps {
|
||||||
runId: string
|
runId: string
|
||||||
trades: BacktestTradeEvent[]
|
trades: BacktestTradeEvent[]
|
||||||
language: string
|
language: Language
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CandlestickChartComponent({ runId, trades, language }: CandlestickChartProps) {
|
export function CandlestickChartComponent({ runId, trades, language }: CandlestickChartProps) {
|
||||||
@@ -289,7 +290,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
|||||||
if (symbols.length === 0) {
|
if (symbols.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="py-12 text-center" style={{ color: '#5E6673' }}>
|
<div className="py-12 text-center" style={{ color: '#5E6673' }}>
|
||||||
{language === 'zh' ? '没有交易记录' : 'No trades to display'}
|
{t('backtestChart.noTrades', language)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -300,7 +301,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CandlestickIcon size={16} style={{ color: '#F0B90B' }} />
|
<CandlestickIcon size={16} style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '币种' : 'Symbol'}
|
{t('backtestChart.symbol', language)}
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={selectedSymbol}
|
value={selectedSymbol}
|
||||||
@@ -319,7 +320,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock size={14} style={{ color: '#848E9C' }} />
|
<Clock size={14} style={{ color: '#848E9C' }} />
|
||||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '周期' : 'Interval'}
|
{t('backtestChart.interval', language)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex rounded overflow-hidden" style={{ border: '1px solid #2B3139' }}>
|
<div className="flex rounded overflow-hidden" style={{ border: '1px solid #2B3139' }}>
|
||||||
{CHART_TIMEFRAMES.map((tf) => (
|
{CHART_TIMEFRAMES.map((tf) => (
|
||||||
@@ -339,7 +340,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-xs" style={{ color: '#5E6673' }}>
|
<span className="text-xs" style={{ color: '#5E6673' }}>
|
||||||
({symbolTrades.length} {language === 'zh' ? '笔交易' : 'trades'})
|
({symbolTrades.length} {t('backtestChart.trades', language)})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -351,7 +352,7 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
|||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center h-[400px]" style={{ color: '#848E9C' }}>
|
<div className="flex items-center justify-center h-[400px]" style={{ color: '#848E9C' }}>
|
||||||
<RefreshCw className="animate-spin mr-2" size={16} />
|
<RefreshCw className="animate-spin mr-2" size={16} />
|
||||||
{language === 'zh' ? '加载K线数据...' : 'Loading kline data...'}
|
{t('backtestChart.loadingKline', language)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
@@ -365,14 +366,14 @@ export function CandlestickChartComponent({ runId, trades, language }: Candlesti
|
|||||||
<div className="flex items-center gap-4 text-xs" style={{ color: '#848E9C' }}>
|
<div className="flex items-center gap-4 text-xs" style={{ color: '#848E9C' }}>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="w-2.5 h-2.5 rounded-full" style={{ background: '#0ECB81' }} />
|
<div className="w-2.5 h-2.5 rounded-full" style={{ background: '#0ECB81' }} />
|
||||||
<span>{language === 'zh' ? '开仓/盈利' : 'Open/Profit'}</span>
|
<span>{t('backtestChart.openProfit', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="w-2.5 h-2.5 rounded-full" style={{ background: '#F6465D' }} />
|
<div className="w-2.5 h-2.5 rounded-full" style={{ background: '#F6465D' }} />
|
||||||
<span>{language === 'zh' ? '亏损平仓' : 'Loss Close'}</span>
|
<span>{t('backtestChart.lossClose', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ color: '#5E6673' }}>|</span>
|
<span style={{ color: '#5E6673' }}>|</span>
|
||||||
<span>▲ Long · ▼ Short · ✕ {language === 'zh' ? '平仓' : 'Close'}</span>
|
<span>▲ Long · ▼ Short · ✕ {t('backtestChart.close', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -384,7 +385,7 @@ interface BacktestChartTabProps {
|
|||||||
equity: BacktestEquityPoint[] | undefined
|
equity: BacktestEquityPoint[] | undefined
|
||||||
trades: BacktestTradeEvent[] | undefined
|
trades: BacktestTradeEvent[] | undefined
|
||||||
selectedRunId: string
|
selectedRunId: string
|
||||||
language: string
|
language: Language
|
||||||
tr: (key: string) => string
|
tr: (key: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +406,7 @@ export function BacktestChartTab({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
||||||
{language === 'zh' ? '资金曲线' : 'Equity Curve'}
|
{t('backtestChart.equityCurve', language)}
|
||||||
</h4>
|
</h4>
|
||||||
{equity && equity.length > 0 ? (
|
{equity && equity.length > 0 ? (
|
||||||
<EquityChart equity={equity} trades={trades ?? []} />
|
<EquityChart equity={equity} trades={trades ?? []} />
|
||||||
@@ -419,7 +420,7 @@ export function BacktestChartTab({
|
|||||||
{selectedRunId && trades && trades.length > 0 && (
|
{selectedRunId && trades && trades.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
<h4 className="text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
||||||
{language === 'zh' ? 'K线图 & 交易标记' : 'Candlestick & Trade Markers'}
|
{t('backtestChart.candlestickTradeMarkers', language)}
|
||||||
</h4>
|
</h4>
|
||||||
<CandlestickChartComponent
|
<CandlestickChartComponent
|
||||||
runId={selectedRunId}
|
runId={selectedRunId}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { AIModel, Strategy } from '../../types'
|
import type { AIModel, Strategy } from '../../types'
|
||||||
|
import { t as globalT } from '../../i18n/translations'
|
||||||
|
import type { Language } from '../../i18n/translations'
|
||||||
|
|
||||||
// ============ Types ============
|
// ============ Types ============
|
||||||
|
|
||||||
@@ -104,12 +106,12 @@ export function BacktestConfigForm({
|
|||||||
}
|
}
|
||||||
}, [selectedStrategy])
|
}, [selectedStrategy])
|
||||||
|
|
||||||
const zh = language === 'zh'
|
const lang = language as Language
|
||||||
const quickRanges = [
|
const quickRanges = [
|
||||||
{ label: zh ? '24小时' : '24h', hours: 24 },
|
{ label: globalT('backtestConfigForm.quickRange24h', lang), hours: 24 },
|
||||||
{ label: zh ? '3天' : '3d', hours: 72 },
|
{ label: globalT('backtestConfigForm.quickRange3d', lang), hours: 72 },
|
||||||
{ label: zh ? '7天' : '7d', hours: 168 },
|
{ label: globalT('backtestConfigForm.quickRange7d', lang), hours: 168 },
|
||||||
{ label: zh ? '30天' : '30d', hours: 720 },
|
{ label: globalT('backtestConfigForm.quickRange30d', lang), hours: 720 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const applyQuickRange = (hours: number) => {
|
const applyQuickRange = (hours: number) => {
|
||||||
@@ -144,9 +146,9 @@ export function BacktestConfigForm({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<span className="ml-2 text-xs" style={{ color: '#848E9C' }}>
|
<span className="ml-2 text-xs" style={{ color: '#848E9C' }}>
|
||||||
{wizardStep === 1 ? (zh ? '选择模型' : 'Select Model')
|
{wizardStep === 1 ? globalT('backtestConfigForm.selectModel', lang)
|
||||||
: wizardStep === 2 ? (zh ? '配置参数' : 'Configure')
|
: wizardStep === 2 ? globalT('backtestConfigForm.configure', lang)
|
||||||
: (zh ? '确认启动' : 'Confirm')}
|
: globalT('backtestConfigForm.confirmStart', lang)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -196,7 +198,7 @@ export function BacktestConfigForm({
|
|||||||
{/* Strategy Selection (Optional) */}
|
{/* Strategy Selection (Optional) */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
|
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{zh ? '策略配置(可选)' : 'Strategy (Optional)'}
|
{globalT('backtestConfigForm.strategyOptional', lang)}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className="w-full p-3 rounded-lg text-sm"
|
className="w-full p-3 rounded-lg text-sm"
|
||||||
@@ -204,7 +206,7 @@ export function BacktestConfigForm({
|
|||||||
value={formState.strategyId}
|
value={formState.strategyId}
|
||||||
onChange={(e) => onFormChange('strategyId', e.target.value)}
|
onChange={(e) => onFormChange('strategyId', e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">{zh ? '不使用保存的策略' : 'No saved strategy'}</option>
|
<option value="">{globalT('backtestConfigForm.noSavedStrategy', lang)}</option>
|
||||||
{strategies?.map((s) => (
|
{strategies?.map((s) => (
|
||||||
<option key={s.id} value={s.id}>
|
<option key={s.id} value={s.id}>
|
||||||
{s.name} {s.is_active && '✓'} {s.is_default && '⭐'}
|
{s.name} {s.is_active && '✓'} {s.is_default && '⭐'}
|
||||||
@@ -215,7 +217,7 @@ export function BacktestConfigForm({
|
|||||||
<div className="mt-2 p-2 rounded" style={{ background: 'rgba(240,185,11,0.1)', border: '1px solid rgba(240,185,11,0.2)' }}>
|
<div className="mt-2 p-2 rounded" style={{ background: 'rgba(240,185,11,0.1)', border: '1px solid rgba(240,185,11,0.2)' }}>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<span style={{ color: '#F0B90B' }}>
|
<span style={{ color: '#F0B90B' }}>
|
||||||
{zh ? '币种来源:' : 'Coin Source:'}
|
{globalT('backtestConfigForm.coinSource', lang)}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium" style={{ color: '#EAECEF' }}>
|
<span className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{coinSourceDescription.type}
|
{coinSourceDescription.type}
|
||||||
@@ -225,9 +227,7 @@ export function BacktestConfigForm({
|
|||||||
</div>
|
</div>
|
||||||
{strategyHasDynamicCoins && (
|
{strategyHasDynamicCoins && (
|
||||||
<div className="text-xs mt-1" style={{ color: '#F0B90B' }}>
|
<div className="text-xs mt-1" style={{ color: '#F0B90B' }}>
|
||||||
{zh
|
{globalT('backtestConfigForm.clearDynamicCoins', lang)}
|
||||||
? '⚡ 清空下方币种输入框即可使用策略的动态币种'
|
|
||||||
: '⚡ Clear the symbols field below to use strategy\'s dynamic coins'}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -239,7 +239,7 @@ export function BacktestConfigForm({
|
|||||||
{tr('form.symbolsLabel')}
|
{tr('form.symbolsLabel')}
|
||||||
{strategyHasDynamicCoins && (
|
{strategyHasDynamicCoins && (
|
||||||
<span className="ml-2" style={{ color: '#5E6673' }}>
|
<span className="ml-2" style={{ color: '#5E6673' }}>
|
||||||
({zh ? '可选 - 策略已配置币种来源' : 'Optional - strategy has coin source'})
|
({globalT('backtestConfigForm.optionalCoinSource', lang)})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
@@ -283,7 +283,7 @@ export function BacktestConfigForm({
|
|||||||
onChange={(e) => onFormChange('symbols', e.target.value)}
|
onChange={(e) => onFormChange('symbols', e.target.value)}
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder={strategyHasDynamicCoins
|
placeholder={strategyHasDynamicCoins
|
||||||
? (zh ? '留空将使用策略配置的币种来源' : 'Leave empty to use strategy coin source')
|
? globalT('backtestConfigForm.leavEmptyForStrategy', lang)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -294,7 +294,7 @@ export function BacktestConfigForm({
|
|||||||
className="absolute top-2 right-2 px-2 py-1 rounded text-xs"
|
className="absolute top-2 right-2 px-2 py-1 rounded text-xs"
|
||||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||||
>
|
>
|
||||||
{zh ? '清空使用策略币种' : 'Clear to use strategy'}
|
{globalT('backtestConfigForm.clearToUseStrategy', lang)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -307,7 +307,7 @@ export function BacktestConfigForm({
|
|||||||
className="w-full py-2.5 rounded-lg font-medium flex items-center justify-center gap-2 transition-all disabled:opacity-50"
|
className="w-full py-2.5 rounded-lg font-medium flex items-center justify-center gap-2 transition-all disabled:opacity-50"
|
||||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||||
>
|
>
|
||||||
{zh ? '下一步' : 'Next'}
|
{globalT('backtestConfigForm.next', lang)}
|
||||||
<ChevronRight className="w-4 h-4" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -359,7 +359,7 @@ export function BacktestConfigForm({
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
|
<label className="block text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{zh ? '时间周期' : 'Timeframes'}
|
{globalT('backtestConfigForm.timeframes', lang)}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{TIMEFRAME_OPTIONS.map((tf) => {
|
{TIMEFRAME_OPTIONS.map((tf) => {
|
||||||
@@ -428,7 +428,7 @@ export function BacktestConfigForm({
|
|||||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-4 h-4" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
{zh ? '上一步' : 'Back'}
|
{globalT('backtestConfigForm.back', lang)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -436,7 +436,7 @@ export function BacktestConfigForm({
|
|||||||
className="flex-1 py-2 rounded-lg font-medium flex items-center justify-center gap-2"
|
className="flex-1 py-2 rounded-lg font-medium flex items-center justify-center gap-2"
|
||||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||||
>
|
>
|
||||||
{zh ? '下一步' : 'Next'}
|
{globalT('backtestConfigForm.next', lang)}
|
||||||
<ChevronRight className="w-4 h-4" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -520,7 +520,7 @@ export function BacktestConfigForm({
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs mb-1" style={{ color: '#848E9C' }}>
|
<label className="block text-xs mb-1" style={{ color: '#848E9C' }}>
|
||||||
{zh ? '策略风格' : 'Strategy Style'}
|
{globalT('backtestConfigForm.strategyStyle', lang)}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{['baseline', 'aggressive', 'conservative', 'scalping'].map((p) => (
|
{['baseline', 'aggressive', 'conservative', 'scalping'].map((p) => (
|
||||||
@@ -570,7 +570,7 @@ export function BacktestConfigForm({
|
|||||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-4 h-4" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
{zh ? '上一步' : 'Back'}
|
{globalT('backtestConfigForm.back', lang)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ArrowDownRight,
|
ArrowDownRight,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { MetricTooltip } from '../common/MetricTooltip'
|
import { MetricTooltip } from '../common/MetricTooltip'
|
||||||
|
import { t, type Language } from '../../i18n/translations'
|
||||||
import { EquityChart } from './BacktestChartTab'
|
import { EquityChart } from './BacktestChartTab'
|
||||||
import type {
|
import type {
|
||||||
BacktestEquityPoint,
|
BacktestEquityPoint,
|
||||||
@@ -131,7 +132,7 @@ export function ProgressRing({ progress, size = 120 }: ProgressRingProps) {
|
|||||||
|
|
||||||
interface PositionsDisplayProps {
|
interface PositionsDisplayProps {
|
||||||
positions: BacktestPositionStatus[]
|
positions: BacktestPositionStatus[]
|
||||||
language: string
|
language: Language
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PositionsDisplay({ positions, language }: PositionsDisplayProps) {
|
export function PositionsDisplay({ positions, language }: PositionsDisplayProps) {
|
||||||
@@ -151,7 +152,7 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Activity className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
<Activity className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{language === 'zh' ? '当前持仓' : 'Active Positions'}
|
{t('backtestOverview.activePositions', language)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="px-1.5 py-0.5 rounded text-xs"
|
className="px-1.5 py-0.5 rounded text-xs"
|
||||||
@@ -162,13 +163,13 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-xs">
|
<div className="flex items-center gap-3 text-xs">
|
||||||
<span style={{ color: '#848E9C' }}>
|
<span style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '保证金' : 'Margin'}: ${totalMargin.toFixed(2)}
|
{t('backtestOverview.margin', language)}: ${totalMargin.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="font-medium"
|
className="font-medium"
|
||||||
style={{ color: totalUnrealizedPnL >= 0 ? '#0ECB81' : '#F6465D' }}
|
style={{ color: totalUnrealizedPnL >= 0 ? '#0ECB81' : '#F6465D' }}
|
||||||
>
|
>
|
||||||
{language === 'zh' ? '浮盈' : 'Unrealized'}: {totalUnrealizedPnL >= 0 ? '+' : ''}
|
{t('backtestOverview.unrealized', language)}: {totalUnrealizedPnL >= 0 ? '+' : ''}
|
||||||
${totalUnrealizedPnL.toFixed(2)}
|
${totalUnrealizedPnL.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,8 +215,8 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px]" style={{ color: '#5E6673' }}>
|
<div className="text-[10px]" style={{ color: '#5E6673' }}>
|
||||||
{language === 'zh' ? '数量' : 'Qty'}: {pos.quantity.toFixed(4)} ·{' '}
|
{t('backtestOverview.qty', language)}: {pos.quantity.toFixed(4)} ·{' '}
|
||||||
{language === 'zh' ? '保证金' : 'Margin'}: ${pos.margin_used.toFixed(2)}
|
{t('backtestOverview.margin', language)}: ${pos.margin_used.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,10 +224,10 @@ export function PositionsDisplay({ positions, language }: PositionsDisplayProps)
|
|||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<span style={{ color: '#848E9C' }}>
|
<span style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '开仓' : 'Entry'}: ${pos.entry_price.toFixed(2)}
|
{t('backtestOverview.entry', language)}: ${pos.entry_price.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ color: '#EAECEF' }}>
|
<span style={{ color: '#EAECEF' }}>
|
||||||
{language === 'zh' ? '现价' : 'Mark'}: ${pos.mark_price.toFixed(2)}
|
{t('backtestOverview.mark', language)}: ${pos.mark_price.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end gap-1.5 mt-0.5">
|
<div className="flex items-center justify-end gap-1.5 mt-0.5">
|
||||||
@@ -255,7 +256,7 @@ interface BacktestOverviewTabProps {
|
|||||||
equity: BacktestEquityPoint[] | undefined
|
equity: BacktestEquityPoint[] | undefined
|
||||||
trades: BacktestTradeEvent[] | undefined
|
trades: BacktestTradeEvent[] | undefined
|
||||||
metrics: BacktestMetrics | undefined
|
metrics: BacktestMetrics | undefined
|
||||||
language: string
|
language: Language
|
||||||
tr: (key: string) => string
|
tr: (key: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +286,7 @@ export function BacktestOverviewTab({
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
|
||||||
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
||||||
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
|
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '胜率' : 'Win Rate'}
|
{t('backtestOverview.winRate', language)}
|
||||||
<MetricTooltip metricKey="win_rate" language={language} size={11} />
|
<MetricTooltip metricKey="win_rate" language={language} size={11} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
||||||
@@ -294,7 +295,7 @@ export function BacktestOverviewTab({
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
||||||
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
|
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '盈亏因子' : 'Profit Factor'}
|
{t('backtestOverview.profitFactor', language)}
|
||||||
<MetricTooltip metricKey="profit_factor" language={language} size={11} />
|
<MetricTooltip metricKey="profit_factor" language={language} size={11} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
||||||
@@ -303,7 +304,7 @@ export function BacktestOverviewTab({
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '总交易数' : 'Total Trades'}
|
{t('backtestOverview.totalTrades', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
||||||
{metrics.trades ?? 0}
|
{metrics.trades ?? 0}
|
||||||
@@ -311,7 +312,7 @@ export function BacktestOverviewTab({
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '最佳币种' : 'Best Symbol'}
|
{t('backtestOverview.bestSymbol', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold" style={{ color: '#0ECB81' }}>
|
<div className="text-lg font-bold" style={{ color: '#0ECB81' }}>
|
||||||
{metrics.best_symbol?.replace('USDT', '') || '-'}
|
{metrics.best_symbol?.replace('USDT', '') || '-'}
|
||||||
|
|||||||
@@ -244,9 +244,9 @@ export function BacktestPage() {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!selectedRunId) return
|
if (!selectedRunId) return
|
||||||
const confirmed = await confirmToast(tr('toasts.confirmDelete', { id: selectedRunId }), {
|
const confirmed = await confirmToast(tr('toasts.confirmDelete', { id: selectedRunId }), {
|
||||||
title: language === 'zh' ? '确认删除' : 'Confirm Delete',
|
title: t('backtestPageExtra.confirmDelete', language),
|
||||||
okText: language === 'zh' ? '删除' : 'Delete',
|
okText: t('backtestPageExtra.delete', language),
|
||||||
cancelText: language === 'zh' ? '取消' : 'Cancel',
|
cancelText: t('backtestPageExtra.cancel', language),
|
||||||
})
|
})
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
try {
|
try {
|
||||||
@@ -328,7 +328,7 @@ export function BacktestPage() {
|
|||||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||||
>
|
>
|
||||||
<Play className="w-4 h-4" />
|
<Play className="w-4 h-4" />
|
||||||
{language === 'zh' ? '新建回测' : 'New Backtest'}
|
{t('backtestPageExtra.newBacktest', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -474,14 +474,14 @@ export function BacktestPage() {
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={Target}
|
icon={Target}
|
||||||
label={language === 'zh' ? '当前净值' : 'Equity'}
|
label={t('backtestPageExtra.equity', language)}
|
||||||
value={(status?.equity ?? 0).toFixed(2)}
|
value={(status?.equity ?? 0).toFixed(2)}
|
||||||
suffix="USDT"
|
suffix="USDT"
|
||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={TrendingUp}
|
icon={TrendingUp}
|
||||||
label={language === 'zh' ? '总收益率' : 'Return'}
|
label={t('backtestPageExtra.totalReturn', language)}
|
||||||
value={`${(metrics?.total_return_pct ?? 0).toFixed(2)}%`}
|
value={`${(metrics?.total_return_pct ?? 0).toFixed(2)}%`}
|
||||||
trend={(metrics?.total_return_pct ?? 0) >= 0 ? 'up' : 'down'}
|
trend={(metrics?.total_return_pct ?? 0) >= 0 ? 'up' : 'down'}
|
||||||
color={(metrics?.total_return_pct ?? 0) >= 0 ? '#0ECB81' : '#F6465D'}
|
color={(metrics?.total_return_pct ?? 0) >= 0 ? '#0ECB81' : '#F6465D'}
|
||||||
@@ -490,7 +490,7 @@ export function BacktestPage() {
|
|||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={AlertTriangle}
|
icon={AlertTriangle}
|
||||||
label={language === 'zh' ? '最大回撤' : 'Max DD'}
|
label={t('backtestPageExtra.maxDD', language)}
|
||||||
value={`${(metrics?.max_drawdown_pct ?? 0).toFixed(2)}%`}
|
value={`${(metrics?.max_drawdown_pct ?? 0).toFixed(2)}%`}
|
||||||
color="#F6465D"
|
color="#F6465D"
|
||||||
metricKey="max_drawdown"
|
metricKey="max_drawdown"
|
||||||
@@ -498,7 +498,7 @@ export function BacktestPage() {
|
|||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={BarChart3}
|
icon={BarChart3}
|
||||||
label={language === 'zh' ? '夏普比率' : 'Sharpe'}
|
label={t('backtestPageExtra.sharpe', language)}
|
||||||
value={(metrics?.sharpe_ratio ?? 0).toFixed(2)}
|
value={(metrics?.sharpe_ratio ?? 0).toFixed(2)}
|
||||||
metricKey="sharpe_ratio"
|
metricKey="sharpe_ratio"
|
||||||
language={language}
|
language={language}
|
||||||
@@ -516,20 +516,12 @@ export function BacktestPage() {
|
|||||||
style={{ color: viewTab === tab ? '#F0B90B' : '#848E9C' }}
|
style={{ color: viewTab === tab ? '#F0B90B' : '#848E9C' }}
|
||||||
>
|
>
|
||||||
{tab === 'overview'
|
{tab === 'overview'
|
||||||
? language === 'zh'
|
? t('backtestPageExtra.tabOverview', language)
|
||||||
? '概览'
|
|
||||||
: 'Overview'
|
|
||||||
: tab === 'chart'
|
: tab === 'chart'
|
||||||
? language === 'zh'
|
? t('backtestPageExtra.tabChart', language)
|
||||||
? '图表'
|
|
||||||
: 'Chart'
|
|
||||||
: tab === 'trades'
|
: tab === 'trades'
|
||||||
? language === 'zh'
|
? t('backtestPageExtra.tabTrades', language)
|
||||||
? '交易'
|
: t('backtestPageExtra.tabDecisions', language)}
|
||||||
: 'Trades'
|
|
||||||
: language === 'zh'
|
|
||||||
? 'AI决策'
|
|
||||||
: 'Decisions'}
|
|
||||||
{viewTab === tab && (
|
{viewTab === tab && (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId="tab-indicator"
|
layoutId="tab-indicator"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Layers,
|
Layers,
|
||||||
Eye,
|
Eye,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { t, type Language } from '../../i18n/translations'
|
||||||
|
|
||||||
// ============ Types ============
|
// ============ Types ============
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ interface BacktestRunListProps {
|
|||||||
runs: BacktestRunItem[]
|
runs: BacktestRunItem[]
|
||||||
selectedRunId: string | undefined
|
selectedRunId: string | undefined
|
||||||
compareRunIds: string[]
|
compareRunIds: string[]
|
||||||
language: string
|
language: Language
|
||||||
tr: (key: string, params?: Record<string, string | number>) => string
|
tr: (key: string, params?: Record<string, string | number>) => string
|
||||||
onSelectRun: (runId: string) => void
|
onSelectRun: (runId: string) => void
|
||||||
onToggleCompare: (runId: string) => void
|
onToggleCompare: (runId: string) => void
|
||||||
@@ -84,7 +85,7 @@ export function BacktestRunList({
|
|||||||
{tr('runList.title')}
|
{tr('runList.title')}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-xs" style={{ color: '#848E9C' }}>
|
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{runs.length} {language === 'zh' ? '条' : 'runs'}
|
{runs.length} {t('backtestPageExtra.runs', language)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -131,7 +132,7 @@ export function BacktestRunList({
|
|||||||
? 'rgba(240,185,11,0.2)'
|
? 'rgba(240,185,11,0.2)'
|
||||||
: 'transparent',
|
: 'transparent',
|
||||||
}}
|
}}
|
||||||
title={language === 'zh' ? '添加到对比' : 'Add to compare'}
|
title={t('backtestPageExtra.addToCompare', language)}
|
||||||
>
|
>
|
||||||
<Eye
|
<Eye
|
||||||
className="w-3 h-3"
|
className="w-3 h-3"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from 'lightweight-charts'
|
} from 'lightweight-charts'
|
||||||
import { useLanguage } from '../../contexts/LanguageContext'
|
import { useLanguage } from '../../contexts/LanguageContext'
|
||||||
import { httpClient } from '../../lib/httpClient'
|
import { httpClient } from '../../lib/httpClient'
|
||||||
|
import { t } from '../../i18n/translations'
|
||||||
import {
|
import {
|
||||||
calculateSMA,
|
calculateSMA,
|
||||||
calculateEMA,
|
calculateEMA,
|
||||||
@@ -20,26 +21,26 @@ import {
|
|||||||
} from '../../utils/indicators'
|
} from '../../utils/indicators'
|
||||||
import { Settings, BarChart2 } from 'lucide-react'
|
import { Settings, BarChart2 } from 'lucide-react'
|
||||||
|
|
||||||
// 订单接口定义
|
// Order marker interface
|
||||||
interface OrderMarker {
|
interface OrderMarker {
|
||||||
time: number
|
time: number
|
||||||
price: number
|
price: number
|
||||||
side: 'long' | 'short'
|
side: 'long' | 'short'
|
||||||
rawSide: string // 原始 side 字段 (buy/sell from database)
|
rawSide: string // Original side field (buy/sell from database)
|
||||||
action: 'open' | 'close'
|
action: 'open' | 'close'
|
||||||
pnl?: number
|
pnl?: number
|
||||||
symbol: string
|
symbol: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 挂单接口定义 (交易所的止盈止损订单)
|
// Open orders interface (exchange TP/SL orders)
|
||||||
interface OpenOrder {
|
interface OpenOrder {
|
||||||
order_id: string
|
order_id: string
|
||||||
symbol: string
|
symbol: string
|
||||||
side: string // BUY/SELL
|
side: string // BUY/SELL
|
||||||
position_side: string // LONG/SHORT
|
position_side: string // LONG/SHORT
|
||||||
type: string // LIMIT/STOP_MARKET/TAKE_PROFIT_MARKET
|
type: string // LIMIT/STOP_MARKET/TAKE_PROFIT_MARKET
|
||||||
price: number // 限价单价格
|
price: number // Limit order price
|
||||||
stop_price: number // 触发价格 (止损/止盈)
|
stop_price: number // Trigger price (SL/TP)
|
||||||
quantity: number
|
quantity: number
|
||||||
status: string
|
status: string
|
||||||
}
|
}
|
||||||
@@ -49,11 +50,11 @@ interface AdvancedChartProps {
|
|||||||
interval?: string
|
interval?: string
|
||||||
traderID?: string
|
traderID?: string
|
||||||
height?: number
|
height?: number
|
||||||
exchange?: string // 交易所类型:binance, bybit, okx, bitget, hyperliquid, aster, lighter
|
exchange?: string // Exchange type: binance, bybit, okx, bitget, hyperliquid, aster, lighter
|
||||||
onSymbolChange?: (symbol: string) => void // 币种切换回调
|
onSymbolChange?: (symbol: string) => void // Symbol change callback
|
||||||
}
|
}
|
||||||
|
|
||||||
// 指标配置
|
// Indicator configuration
|
||||||
interface IndicatorConfig {
|
interface IndicatorConfig {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -62,31 +63,31 @@ interface IndicatorConfig {
|
|||||||
params?: any
|
params?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取成交额货币单位
|
// Get quote currency unit
|
||||||
const getQuoteUnit = (exchange: string): string => {
|
const getQuoteUnit = (exchange: string): string => {
|
||||||
if (['alpaca'].includes(exchange)) {
|
if (['alpaca'].includes(exchange)) {
|
||||||
return 'USD'
|
return 'USD'
|
||||||
}
|
}
|
||||||
if (['forex', 'metals'].includes(exchange)) {
|
if (['forex', 'metals'].includes(exchange)) {
|
||||||
return '' // 外汇/贵金属没有真实成交量
|
return '' // Forex/metals have no real volume
|
||||||
}
|
}
|
||||||
return 'USDT' // 加密货币默认 USDT
|
return 'USDT' // Crypto defaults to USDT
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取成交量数量单位
|
// Get base volume unit
|
||||||
const getBaseUnit = (exchange: string, symbol: string): string => {
|
const getBaseUnit = (exchange: string, symbol: string, language: string): string => {
|
||||||
if (['alpaca'].includes(exchange)) {
|
if (['alpaca'].includes(exchange)) {
|
||||||
return '股'
|
return t('advancedChart.shares', language as 'en' | 'zh' | 'id')
|
||||||
}
|
}
|
||||||
if (['forex', 'metals'].includes(exchange)) {
|
if (['forex', 'metals'].includes(exchange)) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
// 加密货币:从 symbol 提取基础资产
|
// Crypto: extract base asset from symbol
|
||||||
const base = symbol.replace(/USDT$|USD$|BUSD$/, '')
|
const base = symbol.replace(/USDT$|USD$|BUSD$/, '')
|
||||||
return base || '个'
|
return base || t('advancedChart.units', language as 'en' | 'zh' | 'id')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化大数字
|
// Format large numbers
|
||||||
const formatVolume = (value: number): string => {
|
const formatVolume = (value: number): string => {
|
||||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B'
|
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B'
|
||||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M'
|
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M'
|
||||||
@@ -99,43 +100,43 @@ export function AdvancedChart({
|
|||||||
interval = '5m',
|
interval = '5m',
|
||||||
traderID,
|
traderID,
|
||||||
height = 550,
|
height = 550,
|
||||||
exchange = 'binance', // 默认使用 binance
|
exchange = 'binance', // Default to binance
|
||||||
onSymbolChange: _onSymbolChange, // Available for future use
|
onSymbolChange: _onSymbolChange, // Available for future use
|
||||||
}: AdvancedChartProps) {
|
}: AdvancedChartProps) {
|
||||||
void _onSymbolChange // Prevent unused warning
|
void _onSymbolChange // Prevent unused warning
|
||||||
const { language } = useLanguage()
|
const { language } = useLanguage()
|
||||||
const quoteUnit = getQuoteUnit(exchange)
|
const quoteUnit = getQuoteUnit(exchange)
|
||||||
const baseUnit = getBaseUnit(exchange, symbol)
|
const baseUnit = getBaseUnit(exchange, symbol, language)
|
||||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const chartRef = useRef<IChartApi | null>(null)
|
const chartRef = useRef<IChartApi | null>(null)
|
||||||
const candlestickSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null)
|
const candlestickSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null)
|
||||||
const volumeSeriesRef = useRef<ISeriesApi<'Histogram'> | null>(null)
|
const volumeSeriesRef = useRef<ISeriesApi<'Histogram'> | null>(null)
|
||||||
const indicatorSeriesRef = useRef<Map<string, ISeriesApi<any>>>(new Map())
|
const indicatorSeriesRef = useRef<Map<string, ISeriesApi<any>>>(new Map())
|
||||||
const seriesMarkersRef = useRef<any>(null) // Markers primitive for v5
|
const seriesMarkersRef = useRef<any>(null) // Markers primitive for v5
|
||||||
const currentMarkersDataRef = useRef<any[]>([]) // 存储当前的标记数据
|
const currentMarkersDataRef = useRef<any[]>([]) // Store current marker data
|
||||||
const klineDataRef = useRef<Map<number, { volume: number; quoteVolume: number }>>(new Map()) // 存储 kline 额外数据
|
const klineDataRef = useRef<Map<number, { volume: number; quoteVolume: number }>>(new Map()) // Store kline extra data
|
||||||
const priceLinesRef = useRef<any[]>([]) // 存储挂单价格线
|
const priceLinesRef = useRef<any[]>([]) // Store open order price lines
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [showIndicatorPanel, setShowIndicatorPanel] = useState(false)
|
const [showIndicatorPanel, setShowIndicatorPanel] = useState(false)
|
||||||
const [showOrderMarkers, setShowOrderMarkers] = useState(true) // 订单标记显示开关,默认显示
|
const [showOrderMarkers, setShowOrderMarkers] = useState(true) // Order marker toggle, default on
|
||||||
const isInitialLoadRef = useRef(true) // 跟踪是否为初始加载
|
const isInitialLoadRef = useRef(true) // Track if this is initial load
|
||||||
const [tooltipData, setTooltipData] = useState<any>(null)
|
const [tooltipData, setTooltipData] = useState<any>(null)
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// 行情统计数据(当前K线)
|
// Market stats (current candle)
|
||||||
const [marketStats, setMarketStats] = useState<{
|
const [marketStats, setMarketStats] = useState<{
|
||||||
price: number
|
price: number
|
||||||
priceChange: number
|
priceChange: number
|
||||||
priceChangePercent: number
|
priceChangePercent: number
|
||||||
high: number
|
high: number
|
||||||
low: number
|
low: number
|
||||||
volume: number // 数量(BTC/股数)
|
volume: number // Quantity (BTC/shares)
|
||||||
quoteVolume: number // 成交额(USDT/USD)
|
quoteVolume: number // Turnover (USDT/USD)
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
// 指标配置
|
// Indicator configuration
|
||||||
const [indicators, setIndicators] = useState<IndicatorConfig[]>([
|
const [indicators, setIndicators] = useState<IndicatorConfig[]>([
|
||||||
{ id: 'volume', name: 'Volume', enabled: true, color: '#3B82F6' },
|
{ id: 'volume', name: 'Volume', enabled: true, color: '#3B82F6' },
|
||||||
{ id: 'ma5', name: 'MA5', enabled: false, color: '#FF6B6B', params: { period: 5 } },
|
{ id: 'ma5', name: 'MA5', enabled: false, color: '#FF6B6B', params: { period: 5 } },
|
||||||
@@ -147,7 +148,7 @@ export function AdvancedChart({
|
|||||||
{ id: 'bb', name: 'Bollinger Bands', enabled: false, color: '#9B59B6' },
|
{ id: 'bb', name: 'Bollinger Bands', enabled: false, color: '#9B59B6' },
|
||||||
])
|
])
|
||||||
|
|
||||||
// 从服务获取K线数据
|
// Fetch kline data from service
|
||||||
const fetchKlineData = async (symbol: string, interval: string) => {
|
const fetchKlineData = async (symbol: string, interval: string) => {
|
||||||
try {
|
try {
|
||||||
const limit = 1500
|
const limit = 1500
|
||||||
@@ -158,18 +159,18 @@ export function AdvancedChart({
|
|||||||
throw new Error('Failed to fetch kline data')
|
throw new Error('Failed to fetch kline data')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换数据格式
|
// Convert data format
|
||||||
const rawData = result.data.map((candle: any) => ({
|
const rawData = result.data.map((candle: any) => ({
|
||||||
time: Math.floor(candle.openTime / 1000) as UTCTimestamp,
|
time: Math.floor(candle.openTime / 1000) as UTCTimestamp,
|
||||||
open: candle.open,
|
open: candle.open,
|
||||||
high: candle.high,
|
high: candle.high,
|
||||||
low: candle.low,
|
low: candle.low,
|
||||||
close: candle.close,
|
close: candle.close,
|
||||||
volume: candle.volume, // 数量(BTC/股数)
|
volume: candle.volume, // Quantity (BTC/shares)
|
||||||
quoteVolume: candle.quoteVolume, // 成交额(USDT/USD)
|
quoteVolume: candle.quoteVolume, // Turnover (USDT/USD)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 按时间排序并去重(lightweight-charts 要求数据按时间升序且无重复)
|
// Sort by time and deduplicate (lightweight-charts requires ascending, unique times)
|
||||||
const sortedData = rawData.sort((a: any, b: any) => a.time - b.time)
|
const sortedData = rawData.sort((a: any, b: any) => a.time - b.time)
|
||||||
const dedupedData = sortedData.filter((item: any, index: number, arr: any[]) =>
|
const dedupedData = sortedData.filter((item: any, index: number, arr: any[]) =>
|
||||||
index === 0 || item.time !== arr[index - 1].time
|
index === 0 || item.time !== arr[index - 1].time
|
||||||
@@ -186,16 +187,16 @@ export function AdvancedChart({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析时间:支持 Unix 时间戳(数字)或字符串格式
|
// Parse time: supports Unix timestamp (number) or string format
|
||||||
const parseCustomTime = (time: any): number => {
|
const parseCustomTime = (time: any): number => {
|
||||||
if (!time) {
|
if (!time) {
|
||||||
console.warn('[AdvancedChart] Empty time value')
|
console.warn('[AdvancedChart] Empty time value')
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果已经是数字(Unix 时间戳)
|
// If already a number (Unix timestamp)
|
||||||
if (typeof time === 'number') {
|
if (typeof time === 'number') {
|
||||||
// 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳)
|
// Determine ms vs seconds: if > 10^12, treat as milliseconds
|
||||||
if (time > 1000000000000) {
|
if (time > 1000000000000) {
|
||||||
const seconds = Math.floor(time / 1000)
|
const seconds = Math.floor(time / 1000)
|
||||||
console.log('[AdvancedChart] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
|
console.log('[AdvancedChart] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
|
||||||
@@ -208,7 +209,7 @@ export function AdvancedChart({
|
|||||||
const timeStr = String(time)
|
const timeStr = String(time)
|
||||||
console.log('[AdvancedChart] Parsing time string:', timeStr)
|
console.log('[AdvancedChart] Parsing time string:', timeStr)
|
||||||
|
|
||||||
// 尝试标准ISO格式
|
// Try standard ISO format
|
||||||
const isoTime = new Date(timeStr).getTime()
|
const isoTime = new Date(timeStr).getTime()
|
||||||
if (!isNaN(isoTime) && isoTime > 0) {
|
if (!isNaN(isoTime) && isoTime > 0) {
|
||||||
const timestamp = Math.floor(isoTime / 1000)
|
const timestamp = Math.floor(isoTime / 1000)
|
||||||
@@ -216,7 +217,7 @@ export function AdvancedChart({
|
|||||||
return timestamp
|
return timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析自定义格式 "MM-DD HH:mm UTC" (兼容旧数据)
|
// Parse custom format "MM-DD HH:mm UTC" (for legacy data)
|
||||||
const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
|
const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
@@ -237,11 +238,11 @@ export function AdvancedChart({
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取订单数据
|
// Fetch order data
|
||||||
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
|
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
|
||||||
try {
|
try {
|
||||||
console.log('[AdvancedChart] Fetching orders for trader:', traderID, 'symbol:', symbol)
|
console.log('[AdvancedChart] Fetching orders for trader:', traderID, 'symbol:', symbol)
|
||||||
// 获取已成交的订单,增加到200条以显示更多历史订单
|
// Fetch filled orders, up to 200 for more history
|
||||||
const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=200`)
|
const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=200`)
|
||||||
|
|
||||||
console.log('[AdvancedChart] Orders API response:', result)
|
console.log('[AdvancedChart] Orders API response:', result)
|
||||||
@@ -258,14 +259,14 @@ export function AdvancedChart({
|
|||||||
orders.forEach((order: any) => {
|
orders.forEach((order: any) => {
|
||||||
console.log('[AdvancedChart] Processing order:', order)
|
console.log('[AdvancedChart] Processing order:', order)
|
||||||
|
|
||||||
// 处理字段名:支持PascalCase和snake_case
|
// Handle field names: support PascalCase and snake_case
|
||||||
const filledAt = order.filled_at || order.FilledAt || order.created_at || order.CreatedAt
|
const filledAt = order.filled_at || order.FilledAt || order.created_at || order.CreatedAt
|
||||||
const avgPrice = order.avg_fill_price || order.AvgFillPrice || order.price || order.Price
|
const avgPrice = order.avg_fill_price || order.AvgFillPrice || order.price || order.Price
|
||||||
const orderAction = order.order_action || order.OrderAction
|
const orderAction = order.order_action || order.OrderAction
|
||||||
const side = (order.side || order.Side)?.toLowerCase() // BUY/SELL
|
const side = (order.side || order.Side)?.toLowerCase() // BUY/SELL
|
||||||
const symbol = order.symbol || order.Symbol
|
const symbol = order.symbol || order.Symbol
|
||||||
|
|
||||||
// 跳过没有成交时间或价格的订单
|
// Skip orders without fill time or price
|
||||||
if (!filledAt || !avgPrice || avgPrice === 0) {
|
if (!filledAt || !avgPrice || avgPrice === 0) {
|
||||||
console.warn('[AdvancedChart] Skipping order - missing data:', { filledAt, avgPrice })
|
console.warn('[AdvancedChart] Skipping order - missing data:', { filledAt, avgPrice })
|
||||||
return
|
return
|
||||||
@@ -277,7 +278,7 @@ export function AdvancedChart({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据 order_action 判断是开仓还是平仓
|
// Determine open/close from order_action
|
||||||
let action: 'open' | 'close' = 'open'
|
let action: 'open' | 'close' = 'open'
|
||||||
let positionSide: 'long' | 'short' = 'long'
|
let positionSide: 'long' | 'short' = 'long'
|
||||||
|
|
||||||
@@ -290,7 +291,7 @@ export function AdvancedChart({
|
|||||||
positionSide = orderAction.includes('LONG') ? 'long' : 'short'
|
positionSide = orderAction.includes('LONG') ? 'long' : 'short'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有 order_action,根据 side 判断
|
// If no order_action, infer from side
|
||||||
positionSide = side === 'buy' ? 'long' : 'short'
|
positionSide = side === 'buy' ? 'long' : 'short'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +308,7 @@ export function AdvancedChart({
|
|||||||
time: timeSeconds,
|
time: timeSeconds,
|
||||||
price: avgPrice,
|
price: avgPrice,
|
||||||
side: positionSide,
|
side: positionSide,
|
||||||
rawSide: side, // 原始 side 字段 (buy/sell)
|
rawSide: side, // Original side field (buy/sell)
|
||||||
action: action,
|
action: action,
|
||||||
symbol,
|
symbol,
|
||||||
})
|
})
|
||||||
@@ -321,7 +322,7 @@ export function AdvancedChart({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取交易所挂单 (止盈止损订单)
|
// Fetch exchange open orders (TP/SL)
|
||||||
const fetchOpenOrders = async (traderID: string, symbol: string): Promise<OpenOrder[]> => {
|
const fetchOpenOrders = async (traderID: string, symbol: string): Promise<OpenOrder[]> => {
|
||||||
try {
|
try {
|
||||||
console.log('[AdvancedChart] Fetching open orders for trader:', traderID, 'symbol:', symbol)
|
console.log('[AdvancedChart] Fetching open orders for trader:', traderID, 'symbol:', symbol)
|
||||||
@@ -341,7 +342,7 @@ export function AdvancedChart({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化图表
|
// Initialize chart
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartContainerRef.current) return
|
if (!chartContainerRef.current) return
|
||||||
|
|
||||||
@@ -424,7 +425,7 @@ export function AdvancedChart({
|
|||||||
|
|
||||||
chartRef.current = chart
|
chartRef.current = chart
|
||||||
|
|
||||||
// 创建K线系列
|
// Create candlestick series
|
||||||
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||||
upColor: '#0ECB81',
|
upColor: '#0ECB81',
|
||||||
downColor: '#F6465D',
|
downColor: '#F6465D',
|
||||||
@@ -435,7 +436,7 @@ export function AdvancedChart({
|
|||||||
})
|
})
|
||||||
candlestickSeriesRef.current = candlestickSeries as any
|
candlestickSeriesRef.current = candlestickSeries as any
|
||||||
|
|
||||||
// 创建成交量系列
|
// Create volume series
|
||||||
const volumeSeries = chart.addSeries(HistogramSeries, {
|
const volumeSeries = chart.addSeries(HistogramSeries, {
|
||||||
color: '#26a69a',
|
color: '#26a69a',
|
||||||
priceFormat: {
|
priceFormat: {
|
||||||
@@ -447,7 +448,7 @@ export function AdvancedChart({
|
|||||||
})
|
})
|
||||||
volumeSeriesRef.current = volumeSeries as any
|
volumeSeriesRef.current = volumeSeries as any
|
||||||
|
|
||||||
// 响应式调整 (ResizeObserver)
|
// Responsive resize (ResizeObserver)
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
if (entries.length === 0 || !entries[0].contentRect) return
|
if (entries.length === 0 || !entries[0].contentRect) return
|
||||||
const { width, height } = entries[0].contentRect
|
const { width, height } = entries[0].contentRect
|
||||||
@@ -458,7 +459,7 @@ export function AdvancedChart({
|
|||||||
resizeObserver.observe(chartContainerRef.current)
|
resizeObserver.observe(chartContainerRef.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听鼠标移动,显示 OHLC 信息
|
// Listen for crosshair movement to show OHLC info
|
||||||
chart.subscribeCrosshairMove((param) => {
|
chart.subscribeCrosshairMove((param) => {
|
||||||
if (!param.time || !param.point || !candlestickSeriesRef.current) {
|
if (!param.time || !param.point || !candlestickSeriesRef.current) {
|
||||||
setTooltipData(null)
|
setTooltipData(null)
|
||||||
@@ -473,7 +474,7 @@ export function AdvancedChart({
|
|||||||
|
|
||||||
const candleData = data as any
|
const candleData = data as any
|
||||||
|
|
||||||
// 从存储的数据中获取 volume 和 quoteVolume
|
// Get volume and quoteVolume from stored data
|
||||||
const klineExtra = klineDataRef.current.get(param.time as number) || { volume: 0, quoteVolume: 0 }
|
const klineExtra = klineDataRef.current.get(param.time as number) || { volume: 0, quoteVolume: 0 }
|
||||||
|
|
||||||
setTooltipData({
|
setTooltipData({
|
||||||
@@ -496,18 +497,18 @@ export function AdvancedChart({
|
|||||||
}, []) // Chart is created once, ResizeObserver handles dimension changes
|
}, []) // Chart is created once, ResizeObserver handles dimension changes
|
||||||
|
|
||||||
|
|
||||||
// 加载数据和指标
|
// Load data and indicators
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 当 symbol 或 interval 改变时,重置初始加载标志(以便自动适配新数据)
|
// Reset initial load flag when symbol/interval changes (for auto-fit)
|
||||||
isInitialLoadRef.current = true
|
isInitialLoadRef.current = true
|
||||||
|
|
||||||
// 清除旧的标记数据,避免旧数据影响新图表
|
// Clear old marker data to prevent stale data in new chart
|
||||||
currentMarkersDataRef.current = []
|
currentMarkersDataRef.current = []
|
||||||
if (seriesMarkersRef.current) {
|
if (seriesMarkersRef.current) {
|
||||||
try {
|
try {
|
||||||
seriesMarkersRef.current.setMarkers([])
|
seriesMarkersRef.current.setMarkers([])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 忽略错误,稍后会重新创建
|
// Ignore errors, will be recreated later
|
||||||
}
|
}
|
||||||
seriesMarkersRef.current = null
|
seriesMarkersRef.current = null
|
||||||
}
|
}
|
||||||
@@ -516,30 +517,30 @@ export function AdvancedChart({
|
|||||||
if (!candlestickSeriesRef.current) return
|
if (!candlestickSeriesRef.current) return
|
||||||
|
|
||||||
console.log('[AdvancedChart] Loading data for', symbol, interval, isRefresh ? '(refresh)' : '')
|
console.log('[AdvancedChart] Loading data for', symbol, interval, isRefresh ? '(refresh)' : '')
|
||||||
// 只在首次加载时显示 loading,刷新时不显示避免闪烁
|
// Only show loading on first load, avoid flicker on refresh
|
||||||
if (!isRefresh) {
|
if (!isRefresh) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
}
|
}
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 获取K线数据
|
// 1. Fetch kline data
|
||||||
const klineData = await fetchKlineData(symbol, interval)
|
const klineData = await fetchKlineData(symbol, interval)
|
||||||
console.log('[AdvancedChart] Loaded', klineData.length, 'klines')
|
console.log('[AdvancedChart] Loaded', klineData.length, 'klines')
|
||||||
candlestickSeriesRef.current.setData(klineData)
|
candlestickSeriesRef.current.setData(klineData)
|
||||||
|
|
||||||
// 存储 volume/quoteVolume 数据供 tooltip 使用
|
// Store volume/quoteVolume data for tooltip
|
||||||
klineDataRef.current.clear()
|
klineDataRef.current.clear()
|
||||||
klineData.forEach((k: any) => {
|
klineData.forEach((k: any) => {
|
||||||
klineDataRef.current.set(k.time, { volume: k.volume || 0, quoteVolume: k.quoteVolume || 0 })
|
klineDataRef.current.set(k.time, { volume: k.volume || 0, quoteVolume: k.quoteVolume || 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
// 1.5 计算行情统计数据
|
// 1.5 Calculate market stats
|
||||||
if (klineData.length > 1) {
|
if (klineData.length > 1) {
|
||||||
const latestKline = klineData[klineData.length - 1]
|
const latestKline = klineData[klineData.length - 1]
|
||||||
const prevKline = klineData[klineData.length - 2]
|
const prevKline = klineData[klineData.length - 2]
|
||||||
|
|
||||||
// 涨跌幅:当前K线收盘价 vs 前一根K线收盘价
|
// Price change: current candle close vs previous candle close
|
||||||
const priceChange = latestKline.close - prevKline.close
|
const priceChange = latestKline.close - prevKline.close
|
||||||
const priceChangePercent = (priceChange / prevKline.close) * 100
|
const priceChangePercent = (priceChange / prevKline.close) * 100
|
||||||
|
|
||||||
@@ -565,7 +566,7 @@ export function AdvancedChart({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 显示成交量
|
// 2. Display volume
|
||||||
if (volumeSeriesRef.current) {
|
if (volumeSeriesRef.current) {
|
||||||
const volumeEnabled = indicators.find(i => i.id === 'volume')?.enabled
|
const volumeEnabled = indicators.find(i => i.id === 'volume')?.enabled
|
||||||
if (volumeEnabled) {
|
if (volumeEnabled) {
|
||||||
@@ -576,15 +577,15 @@ export function AdvancedChart({
|
|||||||
}))
|
}))
|
||||||
volumeSeriesRef.current.setData(volumeData)
|
volumeSeriesRef.current.setData(volumeData)
|
||||||
} else {
|
} else {
|
||||||
// 关闭成交量时清空数据
|
// Clear data when volume is disabled
|
||||||
volumeSeriesRef.current.setData([])
|
volumeSeriesRef.current.setData([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 添加指标
|
// 3. Add indicators
|
||||||
updateIndicators(klineData)
|
updateIndicators(klineData)
|
||||||
|
|
||||||
// 4. 获取并显示订单标记
|
// 4. Fetch and display order markers
|
||||||
if (traderID && candlestickSeriesRef.current) {
|
if (traderID && candlestickSeriesRef.current) {
|
||||||
console.log('[AdvancedChart] Starting to fetch orders...')
|
console.log('[AdvancedChart] Starting to fetch orders...')
|
||||||
const orders = await fetchOrders(traderID, symbol)
|
const orders = await fetchOrders(traderID, symbol)
|
||||||
@@ -593,17 +594,17 @@ export function AdvancedChart({
|
|||||||
if (orders.length > 0) {
|
if (orders.length > 0) {
|
||||||
console.log('[AdvancedChart] Creating markers from', orders.length, 'orders')
|
console.log('[AdvancedChart] Creating markers from', orders.length, 'orders')
|
||||||
|
|
||||||
// 提取 K 线时间数组(已排序)
|
// Extract sorted kline time array
|
||||||
const klineTimes = klineData.map((k: any) => k.time as number)
|
const klineTimes = klineData.map((k: any) => k.time as number)
|
||||||
const klineMinTime = klineTimes[0] || 0
|
const klineMinTime = klineTimes[0] || 0
|
||||||
const klineMaxTime = klineTimes[klineTimes.length - 1] || 0
|
const klineMaxTime = klineTimes[klineTimes.length - 1] || 0
|
||||||
console.log('[AdvancedChart] Kline time range:', klineMinTime, '-', klineMaxTime, '(', klineTimes.length, 'candles)')
|
console.log('[AdvancedChart] Kline time range:', klineMinTime, '-', klineMaxTime, '(', klineTimes.length, 'candles)')
|
||||||
|
|
||||||
// 二分查找:找到订单时间所属的 K 线蜡烛
|
// Binary search: find the kline candle for the order time
|
||||||
// 返回 time <= orderTime 的最大 K 线时间
|
// Return the largest kline time <= orderTime
|
||||||
const findCandleTime = (orderTime: number): number | null => {
|
const findCandleTime = (orderTime: number): number | null => {
|
||||||
if (orderTime < klineMinTime || orderTime > klineMaxTime) {
|
if (orderTime < klineMinTime || orderTime > klineMaxTime) {
|
||||||
return null // 超出范围
|
return null // Out of range
|
||||||
}
|
}
|
||||||
|
|
||||||
let left = 0
|
let left = 0
|
||||||
@@ -621,11 +622,11 @@ export function AdvancedChart({
|
|||||||
return klineTimes[left]
|
return klineTimes[left]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按 K 线时间分组统计订单
|
// Group orders by kline time
|
||||||
const ordersByCandle = new Map<number, { buys: number; sells: number }>()
|
const ordersByCandle = new Map<number, { buys: number; sells: number }>()
|
||||||
|
|
||||||
orders.forEach(order => {
|
orders.forEach(order => {
|
||||||
// 使用二分查找找到对应的 K 线蜡烛时间
|
// Use binary search to find matching kline candle time
|
||||||
const candleTime = findCandleTime(order.time)
|
const candleTime = findCandleTime(order.time)
|
||||||
|
|
||||||
if (candleTime === null) {
|
if (candleTime === null) {
|
||||||
@@ -643,7 +644,7 @@ export function AdvancedChart({
|
|||||||
ordersByCandle.set(candleTime, existing)
|
ordersByCandle.set(candleTime, existing)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 为每个有订单的 K 线创建标记
|
// Create markers for each kline with orders
|
||||||
const markers: Array<{
|
const markers: Array<{
|
||||||
time: Time
|
time: Time
|
||||||
position: 'belowBar' | 'aboveBar'
|
position: 'belowBar' | 'aboveBar'
|
||||||
@@ -654,7 +655,7 @@ export function AdvancedChart({
|
|||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
ordersByCandle.forEach((counts, candleTime) => {
|
ordersByCandle.forEach((counts, candleTime) => {
|
||||||
// 显示买入标记(绿色,在K线下方)
|
// Show buy markers (green, below bar)
|
||||||
if (counts.buys > 0) {
|
if (counts.buys > 0) {
|
||||||
markers.push({
|
markers.push({
|
||||||
time: candleTime as Time,
|
time: candleTime as Time,
|
||||||
@@ -665,7 +666,7 @@ export function AdvancedChart({
|
|||||||
size: 1,
|
size: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 显示卖出标记(红色,在K线上方)
|
// Show sell markers (red, above bar)
|
||||||
if (counts.sells > 0) {
|
if (counts.sells > 0) {
|
||||||
markers.push({
|
markers.push({
|
||||||
time: candleTime as Time,
|
time: candleTime as Time,
|
||||||
@@ -678,7 +679,7 @@ export function AdvancedChart({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 按时间排序(lightweight-charts 要求标记按时间顺序)
|
// Sort by time (lightweight-charts requires chronological order)
|
||||||
markers.sort((a, b) => (a.time as number) - (b.time as number))
|
markers.sort((a, b) => (a.time as number) - (b.time as number))
|
||||||
|
|
||||||
console.log('[AdvancedChart] Valid markers:', markers.length, 'out of', orders.length)
|
console.log('[AdvancedChart] Valid markers:', markers.length, 'out of', orders.length)
|
||||||
@@ -687,17 +688,17 @@ export function AdvancedChart({
|
|||||||
console.log('[AdvancedChart] Markers data:', JSON.stringify(markers, null, 2))
|
console.log('[AdvancedChart] Markers data:', JSON.stringify(markers, null, 2))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 存储标记数据供后续切换使用
|
// Store marker data for later toggle use
|
||||||
currentMarkersDataRef.current = markers
|
currentMarkersDataRef.current = markers
|
||||||
|
|
||||||
// 使用 v5 API: createSeriesMarkers
|
// Using v5 API: createSeriesMarkers
|
||||||
const markersToShow = showOrderMarkers ? markers : []
|
const markersToShow = showOrderMarkers ? markers : []
|
||||||
|
|
||||||
if (seriesMarkersRef.current) {
|
if (seriesMarkersRef.current) {
|
||||||
// 如果已经存在,更新标记
|
// If already exists, update markers
|
||||||
seriesMarkersRef.current.setMarkers(markersToShow)
|
seriesMarkersRef.current.setMarkers(markersToShow)
|
||||||
} else {
|
} else {
|
||||||
// 首次创建标记
|
// First time creating markers
|
||||||
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markersToShow)
|
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markersToShow)
|
||||||
}
|
}
|
||||||
console.log('[AdvancedChart] ✅ Markers updated! Count:', markersToShow.length, 'Visible:', showOrderMarkers)
|
console.log('[AdvancedChart] ✅ Markers updated! Count:', markersToShow.length, 'Visible:', showOrderMarkers)
|
||||||
@@ -721,7 +722,7 @@ export function AdvancedChart({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只在初始加载时自动适配视图,避免刷新时抖动
|
// Auto-fit view only on initial load, avoid jitter on refresh
|
||||||
if (isInitialLoadRef.current) {
|
if (isInitialLoadRef.current) {
|
||||||
chartRef.current?.timeScale().fitContent()
|
chartRef.current?.timeScale().fitContent()
|
||||||
isInitialLoadRef.current = false
|
isInitialLoadRef.current = false
|
||||||
@@ -734,26 +735,26 @@ export function AdvancedChart({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData(false) // 首次加载
|
loadData(false) // Initial load
|
||||||
|
|
||||||
// 实时自动刷新 (5秒更新一次)
|
// Real-time auto-refresh (every 5 seconds)
|
||||||
const refreshInterval = setInterval(() => loadData(true), 5000)
|
const refreshInterval = setInterval(() => loadData(true), 5000)
|
||||||
return () => clearInterval(refreshInterval)
|
return () => clearInterval(refreshInterval)
|
||||||
}, [symbol, interval, traderID, exchange])
|
}, [symbol, interval, traderID, exchange])
|
||||||
|
|
||||||
// 单独刷新挂单价格线 (60秒刷新一次,避免频繁调用交易所API)
|
// Refresh open order price lines separately (every 60s, avoid frequent exchange API calls)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!traderID || !candlestickSeriesRef.current) return
|
if (!traderID || !candlestickSeriesRef.current) return
|
||||||
|
|
||||||
// 加载挂单并显示价格线
|
// Load open orders and display price lines
|
||||||
const loadOpenOrders = async () => {
|
const loadOpenOrders = async () => {
|
||||||
try {
|
try {
|
||||||
// 先清除旧的价格线
|
// Clear old price lines first
|
||||||
priceLinesRef.current.forEach(line => {
|
priceLinesRef.current.forEach(line => {
|
||||||
try {
|
try {
|
||||||
candlestickSeriesRef.current?.removePriceLine(line)
|
candlestickSeriesRef.current?.removePriceLine(line)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 忽略清除错误
|
// Ignore clear error
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
priceLinesRef.current = []
|
priceLinesRef.current = []
|
||||||
@@ -763,28 +764,28 @@ export function AdvancedChart({
|
|||||||
|
|
||||||
if (openOrders.length > 0 && candlestickSeriesRef.current) {
|
if (openOrders.length > 0 && candlestickSeriesRef.current) {
|
||||||
openOrders.forEach(order => {
|
openOrders.forEach(order => {
|
||||||
// 获取触发价格 (止损/止盈用 stop_price,限价单用 price)
|
// Get trigger price (SL/TP use stop_price, limit orders use price)
|
||||||
const linePrice = order.stop_price > 0 ? order.stop_price : order.price
|
const linePrice = order.stop_price > 0 ? order.stop_price : order.price
|
||||||
if (linePrice <= 0) return
|
if (linePrice <= 0) return
|
||||||
|
|
||||||
// 判断订单类型
|
// Determine order type
|
||||||
const isStopLoss = order.type.includes('STOP') || order.type.includes('SL')
|
const isStopLoss = order.type.includes('STOP') || order.type.includes('SL')
|
||||||
const isTakeProfit = order.type.includes('TAKE_PROFIT') || order.type.includes('TP')
|
const isTakeProfit = order.type.includes('TAKE_PROFIT') || order.type.includes('TP')
|
||||||
const isLimit = order.type === 'LIMIT'
|
const isLimit = order.type === 'LIMIT'
|
||||||
|
|
||||||
// 设置价格线样式
|
// Set price line style
|
||||||
let lineColor = '#F0B90B' // 默认黄色
|
let lineColor = '#F0B90B' // Default yellow
|
||||||
const lineStyle = 2 // 虚线
|
const lineStyle = 2 // dashed
|
||||||
let title = ''
|
let title = ''
|
||||||
|
|
||||||
if (isStopLoss) {
|
if (isStopLoss) {
|
||||||
lineColor = '#F6465D' // 红色 - 止损
|
lineColor = '#F6465D' // red - stop loss
|
||||||
title = `SL ${order.quantity}`
|
title = `SL ${order.quantity}`
|
||||||
} else if (isTakeProfit) {
|
} else if (isTakeProfit) {
|
||||||
lineColor = '#0ECB81' // 绿色 - 止盈
|
lineColor = '#0ECB81' // green - take profit
|
||||||
title = `TP ${order.quantity}`
|
title = `TP ${order.quantity}`
|
||||||
} else if (isLimit) {
|
} else if (isLimit) {
|
||||||
lineColor = '#F0B90B' // 黄色 - 限价单
|
lineColor = '#F0B90B' // yellow - limit order
|
||||||
title = `Limit ${order.side} ${order.quantity}`
|
title = `Limit ${order.side} ${order.quantity}`
|
||||||
} else {
|
} else {
|
||||||
title = `${order.type} ${order.quantity}`
|
title = `${order.type} ${order.quantity}`
|
||||||
@@ -810,10 +811,10 @@ export function AdvancedChart({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始加载 (延迟1秒等待图表初始化完成)
|
// Initial load (delay 1s to wait for chart initialization)
|
||||||
const initialTimeout = setTimeout(loadOpenOrders, 1000)
|
const initialTimeout = setTimeout(loadOpenOrders, 1000)
|
||||||
|
|
||||||
// 60秒刷新一次挂单
|
// Refresh open orders every 60 seconds
|
||||||
const openOrdersInterval = setInterval(loadOpenOrders, 60000)
|
const openOrdersInterval = setInterval(loadOpenOrders, 60000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -822,7 +823,7 @@ export function AdvancedChart({
|
|||||||
}
|
}
|
||||||
}, [symbol, traderID])
|
}, [symbol, traderID])
|
||||||
|
|
||||||
// 单独处理订单标记的显示/隐藏,避免重新加载数据
|
// Handle order marker show/hide separately to avoid reloading data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!seriesMarkersRef.current) return
|
if (!seriesMarkersRef.current) return
|
||||||
|
|
||||||
@@ -835,17 +836,17 @@ export function AdvancedChart({
|
|||||||
}
|
}
|
||||||
}, [showOrderMarkers])
|
}, [showOrderMarkers])
|
||||||
|
|
||||||
// 更新指标
|
// Update indicators
|
||||||
const updateIndicators = (klineData: Kline[]) => {
|
const updateIndicators = (klineData: Kline[]) => {
|
||||||
if (!chartRef.current) return
|
if (!chartRef.current) return
|
||||||
|
|
||||||
// 清除旧指标
|
// Clear old indicators
|
||||||
indicatorSeriesRef.current.forEach(series => {
|
indicatorSeriesRef.current.forEach(series => {
|
||||||
chartRef.current?.removeSeries(series as any)
|
chartRef.current?.removeSeries(series as any)
|
||||||
})
|
})
|
||||||
indicatorSeriesRef.current.clear()
|
indicatorSeriesRef.current.clear()
|
||||||
|
|
||||||
// 添加启用的指标
|
// Add enabled indicators
|
||||||
indicators.forEach(indicator => {
|
indicators.forEach(indicator => {
|
||||||
if (!indicator.enabled || !chartRef.current) return
|
if (!indicator.enabled || !chartRef.current) return
|
||||||
|
|
||||||
@@ -864,7 +865,7 @@ export function AdvancedChart({
|
|||||||
color: indicator.color,
|
color: indicator.color,
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
title: indicator.name,
|
title: indicator.name,
|
||||||
lineStyle: 2, // 虚线
|
lineStyle: 2, // dashed
|
||||||
})
|
})
|
||||||
series.setData(emaData as any)
|
series.setData(emaData as any)
|
||||||
indicatorSeriesRef.current.set(indicator.id, series)
|
indicatorSeriesRef.current.set(indicator.id, series)
|
||||||
@@ -900,7 +901,7 @@ export function AdvancedChart({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换指标
|
// Toggle indicator
|
||||||
const toggleIndicator = (id: string) => {
|
const toggleIndicator = (id: string) => {
|
||||||
setIndicators(prev =>
|
setIndicators(prev =>
|
||||||
prev.map(ind => (ind.id === id ? { ...ind, enabled: !ind.enabled } : ind))
|
prev.map(ind => (ind.id === id ? { ...ind, enabled: !ind.enabled } : ind))
|
||||||
@@ -980,7 +981,7 @@ export function AdvancedChart({
|
|||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{loading && (
|
{loading && (
|
||||||
<span className="text-[10px] text-yellow-400 animate-pulse mr-2">
|
<span className="text-[10px] text-yellow-400 animate-pulse mr-2">
|
||||||
{language === 'zh' ? '更新中...' : 'Updating...'}
|
{t('advancedChart.updating', language)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -992,7 +993,7 @@ export function AdvancedChart({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Settings className="w-3 h-3" />
|
<Settings className="w-3 h-3" />
|
||||||
<span>{language === 'zh' ? '指标' : 'Indicators'}</span>
|
<span>{t('advancedChart.indicators', language)}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -1002,14 +1003,14 @@ export function AdvancedChart({
|
|||||||
background: showOrderMarkers ? 'rgba(16, 185, 129, 0.15)' : 'transparent',
|
background: showOrderMarkers ? 'rgba(16, 185, 129, 0.15)' : 'transparent',
|
||||||
color: showOrderMarkers ? '#10B981' : '#6B7280',
|
color: showOrderMarkers ? '#10B981' : '#6B7280',
|
||||||
}}
|
}}
|
||||||
title={language === 'zh' ? '订单标记' : 'Order Markers'}
|
title={t('advancedChart.orderMarkers', language)}
|
||||||
>
|
>
|
||||||
<span>B/S</span>
|
<span>B/S</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 指标面板 - 专业化设计 */}
|
{/* Indicator panel - professional design */}
|
||||||
{showIndicatorPanel && (
|
{showIndicatorPanel && (
|
||||||
<div
|
<div
|
||||||
className="absolute top-16 right-4 z-10 rounded-lg shadow-2xl backdrop-blur-sm"
|
className="absolute top-16 right-4 z-10 rounded-lg shadow-2xl backdrop-blur-sm"
|
||||||
@@ -1021,7 +1022,7 @@ export function AdvancedChart({
|
|||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 标题栏 */}
|
{/* Title bar */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between px-4 py-3 border-b"
|
className="flex items-center justify-between px-4 py-3 border-b"
|
||||||
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
|
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
|
||||||
@@ -1029,7 +1030,7 @@ export function AdvancedChart({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BarChart2 className="w-4 h-4 text-yellow-400" />
|
<BarChart2 className="w-4 h-4 text-yellow-400" />
|
||||||
<h4 className="text-sm font-bold text-white">
|
<h4 className="text-sm font-bold text-white">
|
||||||
{language === 'zh' ? '技术指标' : 'Technical Indicators'}
|
{t('advancedChart.technicalIndicators', language)}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -1040,7 +1041,7 @@ export function AdvancedChart({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 指标列表 */}
|
{/* Indicator list */}
|
||||||
<div className="p-3 space-y-1">
|
<div className="p-3 space-y-1">
|
||||||
{indicators.map(indicator => (
|
{indicators.map(indicator => (
|
||||||
<label
|
<label
|
||||||
@@ -1069,17 +1070,17 @@ export function AdvancedChart({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部提示 */}
|
{/* Bottom hint */}
|
||||||
<div
|
<div
|
||||||
className="px-4 py-2 text-xs text-gray-500 border-t"
|
className="px-4 py-2 text-xs text-gray-500 border-t"
|
||||||
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
|
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
|
||||||
>
|
>
|
||||||
{language === 'zh' ? '点击选择需要显示的指标' : 'Click to toggle indicators'}
|
{t('advancedChart.clickToToggle', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 图表容器 */}
|
{/* Chart container */}
|
||||||
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
||||||
<div ref={chartContainerRef} style={{ height: '100%', width: '100%' }} />
|
<div ref={chartContainerRef} style={{ height: '100%', width: '100%' }} />
|
||||||
|
|
||||||
@@ -1151,7 +1152,7 @@ export function AdvancedChart({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* NOFX 水印 */}
|
{/* NOFX watermark */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -1177,7 +1178,7 @@ export function AdvancedChart({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 错误提示 */}
|
{/* Error message */}
|
||||||
{error && (
|
{error && (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 flex items-center justify-center"
|
className="absolute inset-0 flex items-center justify-center"
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { EquityChart } from './EquityChart'
|
|||||||
import { AdvancedChart } from './AdvancedChart'
|
import { AdvancedChart } from './AdvancedChart'
|
||||||
import { useLanguage } from '../../contexts/LanguageContext'
|
import { useLanguage } from '../../contexts/LanguageContext'
|
||||||
import { t } from '../../i18n/translations'
|
import { t } from '../../i18n/translations'
|
||||||
|
import { chartTabs, ts } from '../../i18n/strategy-translations'
|
||||||
import { BarChart3, CandlestickChart, ChevronDown, Search } from 'lucide-react'
|
import { BarChart3, CandlestickChart, ChevronDown, Search } from 'lucide-react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
|
||||||
interface ChartTabsProps {
|
interface ChartTabsProps {
|
||||||
traderId: string
|
traderId: string
|
||||||
selectedSymbol?: string // 从外部选择的币种
|
selectedSymbol?: string // Externally selected symbol
|
||||||
updateKey?: number // 强制更新的 key
|
updateKey?: number // Force update key
|
||||||
exchangeId?: string // 交易所ID
|
exchangeId?: string // Exchange ID
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartTab = 'equity' | 'kline'
|
type ChartTab = 'equity' | 'kline'
|
||||||
@@ -23,13 +24,13 @@ interface SymbolInfo {
|
|||||||
category: string
|
category: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 市场类型配置
|
// Market type configuration
|
||||||
const MARKET_CONFIG = {
|
const MARKET_CONFIG = {
|
||||||
hyperliquid: { exchange: 'hyperliquid', defaultSymbol: 'BTC', icon: '🔷', label: { zh: 'HL', en: 'HL' }, color: 'cyan', hasDropdown: true },
|
hyperliquid: { exchange: 'hyperliquid', defaultSymbol: 'BTC', icon: '🔷', labelKey: 'hyperliquid' as const, color: 'cyan', hasDropdown: true },
|
||||||
crypto: { exchange: 'binance', defaultSymbol: 'BTCUSDT', icon: '₿', label: { zh: '加密', en: 'Crypto' }, color: 'yellow', hasDropdown: false },
|
crypto: { exchange: 'binance', defaultSymbol: 'BTCUSDT', icon: '₿', labelKey: 'crypto' as const, color: 'yellow', hasDropdown: false },
|
||||||
stocks: { exchange: 'alpaca', defaultSymbol: 'AAPL', icon: '📈', label: { zh: '美股', en: 'Stocks' }, color: 'green', hasDropdown: false },
|
stocks: { exchange: 'alpaca', defaultSymbol: 'AAPL', icon: '📈', labelKey: 'stocks' as const, color: 'green', hasDropdown: false },
|
||||||
forex: { exchange: 'forex', defaultSymbol: 'EUR/USD', icon: '💱', label: { zh: '外汇', en: 'Forex' }, color: 'blue', hasDropdown: false },
|
forex: { exchange: 'forex', defaultSymbol: 'EUR/USD', icon: '💱', labelKey: 'forex' as const, color: 'blue', hasDropdown: false },
|
||||||
metals: { exchange: 'metals', defaultSymbol: 'XAU/USD', icon: '🥇', label: { zh: '金属', en: 'Metals' }, color: 'amber', hasDropdown: false },
|
metals: { exchange: 'metals', defaultSymbol: 'XAU/USD', icon: '🥇', labelKey: 'metals' as const, color: 'amber', hasDropdown: false },
|
||||||
}
|
}
|
||||||
|
|
||||||
const INTERVALS: { value: Interval; label: string }[] = [
|
const INTERVALS: { value: Interval; label: string }[] = [
|
||||||
@@ -42,12 +43,12 @@ const INTERVALS: { value: Interval; label: string }[] = [
|
|||||||
{ value: '1d', label: '1d' },
|
{ value: '1d', label: '1d' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 根据交易所ID推断市场类型
|
// Infer market type from exchange ID
|
||||||
function getMarketTypeFromExchange(exchangeId: string | undefined): MarketType {
|
function getMarketTypeFromExchange(exchangeId: string | undefined): MarketType {
|
||||||
if (!exchangeId) return 'hyperliquid'
|
if (!exchangeId) return 'hyperliquid'
|
||||||
const lower = exchangeId.toLowerCase()
|
const lower = exchangeId.toLowerCase()
|
||||||
if (lower.includes('hyperliquid')) return 'hyperliquid'
|
if (lower.includes('hyperliquid')) return 'hyperliquid'
|
||||||
// 其他交易所默认使用 crypto 类型
|
// Other exchanges default to crypto type
|
||||||
return 'crypto'
|
return 'crypto'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,25 +64,25 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
|||||||
const [searchFilter, setSearchFilter] = useState('')
|
const [searchFilter, setSearchFilter] = useState('')
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// 当交易所ID变化时,自动切换市场类型
|
// Auto-switch market type when exchange ID changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newMarketType = getMarketTypeFromExchange(exchangeId)
|
const newMarketType = getMarketTypeFromExchange(exchangeId)
|
||||||
setMarketType(newMarketType)
|
setMarketType(newMarketType)
|
||||||
}, [exchangeId])
|
}, [exchangeId])
|
||||||
|
|
||||||
// 根据市场类型确定交易所
|
// Determine exchange from market type
|
||||||
const marketConfig = MARKET_CONFIG[marketType]
|
const marketConfig = MARKET_CONFIG[marketType]
|
||||||
// 优先使用传入的 exchangeId(非 hyperliquid 时)
|
// Prefer passed-in exchangeId (when not hyperliquid)
|
||||||
const currentExchange = marketType === 'hyperliquid' ? 'hyperliquid' : (exchangeId || marketConfig.exchange)
|
const currentExchange = marketType === 'hyperliquid' ? 'hyperliquid' : (exchangeId || marketConfig.exchange)
|
||||||
|
|
||||||
// 获取可用币种列表
|
// Fetch available symbol list
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (marketConfig.hasDropdown) {
|
if (marketConfig.hasDropdown) {
|
||||||
fetch(`/api/symbols?exchange=${marketConfig.exchange}`)
|
fetch(`/api/symbols?exchange=${marketConfig.exchange}`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.symbols) {
|
if (data.symbols) {
|
||||||
// 按类别排序: crypto > stock > forex > commodity > index
|
// Sort by category: crypto > stock > forex > commodity > index
|
||||||
const categoryOrder: Record<string, number> = { crypto: 0, stock: 1, forex: 2, commodity: 3, index: 4 }
|
const categoryOrder: Record<string, number> = { crypto: 0, stock: 1, forex: 2, commodity: 3, index: 4 }
|
||||||
const sorted = [...data.symbols].sort((a: SymbolInfo, b: SymbolInfo) => {
|
const sorted = [...data.symbols].sort((a: SymbolInfo, b: SymbolInfo) => {
|
||||||
const orderA = categoryOrder[a.category] ?? 5
|
const orderA = categoryOrder[a.category] ?? 5
|
||||||
@@ -96,7 +97,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
|||||||
}
|
}
|
||||||
}, [marketType, marketConfig.exchange, marketConfig.hasDropdown])
|
}, [marketType, marketConfig.exchange, marketConfig.hasDropdown])
|
||||||
|
|
||||||
// 点击外部关闭下拉
|
// Close dropdown on outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
@@ -107,33 +108,33 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 切换市场类型时更新默认符号
|
// Update default symbol when switching market type
|
||||||
const handleMarketTypeChange = (type: MarketType) => {
|
const handleMarketTypeChange = (type: MarketType) => {
|
||||||
setMarketType(type)
|
setMarketType(type)
|
||||||
setChartSymbol(MARKET_CONFIG[type].defaultSymbol)
|
setChartSymbol(MARKET_CONFIG[type].defaultSymbol)
|
||||||
setShowDropdown(false)
|
setShowDropdown(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤后的币种列表
|
// Filtered symbol list
|
||||||
const filteredSymbols = availableSymbols.filter(s =>
|
const filteredSymbols = availableSymbols.filter(s =>
|
||||||
s.symbol.toLowerCase().includes(searchFilter.toLowerCase())
|
s.symbol.toLowerCase().includes(searchFilter.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
// 当从外部选择币种时,自动切换到K线图
|
// Auto-switch to kline chart when symbol selected externally
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedSymbol) {
|
if (selectedSymbol) {
|
||||||
console.log('[ChartTabs] 收到币种选择:', selectedSymbol, 'updateKey:', updateKey)
|
console.log('[ChartTabs] Symbol selected:', selectedSymbol, 'updateKey:', updateKey)
|
||||||
setChartSymbol(selectedSymbol)
|
setChartSymbol(selectedSymbol)
|
||||||
setActiveTab('kline')
|
setActiveTab('kline')
|
||||||
}
|
}
|
||||||
}, [selectedSymbol, updateKey])
|
}, [selectedSymbol, updateKey])
|
||||||
|
|
||||||
// 处理手动输入符号
|
// Handle manual symbol input
|
||||||
const handleSymbolSubmit = (e: React.FormEvent) => {
|
const handleSymbolSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (symbolInput.trim()) {
|
if (symbolInput.trim()) {
|
||||||
let symbol = symbolInput.trim().toUpperCase()
|
let symbol = symbolInput.trim().toUpperCase()
|
||||||
// 加密货币自动加 USDT 后缀
|
// Auto-append USDT suffix for crypto
|
||||||
if (marketType === 'crypto' && !symbol.endsWith('USDT')) {
|
if (marketType === 'crypto' && !symbol.endsWith('USDT')) {
|
||||||
symbol = symbol + 'USDT'
|
symbol = symbol + 'USDT'
|
||||||
}
|
}
|
||||||
@@ -198,7 +199,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="mr-1 opacity-70">{config.icon}</span>
|
<span className="mr-1 opacity-70">{config.icon}</span>
|
||||||
{language === 'zh' ? config.label.zh : config.label.en}
|
{ts(chartTabs[config.labelKey], language)}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import {
|
|||||||
} from 'lightweight-charts'
|
} from 'lightweight-charts'
|
||||||
import { useLanguage } from '../../contexts/LanguageContext'
|
import { useLanguage } from '../../contexts/LanguageContext'
|
||||||
import { httpClient } from '../../lib/httpClient'
|
import { httpClient } from '../../lib/httpClient'
|
||||||
|
import { t } from '../../i18n/translations'
|
||||||
|
|
||||||
// 订单接口定义
|
// Order marker interface
|
||||||
interface OrderMarker {
|
interface OrderMarker {
|
||||||
time: number // Unix timestamp (seconds)
|
time: number // Unix timestamp (seconds)
|
||||||
price: number
|
price: number
|
||||||
@@ -21,7 +22,7 @@ interface OrderMarker {
|
|||||||
symbol: string
|
symbol: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// K线数据接口
|
// Kline data interface
|
||||||
interface KlineData {
|
interface KlineData {
|
||||||
time: UTCTimestamp
|
time: UTCTimestamp
|
||||||
open: number
|
open: number
|
||||||
@@ -34,9 +35,9 @@ interface KlineData {
|
|||||||
interface ChartWithOrdersProps {
|
interface ChartWithOrdersProps {
|
||||||
symbol: string
|
symbol: string
|
||||||
interval?: string // 1m, 5m, 15m, 1h, 4h, 1d
|
interval?: string // 1m, 5m, 15m, 1h, 4h, 1d
|
||||||
traderID?: string // 用于获取该trader的订单
|
traderID?: string // Used to fetch orders for this trader
|
||||||
height?: number
|
height?: number
|
||||||
exchange?: string // 交易所类型:binance, bybit, okx, bitget, hyperliquid, aster, lighter
|
exchange?: string // Exchange type: binance, bybit, okx, bitget, hyperliquid, aster, lighter
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChartWithOrders({
|
export function ChartWithOrders({
|
||||||
@@ -44,7 +45,7 @@ export function ChartWithOrders({
|
|||||||
interval = '5m',
|
interval = '5m',
|
||||||
traderID,
|
traderID,
|
||||||
height = 500,
|
height = 500,
|
||||||
exchange = 'binance', // 默认使用 binance
|
exchange = 'binance', // Default to binance
|
||||||
}: ChartWithOrdersProps) {
|
}: ChartWithOrdersProps) {
|
||||||
const { language } = useLanguage()
|
const { language } = useLanguage()
|
||||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -56,16 +57,16 @@ export function ChartWithOrders({
|
|||||||
const [tooltipData, setTooltipData] = useState<any>(null)
|
const [tooltipData, setTooltipData] = useState<any>(null)
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// 解析时间:支持 Unix 时间戳(数字)或字符串格式
|
// Parse time: supports Unix timestamp (number) or string format
|
||||||
const parseCustomTime = (time: any): number => {
|
const parseCustomTime = (time: any): number => {
|
||||||
if (!time) {
|
if (!time) {
|
||||||
console.warn('[ChartWithOrders] Empty time value')
|
console.warn('[ChartWithOrders] Empty time value')
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果已经是数字(Unix 时间戳)
|
// If already a number (Unix timestamp)
|
||||||
if (typeof time === 'number') {
|
if (typeof time === 'number') {
|
||||||
// 判断是毫秒还是秒:如果大于 10^12 则认为是毫秒(2001年之后的毫秒时间戳)
|
// Determine ms vs seconds: if > 10^12, treat as milliseconds
|
||||||
if (time > 1000000000000) {
|
if (time > 1000000000000) {
|
||||||
const seconds = Math.floor(time / 1000)
|
const seconds = Math.floor(time / 1000)
|
||||||
console.log('[ChartWithOrders] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
|
console.log('[ChartWithOrders] ✅ Unix timestamp (ms→s):', time, '→', seconds, '(', new Date(time).toISOString(), ')')
|
||||||
@@ -78,7 +79,7 @@ export function ChartWithOrders({
|
|||||||
const timeStr = String(time)
|
const timeStr = String(time)
|
||||||
console.log('[ChartWithOrders] Parsing time string:', timeStr)
|
console.log('[ChartWithOrders] Parsing time string:', timeStr)
|
||||||
|
|
||||||
// 尝试标准ISO格式
|
// Try standard ISO format
|
||||||
const isoTime = new Date(timeStr).getTime()
|
const isoTime = new Date(timeStr).getTime()
|
||||||
if (!isNaN(isoTime) && isoTime > 0) {
|
if (!isNaN(isoTime) && isoTime > 0) {
|
||||||
const timestamp = Math.floor(isoTime / 1000)
|
const timestamp = Math.floor(isoTime / 1000)
|
||||||
@@ -86,7 +87,7 @@ export function ChartWithOrders({
|
|||||||
return timestamp
|
return timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析自定义格式 "MM-DD HH:mm UTC" (兼容旧数据)
|
// Parse custom format "MM-DD HH:mm UTC" (for legacy data)
|
||||||
const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
|
const match = timeStr.match(/(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+UTC/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
@@ -107,10 +108,10 @@ export function ChartWithOrders({
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从我们的服务获取K线数据
|
// Fetch kline data from our service
|
||||||
const fetchKlineData = async (symbol: string, interval: string): Promise<KlineData[]> => {
|
const fetchKlineData = async (symbol: string, interval: string): Promise<KlineData[]> => {
|
||||||
try {
|
try {
|
||||||
const limit = 2000 // 获取最近2000根K线 (更多历史数据)
|
const limit = 2000 // Fetch recent 2000 candles (more historical data)
|
||||||
const klineUrl = `/api/klines?symbol=${symbol}&interval=${interval}&limit=${limit}&exchange=${exchange}`
|
const klineUrl = `/api/klines?symbol=${symbol}&interval=${interval}&limit=${limit}&exchange=${exchange}`
|
||||||
|
|
||||||
const result = await httpClient.get(klineUrl)
|
const result = await httpClient.get(klineUrl)
|
||||||
@@ -121,10 +122,10 @@ export function ChartWithOrders({
|
|||||||
|
|
||||||
const data = result.data
|
const data = result.data
|
||||||
|
|
||||||
// 转换后端数据格式到 lightweight-charts 格式
|
// Convert backend data format to lightweight-charts format
|
||||||
// 后端返回的是 market.Kline 格式: {OpenTime, Open, High, Low, Close, Volume, ...}
|
// Backend returns market.Kline format: {OpenTime, Open, High, Low, Close, Volume, ...}
|
||||||
return data.map((candle: any) => ({
|
return data.map((candle: any) => ({
|
||||||
time: Math.floor(candle.openTime / 1000) as UTCTimestamp, // 毫秒转秒
|
time: Math.floor(candle.openTime / 1000) as UTCTimestamp, // ms to seconds
|
||||||
open: candle.open,
|
open: candle.open,
|
||||||
high: candle.high,
|
high: candle.high,
|
||||||
low: candle.low,
|
low: candle.low,
|
||||||
@@ -137,10 +138,10 @@ export function ChartWithOrders({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取订单数据
|
// Fetch order data
|
||||||
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
|
const fetchOrders = async (traderID: string, symbol: string): Promise<OrderMarker[]> => {
|
||||||
try {
|
try {
|
||||||
// 从后端 API 获取该 trader 的订单记录(只获取已成交的订单)
|
// Fetch filled orders for this trader from backend API
|
||||||
const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=50`)
|
const result = await httpClient.get(`/api/orders?trader_id=${traderID}&symbol=${symbol}&status=FILLED&limit=50`)
|
||||||
|
|
||||||
if (!result.success || !result.data) {
|
if (!result.success || !result.data) {
|
||||||
@@ -151,7 +152,7 @@ export function ChartWithOrders({
|
|||||||
const orders = result.data
|
const orders = result.data
|
||||||
const markers: OrderMarker[] = []
|
const markers: OrderMarker[] = []
|
||||||
|
|
||||||
// 转换订单数据为标记格式
|
// Convert order data to marker format
|
||||||
orders.forEach((order: any) => {
|
orders.forEach((order: any) => {
|
||||||
const createdAt = order.created_at || order.CreatedAt
|
const createdAt = order.created_at || order.CreatedAt
|
||||||
const filledAt = order.filled_at || order.FilledAt
|
const filledAt = order.filled_at || order.FilledAt
|
||||||
@@ -162,14 +163,14 @@ export function ChartWithOrders({
|
|||||||
const status = order.status || order.Status
|
const status = order.status || order.Status
|
||||||
const symbol = order.symbol || order.Symbol
|
const symbol = order.symbol || order.Symbol
|
||||||
|
|
||||||
// 使用成交时间(如果有)或创建时间
|
// Use fill time (if available) or creation time
|
||||||
const orderTime = filledAt || createdAt
|
const orderTime = filledAt || createdAt
|
||||||
if (!orderTime) return
|
if (!orderTime) return
|
||||||
|
|
||||||
const timeSeconds = parseCustomTime(orderTime)
|
const timeSeconds = parseCustomTime(orderTime)
|
||||||
if (timeSeconds === 0) return
|
if (timeSeconds === 0) return
|
||||||
|
|
||||||
// 使用平均成交价(如果有)或订单价格
|
// Use average fill price (if available) or order price
|
||||||
const orderPrice = avgPrice || price
|
const orderPrice = avgPrice || price
|
||||||
if (!orderPrice || orderPrice === 0) return
|
if (!orderPrice || orderPrice === 0) return
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@ export function ChartWithOrders({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化图表
|
// Initialize chart
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartContainerRef.current) {
|
if (!chartContainerRef.current) {
|
||||||
console.error('[ChartWithOrders] Container ref is null')
|
console.error('[ChartWithOrders] Container ref is null')
|
||||||
@@ -201,7 +202,7 @@ export function ChartWithOrders({
|
|||||||
console.log('[ChartWithOrders] Initializing chart for', symbol, interval)
|
console.log('[ChartWithOrders] Initializing chart for', symbol, interval)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建图表
|
// Create chart
|
||||||
const chart = createChart(chartContainerRef.current, {
|
const chart = createChart(chartContainerRef.current, {
|
||||||
width: chartContainerRef.current.clientWidth,
|
width: chartContainerRef.current.clientWidth,
|
||||||
height: height,
|
height: height,
|
||||||
@@ -240,7 +241,7 @@ export function ChartWithOrders({
|
|||||||
|
|
||||||
chartRef.current = chart
|
chartRef.current = chart
|
||||||
|
|
||||||
// 创建K线系列 (使用 v5 API)
|
// Create candlestick series (using v5 API)
|
||||||
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||||
upColor: '#0ECB81',
|
upColor: '#0ECB81',
|
||||||
downColor: '#F6465D',
|
downColor: '#F6465D',
|
||||||
@@ -252,7 +253,7 @@ export function ChartWithOrders({
|
|||||||
|
|
||||||
candlestickSeriesRef.current = candlestickSeries as any
|
candlestickSeriesRef.current = candlestickSeries as any
|
||||||
|
|
||||||
// 响应式调整
|
// Responsive resize
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (chartContainerRef.current && chartRef.current) {
|
if (chartContainerRef.current && chartRef.current) {
|
||||||
chartRef.current.applyOptions({
|
chartRef.current.applyOptions({
|
||||||
@@ -263,7 +264,7 @@ export function ChartWithOrders({
|
|||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
// 监听鼠标移动,显示 OHLC 信息
|
// Listen for crosshair movement to show OHLC info
|
||||||
chart.subscribeCrosshairMove((param) => {
|
chart.subscribeCrosshairMove((param) => {
|
||||||
if (!param.time || !param.point || !candlestickSeriesRef.current) {
|
if (!param.time || !param.point || !candlestickSeriesRef.current) {
|
||||||
setTooltipData(null)
|
setTooltipData(null)
|
||||||
@@ -298,7 +299,7 @@ export function ChartWithOrders({
|
|||||||
}
|
}
|
||||||
}, [height])
|
}, [height])
|
||||||
|
|
||||||
// 加载数据
|
// Load data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
if (!candlestickSeriesRef.current) {
|
if (!candlestickSeriesRef.current) {
|
||||||
@@ -311,22 +312,22 @@ export function ChartWithOrders({
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 获取K线数据
|
// 1. Fetch kline data
|
||||||
console.log('[ChartWithOrders] Fetching kline data...')
|
console.log('[ChartWithOrders] Fetching kline data...')
|
||||||
const klineData = await fetchKlineData(symbol, interval)
|
const klineData = await fetchKlineData(symbol, interval)
|
||||||
console.log('[ChartWithOrders] Kline data received:', klineData.length, 'candles')
|
console.log('[ChartWithOrders] Kline data received:', klineData.length, 'candles')
|
||||||
candlestickSeriesRef.current.setData(klineData)
|
candlestickSeriesRef.current.setData(klineData)
|
||||||
|
|
||||||
// 构建 K 线时间集合,用于快速查找
|
// Build kline time set for quick lookup
|
||||||
const klineTimeSet = new Set(klineData.map(k => k.time as number))
|
const klineTimeSet = new Set(klineData.map(k => k.time as number))
|
||||||
const klineMinTime = klineData.length > 0 ? klineData[0].time : 0
|
const klineMinTime = klineData.length > 0 ? klineData[0].time : 0
|
||||||
const klineMaxTime = klineData.length > 0 ? klineData[klineData.length - 1].time : 0
|
const klineMaxTime = klineData.length > 0 ? klineData[klineData.length - 1].time : 0
|
||||||
console.log('[ChartWithOrders] Kline time range:', klineMinTime, '-', klineMaxTime, 'candles:', klineData.length)
|
console.log('[ChartWithOrders] Kline time range:', klineMinTime, '-', klineMaxTime, 'candles:', klineData.length)
|
||||||
|
|
||||||
// 计算时间周期的秒数
|
// Calculate interval in seconds
|
||||||
const getIntervalSeconds = (interval: string): number => {
|
const getIntervalSeconds = (interval: string): number => {
|
||||||
const match = interval.match(/(\d+)([smhd])/)
|
const match = interval.match(/(\d+)([smhd])/)
|
||||||
if (!match) return 60 // 默认1分钟
|
if (!match) return 60 // Default 1 minute
|
||||||
const [, num, unit] = match
|
const [, num, unit] = match
|
||||||
const n = parseInt(num)
|
const n = parseInt(num)
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
@@ -340,7 +341,7 @@ export function ChartWithOrders({
|
|||||||
const intervalSeconds = getIntervalSeconds(interval)
|
const intervalSeconds = getIntervalSeconds(interval)
|
||||||
console.log('[ChartWithOrders] Interval:', interval, '=', intervalSeconds, 'seconds')
|
console.log('[ChartWithOrders] Interval:', interval, '=', intervalSeconds, 'seconds')
|
||||||
|
|
||||||
// 2. 获取订单数据并添加标记
|
// 2. Fetch order data and add markers
|
||||||
if (traderID) {
|
if (traderID) {
|
||||||
console.log('[ChartWithOrders] Fetching orders for trader:', traderID, 'symbol:', symbol)
|
console.log('[ChartWithOrders] Fetching orders for trader:', traderID, 'symbol:', symbol)
|
||||||
const orders = await fetchOrders(traderID, symbol)
|
const orders = await fetchOrders(traderID, symbol)
|
||||||
@@ -350,7 +351,7 @@ export function ChartWithOrders({
|
|||||||
console.log('[ChartWithOrders] No orders to display')
|
console.log('[ChartWithOrders] No orders to display')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换订单为图表标记,并对齐到 K 线时间
|
// Convert orders to chart markers, aligned to kline time
|
||||||
const markers: Array<{
|
const markers: Array<{
|
||||||
time: Time
|
time: Time
|
||||||
position: 'belowBar'
|
position: 'belowBar'
|
||||||
@@ -362,10 +363,10 @@ export function ChartWithOrders({
|
|||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
orders.forEach((order) => {
|
orders.forEach((order) => {
|
||||||
// 将订单时间对齐到 K 线周期(向下取整)
|
// Align order time to kline interval (floor)
|
||||||
const alignedTime = Math.floor(order.time / intervalSeconds) * intervalSeconds
|
const alignedTime = Math.floor(order.time / intervalSeconds) * intervalSeconds
|
||||||
|
|
||||||
// 检查对齐后的时间是否在 K 线数据中存在
|
// Check if aligned time exists in kline data
|
||||||
if (!klineTimeSet.has(alignedTime)) {
|
if (!klineTimeSet.has(alignedTime)) {
|
||||||
console.warn('[ChartWithOrders] ⚠️ Skipping order - no matching kline:',
|
console.warn('[ChartWithOrders] ⚠️ Skipping order - no matching kline:',
|
||||||
order.time, '→', alignedTime, '(', new Date(order.time * 1000).toISOString(), ')')
|
order.time, '→', alignedTime, '(', new Date(order.time * 1000).toISOString(), ')')
|
||||||
@@ -389,12 +390,12 @@ export function ChartWithOrders({
|
|||||||
console.log('[ChartWithOrders] Setting', markers.length, 'markers on chart')
|
console.log('[ChartWithOrders] Setting', markers.length, 'markers on chart')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用 v5 API: createSeriesMarkers
|
// Using v5 API: createSeriesMarkers
|
||||||
if (seriesMarkersRef.current) {
|
if (seriesMarkersRef.current) {
|
||||||
// 如果已经存在,更新标记
|
// If already exists, update markers
|
||||||
seriesMarkersRef.current.setMarkers(markers)
|
seriesMarkersRef.current.setMarkers(markers)
|
||||||
} else {
|
} else {
|
||||||
// 首次创建标记
|
// First time creating markers
|
||||||
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markers)
|
seriesMarkersRef.current = createSeriesMarkers(candlestickSeriesRef.current, markers)
|
||||||
}
|
}
|
||||||
console.log('[ChartWithOrders] ✅ Markers set successfully!')
|
console.log('[ChartWithOrders] ✅ Markers set successfully!')
|
||||||
@@ -403,23 +404,23 @@ export function ChartWithOrders({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动适配视图
|
// Auto-fit view
|
||||||
chartRef.current?.timeScale().fitContent()
|
chartRef.current?.timeScale().fitContent()
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading chart data:', err)
|
console.error('Error loading chart data:', err)
|
||||||
setError(language === 'zh' ? '加载图表数据失败' : 'Failed to load chart data')
|
setError(t('chartWithOrders.failedToLoad', language))
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData()
|
loadData()
|
||||||
|
|
||||||
// 自动刷新 - 每30秒更新一次K线数据
|
// Auto-refresh - update kline data every 30 seconds
|
||||||
const refreshInterval = setInterval(() => {
|
const refreshInterval = setInterval(() => {
|
||||||
loadData()
|
loadData()
|
||||||
}, 30000) // 30秒
|
}, 30000) // 30 seconds
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(refreshInterval)
|
clearInterval(refreshInterval)
|
||||||
@@ -428,7 +429,7 @@ export function ChartWithOrders({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" style={{ background: '#0B0E11', borderRadius: '8px', overflow: 'hidden' }}>
|
<div className="relative" style={{ background: '#0B0E11', borderRadius: '8px', overflow: 'hidden' }}>
|
||||||
{/* 标题栏 */}
|
{/* Title bar */}
|
||||||
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid #2B3139' }}>
|
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid #2B3139' }}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xl">📈</span>
|
<span className="text-xl">📈</span>
|
||||||
@@ -438,12 +439,12 @@ export function ChartWithOrders({
|
|||||||
</div>
|
</div>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="text-sm" style={{ color: '#848E9C' }}>
|
<div className="text-sm" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '加载中...' : 'Loading...'}
|
{t('chartWithOrders.loading', language)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 图表容器 */}
|
{/* Chart container */}
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<div ref={chartContainerRef} />
|
<div ref={chartContainerRef} />
|
||||||
|
|
||||||
@@ -498,7 +499,7 @@ export function ChartWithOrders({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 错误提示 */}
|
{/* Error display */}
|
||||||
{error && (
|
{error && (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 flex items-center justify-center"
|
className="absolute inset-0 flex items-center justify-center"
|
||||||
@@ -511,15 +512,15 @@ export function ChartWithOrders({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 图例说明 */}
|
{/* Legend */}
|
||||||
<div className="flex items-center gap-4 p-4 text-xs" style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}>
|
<div className="flex items-center gap-4 p-4 text-xs" style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-bold" style={{ color: '#0ECB81' }}>B</span>
|
<span className="font-bold" style={{ color: '#0ECB81' }}>B</span>
|
||||||
<span>{language === 'zh' ? 'BUY (买入)' : 'BUY'}</span>
|
<span>{t('chartWithOrders.buy', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-bold" style={{ color: '#F6465D' }}>S</span>
|
<span className="font-bold" style={{ color: '#F6465D' }}>S</span>
|
||||||
<span>{language === 'zh' ? 'SELL (卖出)' : 'SELL'}</span>
|
<span>{t('chartWithOrders.sell', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ import { BarChart3, TrendingUp, TrendingDown, Zap } from 'lucide-react'
|
|||||||
|
|
||||||
// Time period options: 1D, 3D, 7D, 30D, All
|
// Time period options: 1D, 3D, 7D, 30D, All
|
||||||
const TIME_PERIODS = [
|
const TIME_PERIODS = [
|
||||||
{ key: '1d', hours: 24, label: { en: '1D', zh: '1天' } },
|
{ key: '1d', hours: 24 },
|
||||||
{ key: '3d', hours: 72, label: { en: '3D', zh: '3天' } },
|
{ key: '3d', hours: 72 },
|
||||||
{ key: '7d', hours: 168, label: { en: '7D', zh: '7天' } },
|
{ key: '7d', hours: 168 },
|
||||||
{ key: '30d', hours: 720, label: { en: '30D', zh: '30天' } },
|
{ key: '30d', hours: 720 },
|
||||||
{ key: 'all', hours: 0, label: { en: 'All', zh: '全部' } },
|
{ key: 'all', hours: 0 },
|
||||||
]
|
] as const
|
||||||
|
|
||||||
interface ComparisonChartProps {
|
interface ComparisonChartProps {
|
||||||
traders: CompetitionTraderData[]
|
traders: CompetitionTraderData[]
|
||||||
@@ -352,7 +352,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
|||||||
border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
|
border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language === 'zh' ? period.label.zh : period.label.en}
|
{t(`comparisonChart.${period.key}`, language)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ export function ConfirmDialogProvider({
|
|||||||
const [state, setState] = useState<ConfirmState>({
|
const [state, setState] = useState<ConfirmState>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
message: '',
|
message: '',
|
||||||
okText: '确认',
|
okText: 'Confirm',
|
||||||
cancelText: '取消',
|
cancelText: 'Cancel',
|
||||||
})
|
})
|
||||||
|
|
||||||
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
|
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
|
||||||
@@ -68,14 +68,14 @@ export function ConfirmDialogProvider({
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: options.title,
|
title: options.title,
|
||||||
message: options.message,
|
message: options.message,
|
||||||
okText: options.okText || '确认',
|
okText: options.okText || 'Confirm',
|
||||||
cancelText: options.cancelText || '取消',
|
cancelText: options.cancelText || 'Cancel',
|
||||||
resolve,
|
resolve,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 注册全局 confirm 函数
|
// Register global confirm function
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGlobalConfirm(confirm)
|
setGlobalConfirm(confirm)
|
||||||
}, [confirm])
|
}, [confirm])
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'
|
|||||||
import { HelpCircle } from 'lucide-react'
|
import { HelpCircle } from 'lucide-react'
|
||||||
import katex from 'katex'
|
import katex from 'katex'
|
||||||
import 'katex/dist/katex.min.css'
|
import 'katex/dist/katex.min.css'
|
||||||
|
import { t } from '../../i18n/translations'
|
||||||
|
|
||||||
export interface MetricDefinition {
|
export interface MetricDefinition {
|
||||||
key: string
|
key: string
|
||||||
@@ -241,6 +242,7 @@ export function MetricTooltip({
|
|||||||
|
|
||||||
const name = language === 'zh' ? metric.nameZh : metric.nameEn
|
const name = language === 'zh' ? metric.nameZh : metric.nameEn
|
||||||
const description = language === 'zh' ? metric.descriptionZh : metric.descriptionEn
|
const description = language === 'zh' ? metric.descriptionZh : metric.descriptionEn
|
||||||
|
const formulaLabel = t('metricTooltip.formula', language as 'en' | 'zh' | 'id')
|
||||||
|
|
||||||
const tooltipContent = (
|
const tooltipContent = (
|
||||||
<div
|
<div
|
||||||
@@ -292,7 +294,7 @@ export function MetricTooltip({
|
|||||||
marginBottom: '12px'
|
marginBottom: '12px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: '12px', color: '#848E9C', marginBottom: '8px' }}>
|
<div style={{ fontSize: '12px', color: '#848E9C', marginBottom: '8px' }}>
|
||||||
{language === 'zh' ? '计算公式' : 'Formula'}
|
{formulaLabel}
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap, Shuffle } from 'lucide-react'
|
import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap, Shuffle } from 'lucide-react'
|
||||||
import type { CoinSourceConfig } from '../../types'
|
import type { CoinSourceConfig } from '../../types'
|
||||||
|
import { coinSource, ts } from '../../i18n/strategy-translations'
|
||||||
|
|
||||||
interface CoinSourceEditorProps {
|
interface CoinSourceEditorProps {
|
||||||
config: CoinSourceConfig
|
config: CoinSourceConfig
|
||||||
@@ -18,52 +19,6 @@ export function CoinSourceEditor({
|
|||||||
const [newCoin, setNewCoin] = useState('')
|
const [newCoin, setNewCoin] = useState('')
|
||||||
const [newExcludedCoin, setNewExcludedCoin] = useState('')
|
const [newExcludedCoin, setNewExcludedCoin] = useState('')
|
||||||
|
|
||||||
const t = (key: string) => {
|
|
||||||
const translations: Record<string, Record<string, string>> = {
|
|
||||||
sourceType: { zh: '数据来源类型', en: 'Source Type' },
|
|
||||||
static: { zh: '静态列表', en: 'Static List' },
|
|
||||||
ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider' },
|
|
||||||
oi_top: { zh: 'OI 持仓增加', en: 'OI Increase' },
|
|
||||||
oi_low: { zh: 'OI 持仓减少', en: 'OI Decrease' },
|
|
||||||
mixed: { zh: '混合模式', en: 'Mixed Mode' },
|
|
||||||
staticCoins: { zh: '自定义币种', en: 'Custom Coins' },
|
|
||||||
addCoin: { zh: '添加币种', en: 'Add Coin' },
|
|
||||||
useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
|
|
||||||
ai500Limit: { zh: '数量上限', en: 'Limit' },
|
|
||||||
useOITop: { zh: '启用 OI 持仓增加榜', en: 'Enable OI Increase' },
|
|
||||||
oiTopLimit: { zh: '数量上限', en: 'Limit' },
|
|
||||||
useOILow: { zh: '启用 OI 持仓减少榜', en: 'Enable OI Decrease' },
|
|
||||||
oiLowLimit: { zh: '数量上限', en: 'Limit' },
|
|
||||||
staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' },
|
|
||||||
ai500Desc: {
|
|
||||||
zh: '使用 AI500 智能筛选的热门币种',
|
|
||||||
en: 'Use AI500 smart-filtered popular coins',
|
|
||||||
},
|
|
||||||
oiTopDesc: {
|
|
||||||
zh: '持仓增加榜,适合做多',
|
|
||||||
en: 'OI increase ranking, for long',
|
|
||||||
},
|
|
||||||
oi_lowDesc: {
|
|
||||||
zh: '持仓减少榜,适合做空',
|
|
||||||
en: 'OI decrease ranking, for short',
|
|
||||||
},
|
|
||||||
mixedDesc: {
|
|
||||||
zh: '组合多种数据源',
|
|
||||||
en: 'Combine multiple sources',
|
|
||||||
},
|
|
||||||
mixedConfig: { zh: '组合数据源配置', en: 'Combined Sources Configuration' },
|
|
||||||
mixedSummary: { zh: '已选组合', en: 'Selected Sources' },
|
|
||||||
maxCoins: { zh: '最多', en: 'Up to' },
|
|
||||||
coins: { zh: '个币种', en: 'coins' },
|
|
||||||
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
|
||||||
excludedCoins: { zh: '排除币种', en: 'Excluded Coins' },
|
|
||||||
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded' },
|
|
||||||
addExcludedCoin: { zh: '添加排除', en: 'Add Excluded' },
|
|
||||||
nofxosNote: { zh: '使用 NofxOS API Key(在指标配置中设置)', en: 'Uses NofxOS API Key (set in Indicators config)' },
|
|
||||||
}
|
|
||||||
return translations[key]?.[language] || key
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceTypes = [
|
const sourceTypes = [
|
||||||
{ value: 'static', icon: List, color: '#848E9C' },
|
{ value: 'static', icon: List, color: '#848E9C' },
|
||||||
{ value: 'ai500', icon: Database, color: '#F0B90B' },
|
{ value: 'ai500', icon: Database, color: '#F0B90B' },
|
||||||
@@ -82,15 +37,15 @@ export function CoinSourceEditor({
|
|||||||
totalLimit += config.ai500_limit || 10
|
totalLimit += config.ai500_limit || 10
|
||||||
}
|
}
|
||||||
if (config.use_oi_top) {
|
if (config.use_oi_top) {
|
||||||
sources.push(`${language === 'zh' ? 'OI增' : 'OI↑'}(${config.oi_top_limit || 10})`)
|
sources.push(`${ts(coinSource.oiIncreaseShort, language)}(${config.oi_top_limit || 10})`)
|
||||||
totalLimit += config.oi_top_limit || 10
|
totalLimit += config.oi_top_limit || 10
|
||||||
}
|
}
|
||||||
if (config.use_oi_low) {
|
if (config.use_oi_low) {
|
||||||
sources.push(`${language === 'zh' ? 'OI减' : 'OI↓'}(${config.oi_low_limit || 10})`)
|
sources.push(`${ts(coinSource.oiDecreaseShort, language)}(${config.oi_low_limit || 10})`)
|
||||||
totalLimit += config.oi_low_limit || 10
|
totalLimit += config.oi_low_limit || 10
|
||||||
}
|
}
|
||||||
if ((config.static_coins || []).length > 0) {
|
if ((config.static_coins || []).length > 0) {
|
||||||
sources.push(`${language === 'zh' ? '自定义' : 'Custom'}(${config.static_coins?.length || 0})`)
|
sources.push(`${ts(coinSource.custom, language)}(${config.static_coins?.length || 0})`)
|
||||||
totalLimit += config.static_coins?.length || 0
|
totalLimit += config.static_coins?.length || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +146,7 @@ export function CoinSourceEditor({
|
|||||||
{/* Source Type Selector */}
|
{/* Source Type Selector */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||||
{t('sourceType')}
|
{ts(coinSource.sourceType, language)}
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-5 gap-2">
|
<div className="grid grid-cols-5 gap-2">
|
||||||
{sourceTypes.map(({ value, icon: Icon, color }) => (
|
{sourceTypes.map(({ value, icon: Icon, color }) => (
|
||||||
@@ -209,10 +164,10 @@ export function CoinSourceEditor({
|
|||||||
>
|
>
|
||||||
<Icon className="w-6 h-6 mx-auto mb-2" style={{ color }} />
|
<Icon className="w-6 h-6 mx-auto mb-2" style={{ color }} />
|
||||||
<div className="text-sm font-medium text-nofx-text">
|
<div className="text-sm font-medium text-nofx-text">
|
||||||
{t(value)}
|
{ts(coinSource[value as keyof typeof coinSource], language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs mt-1 text-nofx-text-muted">
|
<div className="text-xs mt-1 text-nofx-text-muted">
|
||||||
{t(`${value}Desc`)}
|
{ts(coinSource[`${value}Desc` as keyof typeof coinSource], language)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -223,7 +178,7 @@ export function CoinSourceEditor({
|
|||||||
{config.source_type === 'static' && (
|
{config.source_type === 'static' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||||
{t('staticCoins')}
|
{ts(coinSource.staticCoins, language)}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
{(config.static_coins || []).map((coin) => (
|
{(config.static_coins || []).map((coin) => (
|
||||||
@@ -258,7 +213,7 @@ export function CoinSourceEditor({
|
|||||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors bg-nofx-gold text-black hover:bg-yellow-500"
|
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors bg-nofx-gold text-black hover:bg-yellow-500"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
{t('addCoin')}
|
{ts(coinSource.addCoin, language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -270,11 +225,11 @@ export function CoinSourceEditor({
|
|||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Ban className="w-4 h-4 text-nofx-danger" />
|
<Ban className="w-4 h-4 text-nofx-danger" />
|
||||||
<label className="text-sm font-medium text-nofx-text">
|
<label className="text-sm font-medium text-nofx-text">
|
||||||
{t('excludedCoins')}
|
{ts(coinSource.excludedCoins, language)}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs mb-3 text-nofx-text-muted">
|
<p className="text-xs mb-3 text-nofx-text-muted">
|
||||||
{t('excludedCoinsDesc')}
|
{ts(coinSource.excludedCoinsDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
{(config.excluded_coins || []).map((coin) => (
|
{(config.excluded_coins || []).map((coin) => (
|
||||||
@@ -295,7 +250,7 @@ export function CoinSourceEditor({
|
|||||||
))}
|
))}
|
||||||
{(config.excluded_coins || []).length === 0 && (
|
{(config.excluded_coins || []).length === 0 && (
|
||||||
<span className="text-xs italic text-nofx-text-muted">
|
<span className="text-xs italic text-nofx-text-muted">
|
||||||
{language === 'zh' ? '无' : 'None'}
|
{ts(coinSource.excludedNone, language)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -314,7 +269,7 @@ export function CoinSourceEditor({
|
|||||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm bg-nofx-danger text-white hover:bg-red-600"
|
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm bg-nofx-danger text-white hover:bg-red-600"
|
||||||
>
|
>
|
||||||
<Ban className="w-4 h-4" />
|
<Ban className="w-4 h-4" />
|
||||||
{t('addExcludedCoin')}
|
{ts(coinSource.addExcludedCoin, language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -329,7 +284,7 @@ export function CoinSourceEditor({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Zap className="w-4 h-4 text-nofx-gold" />
|
<Zap className="w-4 h-4 text-nofx-gold" />
|
||||||
<span className="text-sm font-medium text-nofx-text">
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
AI500 {t('dataSourceConfig')}
|
AI500 {ts(coinSource.dataSourceConfig, language)}
|
||||||
</span>
|
</span>
|
||||||
<NofxOSBadge />
|
<NofxOSBadge />
|
||||||
</div>
|
</div>
|
||||||
@@ -346,13 +301,13 @@ export function CoinSourceEditor({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="w-5 h-5 rounded accent-nofx-gold"
|
className="w-5 h-5 rounded accent-nofx-gold"
|
||||||
/>
|
/>
|
||||||
<span className="text-nofx-text">{t('useAI500')}</span>
|
<span className="text-nofx-text">{ts(coinSource.useAI500, language)}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{config.use_ai500 && (
|
{config.use_ai500 && (
|
||||||
<div className="flex items-center gap-3 pl-8">
|
<div className="flex items-center gap-3 pl-8">
|
||||||
<span className="text-sm text-nofx-text-muted">
|
<span className="text-sm text-nofx-text-muted">
|
||||||
{t('ai500Limit')}:
|
{ts(coinSource.ai500Limit, language)}:
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={config.ai500_limit || 10}
|
value={config.ai500_limit || 10}
|
||||||
@@ -371,7 +326,7 @@ export function CoinSourceEditor({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||||
{t('nofxosNote')}
|
{ts(coinSource.nofxosNote, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,7 +341,7 @@ export function CoinSourceEditor({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||||
<span className="text-sm font-medium text-nofx-text">
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
OI {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig')}
|
{ts(coinSource.oiIncreaseTitle, language)} {ts(coinSource.dataSourceConfig, language)}
|
||||||
</span>
|
</span>
|
||||||
<NofxOSBadge />
|
<NofxOSBadge />
|
||||||
</div>
|
</div>
|
||||||
@@ -403,13 +358,13 @@ export function CoinSourceEditor({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="w-5 h-5 rounded accent-nofx-success"
|
className="w-5 h-5 rounded accent-nofx-success"
|
||||||
/>
|
/>
|
||||||
<span className="text-nofx-text">{t('useOITop')}</span>
|
<span className="text-nofx-text">{ts(coinSource.useOITop, language)}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{config.use_oi_top && (
|
{config.use_oi_top && (
|
||||||
<div className="flex items-center gap-3 pl-8">
|
<div className="flex items-center gap-3 pl-8">
|
||||||
<span className="text-sm text-nofx-text-muted">
|
<span className="text-sm text-nofx-text-muted">
|
||||||
{t('oiTopLimit')}:
|
{ts(coinSource.oiTopLimit, language)}:
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={config.oi_top_limit || 10}
|
value={config.oi_top_limit || 10}
|
||||||
@@ -428,7 +383,7 @@ export function CoinSourceEditor({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||||
{t('nofxosNote')}
|
{ts(coinSource.nofxosNote, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -443,7 +398,7 @@ export function CoinSourceEditor({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
||||||
<span className="text-sm font-medium text-nofx-text">
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig')}
|
{ts(coinSource.oiDecreaseTitle, language)} {ts(coinSource.dataSourceConfig, language)}
|
||||||
</span>
|
</span>
|
||||||
<NofxOSBadge />
|
<NofxOSBadge />
|
||||||
</div>
|
</div>
|
||||||
@@ -460,13 +415,13 @@ export function CoinSourceEditor({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="w-5 h-5 rounded accent-red-500"
|
className="w-5 h-5 rounded accent-red-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-nofx-text">{t('useOILow')}</span>
|
<span className="text-nofx-text">{ts(coinSource.useOILow, language)}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{config.use_oi_low && (
|
{config.use_oi_low && (
|
||||||
<div className="flex items-center gap-3 pl-8">
|
<div className="flex items-center gap-3 pl-8">
|
||||||
<span className="text-sm text-nofx-text-muted">
|
<span className="text-sm text-nofx-text-muted">
|
||||||
{t('oiLowLimit')}:
|
{ts(coinSource.oiLowLimit, language)}:
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={config.oi_low_limit || 10}
|
value={config.oi_low_limit || 10}
|
||||||
@@ -485,7 +440,7 @@ export function CoinSourceEditor({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||||
{t('nofxosNote')}
|
{ts(coinSource.nofxosNote, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -497,7 +452,7 @@ export function CoinSourceEditor({
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Shuffle className="w-4 h-4 text-blue-400" />
|
<Shuffle className="w-4 h-4 text-blue-400" />
|
||||||
<span className="text-sm font-medium text-nofx-text">
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
{t('mixedConfig')}
|
{ts(coinSource.mixedConfig, language)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -566,11 +521,11 @@ export function CoinSourceEditor({
|
|||||||
/>
|
/>
|
||||||
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||||
<span className="text-sm font-medium text-nofx-text">
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
{language === 'zh' ? 'OI 增加' : 'OI Increase'}
|
{ts(coinSource.oiIncreaseLabel, language)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
||||||
{language === 'zh' ? '适合做多' : 'For long'}
|
{ts(coinSource.forLong, language)}
|
||||||
</p>
|
</p>
|
||||||
{config.use_oi_top && (
|
{config.use_oi_top && (
|
||||||
<div className="flex items-center gap-2 mt-2 pl-6">
|
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||||
@@ -613,11 +568,11 @@ export function CoinSourceEditor({
|
|||||||
/>
|
/>
|
||||||
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
||||||
<span className="text-sm font-medium text-nofx-text">
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
{language === 'zh' ? 'OI 减少' : 'OI Decrease'}
|
{ts(coinSource.oiDecreaseLabel, language)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
||||||
{language === 'zh' ? '适合做空' : 'For short'}
|
{ts(coinSource.forShort, language)}
|
||||||
</p>
|
</p>
|
||||||
{config.use_oi_low && (
|
{config.use_oi_low && (
|
||||||
<div className="flex items-center gap-2 mt-2 pl-6">
|
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||||
@@ -651,7 +606,7 @@ export function CoinSourceEditor({
|
|||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<List className="w-4 h-4 text-gray-400" />
|
<List className="w-4 h-4 text-gray-400" />
|
||||||
<span className="text-sm font-medium text-nofx-text">
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
{language === 'zh' ? '自定义' : 'Custom'}
|
{ts(coinSource.custom, language)}
|
||||||
</span>
|
</span>
|
||||||
{(config.static_coins || []).length > 0 && (
|
{(config.static_coins || []).length > 0 && (
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">
|
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">
|
||||||
@@ -720,13 +675,13 @@ export function CoinSourceEditor({
|
|||||||
return (
|
return (
|
||||||
<div className="p-2 rounded bg-nofx-bg border border-nofx-border">
|
<div className="p-2 rounded bg-nofx-bg border border-nofx-border">
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-nofx-text-muted">{t('mixedSummary')}:</span>
|
<span className="text-nofx-text-muted">{ts(coinSource.mixedSummary, language)}:</span>
|
||||||
<span className="text-nofx-text font-medium">
|
<span className="text-nofx-text font-medium">
|
||||||
{sources.join(' + ')}
|
{sources.join(' + ')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-nofx-text-muted mt-1">
|
<div className="text-xs text-nofx-text-muted mt-1">
|
||||||
{t('maxCoins')} {totalLimit} {t('coins')}
|
{ts(coinSource.maxCoins, language)} {totalLimit} {ts(coinSource.coins, language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Grid, DollarSign, TrendingUp, Shield, Compass } from 'lucide-react'
|
import { Grid, DollarSign, TrendingUp, Shield, Compass } from 'lucide-react'
|
||||||
import type { GridStrategyConfig } from '../../types'
|
import type { GridStrategyConfig } from '../../types'
|
||||||
|
import { gridConfig, ts } from '../../i18n/strategy-translations'
|
||||||
|
|
||||||
interface GridConfigEditorProps {
|
interface GridConfigEditorProps {
|
||||||
config: GridStrategyConfig
|
config: GridStrategyConfig
|
||||||
@@ -8,7 +9,7 @@ interface GridConfigEditorProps {
|
|||||||
language: string
|
language: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default grid config
|
// Default grid configuration
|
||||||
export const defaultGridConfig: GridStrategyConfig = {
|
export const defaultGridConfig: GridStrategyConfig = {
|
||||||
symbol: 'BTCUSDT',
|
symbol: 'BTCUSDT',
|
||||||
grid_count: 10,
|
grid_count: 10,
|
||||||
@@ -33,71 +34,6 @@ export function GridConfigEditor({
|
|||||||
disabled,
|
disabled,
|
||||||
language,
|
language,
|
||||||
}: GridConfigEditorProps) {
|
}: GridConfigEditorProps) {
|
||||||
const t = (key: string) => {
|
|
||||||
const translations: Record<string, Record<string, string>> = {
|
|
||||||
// Section titles
|
|
||||||
tradingPair: { zh: '交易设置', en: 'Trading Setup' },
|
|
||||||
gridParameters: { zh: '网格参数', en: 'Grid Parameters' },
|
|
||||||
priceBounds: { zh: '价格边界', en: 'Price Bounds' },
|
|
||||||
riskControl: { zh: '风险控制', en: 'Risk Control' },
|
|
||||||
|
|
||||||
// Trading pair
|
|
||||||
symbol: { zh: '交易对', en: 'Trading Pair' },
|
|
||||||
symbolDesc: { zh: '选择要进行网格交易的交易对', en: 'Select trading pair for grid trading' },
|
|
||||||
|
|
||||||
// Investment
|
|
||||||
totalInvestment: { zh: '投资金额 (USDT)', en: 'Investment (USDT)' },
|
|
||||||
totalInvestmentDesc: { zh: '网格策略的总投资金额', en: 'Total investment for grid strategy' },
|
|
||||||
leverage: { zh: '杠杆倍数', en: 'Leverage' },
|
|
||||||
leverageDesc: { zh: '交易使用的杠杆倍数 (1-5)', en: 'Leverage for trading (1-5)' },
|
|
||||||
|
|
||||||
// Grid parameters
|
|
||||||
gridCount: { zh: '网格数量', en: 'Grid Count' },
|
|
||||||
gridCountDesc: { zh: '网格层级数量 (5-50)', en: 'Number of grid levels (5-50)' },
|
|
||||||
distribution: { zh: '资金分配方式', en: 'Distribution' },
|
|
||||||
distributionDesc: { zh: '网格层级的资金分配方式', en: 'Fund allocation across grid levels' },
|
|
||||||
uniform: { zh: '均匀分配', en: 'Uniform' },
|
|
||||||
gaussian: { zh: '高斯分配 (推荐)', en: 'Gaussian (Recommended)' },
|
|
||||||
pyramid: { zh: '金字塔分配', en: 'Pyramid' },
|
|
||||||
|
|
||||||
// Price bounds
|
|
||||||
useAtrBounds: { zh: '自动计算边界 (ATR)', en: 'Auto-calculate Bounds (ATR)' },
|
|
||||||
useAtrBoundsDesc: { zh: '基于 ATR 自动计算网格上下边界', en: 'Auto-calculate bounds based on ATR' },
|
|
||||||
atrMultiplier: { zh: 'ATR 倍数', en: 'ATR Multiplier' },
|
|
||||||
atrMultiplierDesc: { zh: '边界距离当前价格的 ATR 倍数', en: 'ATR multiplier for bounds distance' },
|
|
||||||
upperPrice: { zh: '上边界价格', en: 'Upper Price' },
|
|
||||||
upperPriceDesc: { zh: '网格上边界价格 (0=自动计算)', en: 'Grid upper bound (0=auto)' },
|
|
||||||
lowerPrice: { zh: '下边界价格', en: 'Lower Price' },
|
|
||||||
lowerPriceDesc: { zh: '网格下边界价格 (0=自动计算)', en: 'Grid lower bound (0=auto)' },
|
|
||||||
|
|
||||||
// Risk control
|
|
||||||
maxDrawdown: { zh: '最大回撤 (%)', en: 'Max Drawdown (%)' },
|
|
||||||
maxDrawdownDesc: { zh: '触发紧急退出的最大回撤百分比', en: 'Max drawdown before emergency exit' },
|
|
||||||
stopLoss: { zh: '止损 (%)', en: 'Stop Loss (%)' },
|
|
||||||
stopLossDesc: { zh: '单仓位止损百分比', en: 'Stop loss per position' },
|
|
||||||
dailyLossLimit: { zh: '日损失限制 (%)', en: 'Daily Loss Limit (%)' },
|
|
||||||
dailyLossLimitDesc: { zh: '每日最大亏损百分比', en: 'Maximum daily loss percentage' },
|
|
||||||
useMakerOnly: { zh: '仅使用 Maker 订单', en: 'Maker Only Orders' },
|
|
||||||
useMakerOnlyDesc: { zh: '使用限价单以降低手续费', en: 'Use limit orders for lower fees' },
|
|
||||||
|
|
||||||
// Direction adjustment
|
|
||||||
directionAdjust: { zh: '方向自动调整', en: 'Direction Auto-Adjust' },
|
|
||||||
enableDirectionAdjust: { zh: '启用方向调整', en: 'Enable Direction Adjust' },
|
|
||||||
enableDirectionAdjustDesc: { zh: '根据箱体突破自动调整网格方向', en: 'Auto-adjust grid direction based on box breakouts' },
|
|
||||||
directionBiasRatio: { zh: '偏向强度', en: 'Bias Strength' },
|
|
||||||
directionBiasRatioDesc: { zh: '偏多/偏空模式的强度', en: 'Strength for long_bias/short_bias modes' },
|
|
||||||
directionBiasExplain: { zh: '偏多模式:X%买 + (100-X)%卖 | 偏空模式:(100-X)%买 + X%卖', en: 'Long bias: X% buy + (100-X)% sell | Short bias: (100-X)% buy + X% sell' },
|
|
||||||
directionExplain: { zh: '短期箱体突破 → 偏向,中期箱体突破 → 全仓,价格回归 → 逐步恢复中性', en: 'Short box breakout → bias, Mid box breakout → full, Price return → gradually recover to neutral' },
|
|
||||||
directionModes: { zh: '方向模式说明', en: 'Direction Modes' },
|
|
||||||
modeNeutral: { zh: '中性:50%买 + 50%卖(默认)', en: 'Neutral: 50% buy + 50% sell (default)' },
|
|
||||||
modeLongBias: { zh: '偏多:X%买 + (100-X)%卖', en: 'Long Bias: X% buy + (100-X)% sell' },
|
|
||||||
modeLong: { zh: '全多:100%买 + 0%卖', en: 'Long: 100% buy + 0% sell' },
|
|
||||||
modeShortBias: { zh: '偏空:(100-X)%买 + X%卖', en: 'Short Bias: (100-X)% buy + X% sell' },
|
|
||||||
modeShort: { zh: '全空:0%买 + 100%卖', en: 'Short: 0% buy + 100% sell' },
|
|
||||||
}
|
|
||||||
return translations[key]?.[language] || key
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateField = <K extends keyof GridStrategyConfig>(
|
const updateField = <K extends keyof GridStrategyConfig>(
|
||||||
key: K,
|
key: K,
|
||||||
value: GridStrategyConfig[K]
|
value: GridStrategyConfig[K]
|
||||||
@@ -125,7 +61,7 @@ export function GridConfigEditor({
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<DollarSign className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
<DollarSign className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('tradingPair')}
|
{ts(gridConfig.tradingPair, language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -133,10 +69,10 @@ export function GridConfigEditor({
|
|||||||
{/* Symbol */}
|
{/* Symbol */}
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('symbol')}
|
{ts(gridConfig.symbol, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('symbolDesc')}
|
{ts(gridConfig.symbolDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<select
|
<select
|
||||||
value={config.symbol}
|
value={config.symbol}
|
||||||
@@ -157,10 +93,10 @@ export function GridConfigEditor({
|
|||||||
{/* Investment */}
|
{/* Investment */}
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('totalInvestment')}
|
{ts(gridConfig.totalInvestment, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('totalInvestmentDesc')}
|
{ts(gridConfig.totalInvestmentDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -177,10 +113,10 @@ export function GridConfigEditor({
|
|||||||
{/* Leverage */}
|
{/* Leverage */}
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('leverage')}
|
{ts(gridConfig.leverage, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('leverageDesc')}
|
{ts(gridConfig.leverageDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -201,7 +137,7 @@ export function GridConfigEditor({
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Grid className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
<Grid className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('gridParameters')}
|
{ts(gridConfig.gridParameters, language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -209,10 +145,10 @@ export function GridConfigEditor({
|
|||||||
{/* Grid Count */}
|
{/* Grid Count */}
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('gridCount')}
|
{ts(gridConfig.gridCount, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('gridCountDesc')}
|
{ts(gridConfig.gridCountDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -229,10 +165,10 @@ export function GridConfigEditor({
|
|||||||
{/* Distribution */}
|
{/* Distribution */}
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('distribution')}
|
{ts(gridConfig.distribution, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('distributionDesc')}
|
{ts(gridConfig.distributionDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<select
|
<select
|
||||||
value={config.distribution}
|
value={config.distribution}
|
||||||
@@ -241,9 +177,9 @@ export function GridConfigEditor({
|
|||||||
className="w-full px-3 py-2 rounded"
|
className="w-full px-3 py-2 rounded"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
>
|
>
|
||||||
<option value="uniform">{t('uniform')}</option>
|
<option value="uniform">{ts(gridConfig.uniform, language)}</option>
|
||||||
<option value="gaussian">{t('gaussian')}</option>
|
<option value="gaussian">{ts(gridConfig.gaussian, language)}</option>
|
||||||
<option value="pyramid">{t('pyramid')}</option>
|
<option value="pyramid">{ts(gridConfig.pyramid, language)}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,7 +190,7 @@ export function GridConfigEditor({
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<TrendingUp className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
<TrendingUp className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('priceBounds')}
|
{ts(gridConfig.priceBounds, language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -263,10 +199,10 @@ export function GridConfigEditor({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm" style={{ color: '#EAECEF' }}>
|
||||||
{t('useAtrBounds')}
|
{ts(gridConfig.useAtrBounds, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{t('useAtrBoundsDesc')}
|
{ts(gridConfig.useAtrBoundsDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
@@ -285,10 +221,10 @@ export function GridConfigEditor({
|
|||||||
{config.use_atr_bounds ? (
|
{config.use_atr_bounds ? (
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('atrMultiplier')}
|
{ts(gridConfig.atrMultiplier, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('atrMultiplierDesc')}
|
{ts(gridConfig.atrMultiplierDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -306,10 +242,10 @@ export function GridConfigEditor({
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('upperPrice')}
|
{ts(gridConfig.upperPrice, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('upperPriceDesc')}
|
{ts(gridConfig.upperPriceDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -324,10 +260,10 @@ export function GridConfigEditor({
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('lowerPrice')}
|
{ts(gridConfig.lowerPrice, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('lowerPriceDesc')}
|
{ts(gridConfig.lowerPriceDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -349,17 +285,17 @@ export function GridConfigEditor({
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('riskControl')}
|
{ts(gridConfig.riskControl, language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('maxDrawdown')}
|
{ts(gridConfig.maxDrawdown, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('maxDrawdownDesc')}
|
{ts(gridConfig.maxDrawdownDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -375,10 +311,10 @@ export function GridConfigEditor({
|
|||||||
|
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('stopLoss')}
|
{ts(gridConfig.stopLoss, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('stopLossDesc')}
|
{ts(gridConfig.stopLossDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -394,10 +330,10 @@ export function GridConfigEditor({
|
|||||||
|
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('dailyLossLimit')}
|
{ts(gridConfig.dailyLossLimit, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('dailyLossLimitDesc')}
|
{ts(gridConfig.dailyLossLimitDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -417,10 +353,10 @@ export function GridConfigEditor({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm" style={{ color: '#EAECEF' }}>
|
||||||
{t('useMakerOnly')}
|
{ts(gridConfig.useMakerOnly, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{t('useMakerOnlyDesc')}
|
{ts(gridConfig.useMakerOnlyDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
@@ -442,7 +378,7 @@ export function GridConfigEditor({
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Compass className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
<Compass className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('directionAdjust')}
|
{ts(gridConfig.directionAdjust, language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -451,10 +387,10 @@ export function GridConfigEditor({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm" style={{ color: '#EAECEF' }}>
|
||||||
{t('enableDirectionAdjust')}
|
{ts(gridConfig.enableDirectionAdjust, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{t('enableDirectionAdjustDesc')}
|
{ts(gridConfig.enableDirectionAdjustDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
@@ -475,30 +411,30 @@ export function GridConfigEditor({
|
|||||||
{/* Direction Modes Explanation */}
|
{/* Direction Modes Explanation */}
|
||||||
<div className="p-4 rounded-lg mb-4" style={{ background: '#1E2329', border: '1px solid #F0B90B33' }}>
|
<div className="p-4 rounded-lg mb-4" style={{ background: '#1E2329', border: '1px solid #F0B90B33' }}>
|
||||||
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
|
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
|
||||||
📊 {t('directionModes')}
|
📊 {ts(gridConfig.directionModes, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs" style={{ color: '#848E9C' }}>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs" style={{ color: '#848E9C' }}>
|
||||||
<div>• {t('modeNeutral')}</div>
|
<div>• {ts(gridConfig.modeNeutral, language)}</div>
|
||||||
<div>• <span style={{ color: '#0ECB81' }}>{t('modeLongBias')}</span></div>
|
<div>• <span style={{ color: '#0ECB81' }}>{ts(gridConfig.modeLongBias, language)}</span></div>
|
||||||
<div>• <span style={{ color: '#0ECB81' }}>{t('modeLong')}</span></div>
|
<div>• <span style={{ color: '#0ECB81' }}>{ts(gridConfig.modeLong, language)}</span></div>
|
||||||
<div>• <span style={{ color: '#F6465D' }}>{t('modeShortBias')}</span></div>
|
<div>• <span style={{ color: '#F6465D' }}>{ts(gridConfig.modeShortBias, language)}</span></div>
|
||||||
<div>• <span style={{ color: '#F6465D' }}>{t('modeShort')}</span></div>
|
<div>• <span style={{ color: '#F6465D' }}>{ts(gridConfig.modeShort, language)}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs mt-3 pt-2 border-t border-zinc-700" style={{ color: '#848E9C' }}>
|
<p className="text-xs mt-3 pt-2 border-t border-zinc-700" style={{ color: '#848E9C' }}>
|
||||||
💡 {t('directionExplain')}
|
💡 {ts(gridConfig.directionExplain, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bias Strength */}
|
{/* Bias Strength */}
|
||||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('directionBiasRatio')} (X)
|
{ts(gridConfig.directionBiasRatio, language)} (X)
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-1" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-1" style={{ color: '#848E9C' }}>
|
||||||
{t('directionBiasRatioDesc')}
|
{ts(gridConfig.directionBiasRatioDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs mb-3" style={{ color: '#F0B90B' }}>
|
<p className="text-xs mb-3" style={{ color: '#F0B90B' }}>
|
||||||
{t('directionBiasExplain')}
|
{ts(gridConfig.directionBiasExplain, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
@@ -518,12 +454,12 @@ export function GridConfigEditor({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-2 grid grid-cols-2 gap-2 text-xs">
|
<div className="mt-2 grid grid-cols-2 gap-2 text-xs">
|
||||||
<div className="p-2 rounded" style={{ background: '#0ECB8115', border: '1px solid #0ECB8130' }}>
|
<div className="p-2 rounded" style={{ background: '#0ECB8115', border: '1px solid #0ECB8130' }}>
|
||||||
<span style={{ color: '#0ECB81' }}>偏多/Long Bias: </span>
|
<span style={{ color: '#0ECB81' }}>Long Bias: </span>
|
||||||
<span style={{ color: '#EAECEF' }}>{Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% 买 + {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% 卖</span>
|
<span style={{ color: '#EAECEF' }}>{Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% {ts(gridConfig.buy, language)} + {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% {ts(gridConfig.sell, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 rounded" style={{ background: '#F6465D15', border: '1px solid #F6465D30' }}>
|
<div className="p-2 rounded" style={{ background: '#F6465D15', border: '1px solid #F6465D30' }}>
|
||||||
<span style={{ color: '#F6465D' }}>偏空/Short Bias: </span>
|
<span style={{ color: '#F6465D' }}>Short Bias: </span>
|
||||||
<span style={{ color: '#EAECEF' }}>{Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% 买 + {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% 卖</span>
|
<span style={{ color: '#EAECEF' }}>{Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% {ts(gridConfig.buy, language)} + {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% {ts(gridConfig.sell, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Shield, TrendingUp, AlertTriangle, Activity, Box, ChevronDown, ChevronUp } from 'lucide-react'
|
import { Shield, TrendingUp, AlertTriangle, Activity, Box, ChevronDown, ChevronUp } from 'lucide-react'
|
||||||
import type { GridRiskInfo } from '../../types'
|
import type { GridRiskInfo } from '../../types'
|
||||||
|
import { gridRisk, ts } from '../../i18n/strategy-translations'
|
||||||
|
|
||||||
interface GridRiskPanelProps {
|
interface GridRiskPanelProps {
|
||||||
traderId: string
|
traderId: string
|
||||||
@@ -18,66 +19,6 @@ export function GridRiskPanel({
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
const t = (key: string) => {
|
|
||||||
const translations: Record<string, Record<string, string>> = {
|
|
||||||
// Section titles
|
|
||||||
gridRisk: { zh: '网格风控', en: 'Grid Risk' },
|
|
||||||
leverageInfo: { zh: '杠杆', en: 'Leverage' },
|
|
||||||
positionInfo: { zh: '仓位', en: 'Position' },
|
|
||||||
liquidationInfo: { zh: '清算', en: 'Liquidation' },
|
|
||||||
marketState: { zh: '市场', en: 'Market' },
|
|
||||||
boxState: { zh: '箱体', en: 'Box' },
|
|
||||||
|
|
||||||
// Leverage
|
|
||||||
currentLeverage: { zh: '当前', en: 'Current' },
|
|
||||||
effectiveLeverage: { zh: '有效', en: 'Effective' },
|
|
||||||
recommendedLeverage: { zh: '建议', en: 'Recommend' },
|
|
||||||
|
|
||||||
// Position
|
|
||||||
currentPosition: { zh: '当前', en: 'Current' },
|
|
||||||
maxPosition: { zh: '最大', en: 'Max' },
|
|
||||||
positionPercent: { zh: '占比', en: 'Usage' },
|
|
||||||
|
|
||||||
// Liquidation
|
|
||||||
liquidationPrice: { zh: '清算价', en: 'Liq Price' },
|
|
||||||
liquidationDistance: { zh: '距离', en: 'Distance' },
|
|
||||||
|
|
||||||
// Market
|
|
||||||
regimeLevel: { zh: '波动', en: 'Regime' },
|
|
||||||
currentPrice: { zh: '价格', en: 'Price' },
|
|
||||||
breakoutLevel: { zh: '突破', en: 'Breakout' },
|
|
||||||
breakoutDirection: { zh: '方向', en: 'Direction' },
|
|
||||||
|
|
||||||
// Box
|
|
||||||
shortBox: { zh: '短期', en: 'Short' },
|
|
||||||
midBox: { zh: '中期', en: 'Mid' },
|
|
||||||
longBox: { zh: '长期', en: 'Long' },
|
|
||||||
|
|
||||||
// Regime levels
|
|
||||||
narrow: { zh: '窄幅', en: 'Narrow' },
|
|
||||||
standard: { zh: '标准', en: 'Standard' },
|
|
||||||
wide: { zh: '宽幅', en: 'Wide' },
|
|
||||||
volatile: { zh: '剧烈', en: 'Volatile' },
|
|
||||||
trending: { zh: '趋势', en: 'Trending' },
|
|
||||||
|
|
||||||
// Breakout levels
|
|
||||||
none: { zh: '无', en: 'None' },
|
|
||||||
short: { zh: '短期', en: 'Short' },
|
|
||||||
mid: { zh: '中期', en: 'Mid' },
|
|
||||||
long: { zh: '长期', en: 'Long' },
|
|
||||||
|
|
||||||
// Directions
|
|
||||||
up: { zh: '↑', en: '↑' },
|
|
||||||
down: { zh: '↓', en: '↓' },
|
|
||||||
|
|
||||||
// Status
|
|
||||||
loading: { zh: '加载中...', en: 'Loading...' },
|
|
||||||
error: { zh: '加载失败', en: 'Load Failed' },
|
|
||||||
noData: { zh: '暂无数据', en: 'No Data' },
|
|
||||||
}
|
|
||||||
return translations[key]?.[language] || key
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchRiskInfo = useCallback(async () => {
|
const fetchRiskInfo = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token')
|
const token = localStorage.getItem('auth_token')
|
||||||
@@ -153,7 +94,7 @@ export function GridRiskPanel({
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
|
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
|
||||||
{t('loading')}
|
{ts(gridRisk.loading, language)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -161,7 +102,7 @@ export function GridRiskPanel({
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-3 text-center text-xs" style={{ color: '#F6465D' }}>
|
<div className="p-3 text-center text-xs" style={{ color: '#F6465D' }}>
|
||||||
{t('error')}: {error}
|
{ts(gridRisk.error, language)}: {error}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -169,7 +110,7 @@ export function GridRiskPanel({
|
|||||||
if (!riskInfo) {
|
if (!riskInfo) {
|
||||||
return (
|
return (
|
||||||
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
|
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
|
||||||
{t('noData')}
|
{ts(gridRisk.noData, language)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -184,7 +125,7 @@ export function GridRiskPanel({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Shield className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
<Shield className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
|
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
|
||||||
{t('gridRisk')}
|
{ts(gridRisk.gridRisk, language)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -194,7 +135,7 @@ export function GridRiskPanel({
|
|||||||
className="px-2 py-0.5 rounded"
|
className="px-2 py-0.5 rounded"
|
||||||
style={{ background: getRegimeColor(riskInfo.regime_level) + '20', color: getRegimeColor(riskInfo.regime_level) }}
|
style={{ background: getRegimeColor(riskInfo.regime_level) + '20', color: getRegimeColor(riskInfo.regime_level) }}
|
||||||
>
|
>
|
||||||
{t(riskInfo.regime_level || 'standard')}
|
{ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
||||||
{riskInfo.effective_leverage.toFixed(1)}x
|
{riskInfo.effective_leverage.toFixed(1)}x
|
||||||
@@ -223,19 +164,19 @@ export function GridRiskPanel({
|
|||||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||||
<div className="flex items-center gap-1 mb-2">
|
<div className="flex items-center gap-1 mb-2">
|
||||||
<TrendingUp className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
<TrendingUp className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('leverageInfo')}</span>
|
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.leverageInfo, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-1 text-xs">
|
<div className="grid grid-cols-3 gap-1 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('currentLeverage')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentLeverage, language)}</div>
|
||||||
<div className="font-mono" style={{ color: '#EAECEF' }}>{riskInfo.current_leverage}x</div>
|
<div className="font-mono" style={{ color: '#EAECEF' }}>{riskInfo.current_leverage}x</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('effectiveLeverage')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.effectiveLeverage, language)}</div>
|
||||||
<div className="font-mono" style={{ color: '#F0B90B' }}>{riskInfo.effective_leverage.toFixed(2)}x</div>
|
<div className="font-mono" style={{ color: '#F0B90B' }}>{riskInfo.effective_leverage.toFixed(2)}x</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('recommendedLeverage')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.recommendedLeverage, language)}</div>
|
||||||
<div
|
<div
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
style={{ color: riskInfo.current_leverage > riskInfo.recommended_leverage ? '#F6465D' : '#0ECB81' }}
|
style={{ color: riskInfo.current_leverage > riskInfo.recommended_leverage ? '#F6465D' : '#0ECB81' }}
|
||||||
@@ -250,19 +191,19 @@ export function GridRiskPanel({
|
|||||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||||
<div className="flex items-center gap-1 mb-2">
|
<div className="flex items-center gap-1 mb-2">
|
||||||
<Activity className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
<Activity className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('positionInfo')}</span>
|
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.positionInfo, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-1 text-xs">
|
<div className="grid grid-cols-3 gap-1 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('currentPosition')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPosition, language)}</div>
|
||||||
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.current_position)}</div>
|
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.current_position)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('maxPosition')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.maxPosition, language)}</div>
|
||||||
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.max_position)}</div>
|
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.max_position)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('positionPercent')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.positionPercent, language)}</div>
|
||||||
<div className="font-mono" style={{ color: getPositionColor(riskInfo.position_percent) }}>
|
<div className="font-mono" style={{ color: getPositionColor(riskInfo.position_percent) }}>
|
||||||
{riskInfo.position_percent.toFixed(1)}%
|
{riskInfo.position_percent.toFixed(1)}%
|
||||||
</div>
|
</div>
|
||||||
@@ -284,32 +225,32 @@ export function GridRiskPanel({
|
|||||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||||
<div className="flex items-center gap-1 mb-2">
|
<div className="flex items-center gap-1 mb-2">
|
||||||
<Shield className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
<Shield className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('marketState')}</span>
|
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.marketState, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('regimeLevel')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.regimeLevel, language)}</div>
|
||||||
<div className="font-medium" style={{ color: getRegimeColor(riskInfo.regime_level) }}>
|
<div className="font-medium" style={{ color: getRegimeColor(riskInfo.regime_level) }}>
|
||||||
{t(riskInfo.regime_level || 'standard')}
|
{ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('currentPrice')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPrice, language)}</div>
|
||||||
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatPrice(riskInfo.current_price)}</div>
|
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatPrice(riskInfo.current_price)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('breakoutLevel')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutLevel, language)}</div>
|
||||||
<div className="font-medium" style={{ color: getBreakoutColor(riskInfo.breakout_level) }}>
|
<div className="font-medium" style={{ color: getBreakoutColor(riskInfo.breakout_level) }}>
|
||||||
{t(riskInfo.breakout_level || 'none')}
|
{ts(gridRisk[(riskInfo.breakout_level || 'none') as keyof typeof gridRisk], language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('breakoutDirection')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutDirection, language)}</div>
|
||||||
<div
|
<div
|
||||||
className="font-medium"
|
className="font-medium"
|
||||||
style={{ color: riskInfo.breakout_direction === 'up' ? '#0ECB81' : riskInfo.breakout_direction === 'down' ? '#F6465D' : '#848E9C' }}
|
style={{ color: riskInfo.breakout_direction === 'up' ? '#0ECB81' : riskInfo.breakout_direction === 'down' ? '#F6465D' : '#848E9C' }}
|
||||||
>
|
>
|
||||||
{riskInfo.breakout_direction ? t(riskInfo.breakout_direction) : '-'}
|
{riskInfo.breakout_direction ? ts(gridRisk[riskInfo.breakout_direction as keyof typeof gridRisk], language) : '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -319,17 +260,17 @@ export function GridRiskPanel({
|
|||||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||||
<div className="flex items-center gap-1 mb-2">
|
<div className="flex items-center gap-1 mb-2">
|
||||||
<AlertTriangle className="w-3 h-3" style={{ color: '#F6465D' }} />
|
<AlertTriangle className="w-3 h-3" style={{ color: '#F6465D' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('liquidationInfo')}</span>
|
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.liquidationInfo, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('liquidationPrice')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationPrice, language)}</div>
|
||||||
<div className="font-mono" style={{ color: '#F6465D' }}>
|
<div className="font-mono" style={{ color: '#F6465D' }}>
|
||||||
{riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'}
|
{riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: '#5E6673' }}>{t('liquidationDistance')}</div>
|
<div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationDistance, language)}</div>
|
||||||
<div className="font-mono" style={{ color: '#F6465D' }}>
|
<div className="font-mono" style={{ color: '#F6465D' }}>
|
||||||
{riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'}
|
{riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'}
|
||||||
</div>
|
</div>
|
||||||
@@ -342,23 +283,23 @@ export function GridRiskPanel({
|
|||||||
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
|
||||||
<div className="flex items-center gap-1 mb-2">
|
<div className="flex items-center gap-1 mb-2">
|
||||||
<Box className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
<Box className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('boxState')}</span>
|
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.boxState, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span style={{ color: '#5E6673' }}>{t('shortBox')}</span>
|
<span style={{ color: '#5E6673' }}>{ts(gridRisk.shortBox, language)}</span>
|
||||||
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
||||||
{formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)}
|
{formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span style={{ color: '#5E6673' }}>{t('midBox')}</span>
|
<span style={{ color: '#5E6673' }}>{ts(gridRisk.midBox, language)}</span>
|
||||||
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
||||||
{formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)}
|
{formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span style={{ color: '#5E6673' }}>{t('longBox')}</span>
|
<span style={{ color: '#5E6673' }}>{ts(gridRisk.longBox, language)}</span>
|
||||||
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
<span className="font-mono" style={{ color: '#EAECEF' }}>
|
||||||
{formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)}
|
{formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Clock, Activity, TrendingUp, BarChart2, Info, Lock, ExternalLink, Zap, Check, AlertCircle, Key } from 'lucide-react'
|
import { Clock, Activity, TrendingUp, BarChart2, Info, Lock, ExternalLink, Zap, Check, AlertCircle, Key } from 'lucide-react'
|
||||||
import type { IndicatorConfig } from '../../types'
|
import type { IndicatorConfig } from '../../types'
|
||||||
|
import { indicator, ts } from '../../i18n/strategy-translations'
|
||||||
|
|
||||||
// Default NofxOS API Key
|
// Default NofxOS API Key
|
||||||
const DEFAULT_NOFXOS_API_KEY = 'cm_568c67eae410d912c54c'
|
const DEFAULT_NOFXOS_API_KEY = 'cm_568c67eae410d912c54c'
|
||||||
@@ -11,7 +12,7 @@ interface IndicatorEditorProps {
|
|||||||
language: string
|
language: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有可用时间周期
|
// All available timeframes
|
||||||
const allTimeframes = [
|
const allTimeframes = [
|
||||||
{ value: '1m', label: '1m', category: 'scalp' },
|
{ value: '1m', label: '1m', category: 'scalp' },
|
||||||
{ value: '3m', label: '3m', category: 'scalp' },
|
{ value: '3m', label: '3m', category: 'scalp' },
|
||||||
@@ -35,92 +36,10 @@ export function IndicatorEditor({
|
|||||||
disabled,
|
disabled,
|
||||||
language,
|
language,
|
||||||
}: IndicatorEditorProps) {
|
}: IndicatorEditorProps) {
|
||||||
const t = (key: string) => {
|
// Get currently selected timeframes
|
||||||
const translations: Record<string, Record<string, string>> = {
|
|
||||||
// Section titles
|
|
||||||
marketData: { zh: '市场数据', en: 'Market Data' },
|
|
||||||
marketDataDesc: { zh: 'AI 分析所需的核心价格数据', en: 'Core price data for AI analysis' },
|
|
||||||
technicalIndicators: { zh: '技术指标', en: 'Technical Indicators' },
|
|
||||||
technicalIndicatorsDesc: { zh: '可选的技术分析指标,AI 可自行计算', en: 'Optional indicators, AI can calculate them' },
|
|
||||||
marketSentiment: { zh: '市场情绪', en: 'Market Sentiment' },
|
|
||||||
marketSentimentDesc: { zh: '持仓量、资金费率等市场情绪数据', en: 'OI, funding rate and market sentiment data' },
|
|
||||||
quantData: { zh: '量化数据', en: 'Quant Data' },
|
|
||||||
quantDataDesc: { zh: '资金流向、大户动向', en: 'Netflow, whale movements' },
|
|
||||||
|
|
||||||
// Timeframes
|
|
||||||
timeframes: { zh: '时间周期', en: 'Timeframes' },
|
|
||||||
timeframesDesc: { zh: '选择 K 线分析周期,★ 为主周期(双击设置)', en: 'Select K-line timeframes, ★ = primary (double-click)' },
|
|
||||||
klineCount: { zh: 'K 线数量', en: 'K-line Count' },
|
|
||||||
scalp: { zh: '超短', en: 'Scalp' },
|
|
||||||
intraday: { zh: '日内', en: 'Intraday' },
|
|
||||||
swing: { zh: '波段', en: 'Swing' },
|
|
||||||
position: { zh: '趋势', en: 'Position' },
|
|
||||||
|
|
||||||
// Data types
|
|
||||||
rawKlines: { zh: 'OHLCV 原始 K 线', en: 'Raw OHLCV K-lines' },
|
|
||||||
rawKlinesDesc: { zh: '必须 - 开高低收量原始数据,AI 核心分析依据', en: 'Required - Open/High/Low/Close/Volume data for AI' },
|
|
||||||
required: { zh: '必须', en: 'Required' },
|
|
||||||
|
|
||||||
// Indicators
|
|
||||||
ema: { zh: 'EMA 均线', en: 'EMA' },
|
|
||||||
emaDesc: { zh: '指数移动平均线', en: 'Exponential Moving Average' },
|
|
||||||
macd: { zh: 'MACD', en: 'MACD' },
|
|
||||||
macdDesc: { zh: '异同移动平均线', en: 'Moving Average Convergence Divergence' },
|
|
||||||
rsi: { zh: 'RSI', en: 'RSI' },
|
|
||||||
rsiDesc: { zh: '相对强弱指标', en: 'Relative Strength Index' },
|
|
||||||
atr: { zh: 'ATR', en: 'ATR' },
|
|
||||||
atrDesc: { zh: '真实波幅均值', en: 'Average True Range' },
|
|
||||||
boll: { zh: 'BOLL 布林带', en: 'Bollinger Bands' },
|
|
||||||
bollDesc: { zh: '布林带指标(上中下轨)', en: 'Upper/Middle/Lower Bands' },
|
|
||||||
volume: { zh: '成交量', en: 'Volume' },
|
|
||||||
volumeDesc: { zh: '交易量分析', en: 'Trading volume analysis' },
|
|
||||||
oi: { zh: '持仓量', en: 'Open Interest' },
|
|
||||||
oiDesc: { zh: '合约未平仓量', en: 'Futures open interest' },
|
|
||||||
fundingRate: { zh: '资金费率', en: 'Funding Rate' },
|
|
||||||
fundingRateDesc: { zh: '永续合约资金费率', en: 'Perpetual funding rate' },
|
|
||||||
|
|
||||||
// OI Ranking
|
|
||||||
oiRanking: { zh: 'OI 排行', en: 'OI Ranking' },
|
|
||||||
oiRankingDesc: { zh: '持仓量增减排行', en: 'OI change ranking' },
|
|
||||||
oiRankingNote: { zh: '显示持仓量增加/减少的币种排行,帮助发现资金流向', en: 'Shows coins with OI increase/decrease, helps identify capital flow' },
|
|
||||||
|
|
||||||
// NetFlow Ranking
|
|
||||||
netflowRanking: { zh: '资金流向', en: 'NetFlow' },
|
|
||||||
netflowRankingDesc: { zh: '机构/散户资金流向', en: 'Institution/retail fund flow' },
|
|
||||||
netflowRankingNote: { zh: '显示机构资金流入/流出排行,散户动向对比,发现聪明钱信号', en: 'Shows institution inflow/outflow ranking, retail flow comparison, Smart Money signals' },
|
|
||||||
|
|
||||||
// Price Ranking
|
|
||||||
priceRanking: { zh: '涨跌幅排行', en: 'Price Ranking' },
|
|
||||||
priceRankingDesc: { zh: '涨跌幅排行榜', en: 'Gainers/losers ranking' },
|
|
||||||
priceRankingNote: { zh: '显示涨幅/跌幅排行,结合资金流和持仓变化分析趋势强度', en: 'Shows top gainers/losers, combined with fund flow and OI for trend analysis' },
|
|
||||||
priceRankingMulti: { zh: '多周期', en: 'Multi-period' },
|
|
||||||
|
|
||||||
// Common settings
|
|
||||||
duration: { zh: '周期', en: 'Duration' },
|
|
||||||
limit: { zh: '数量', en: 'Limit' },
|
|
||||||
|
|
||||||
// Tips
|
|
||||||
aiCanCalculate: { zh: '💡 提示:AI 可自行计算这些指标,开启可减少 AI 计算量', en: '💡 Tip: AI can calculate these, enabling reduces AI workload' },
|
|
||||||
|
|
||||||
// NofxOS Data Provider
|
|
||||||
nofxosTitle: { zh: 'NofxOS 量化数据源', en: 'NofxOS Data Provider' },
|
|
||||||
nofxosDesc: { zh: '专业加密货币量化数据服务', en: 'Professional crypto quant data service' },
|
|
||||||
nofxosFeatures: { zh: 'AI500 · OI排行 · 资金流向 · 涨跌榜', en: 'AI500 · OI Ranking · Fund Flow · Price Ranking' },
|
|
||||||
viewApiDocs: { zh: 'API 文档', en: 'API Docs' },
|
|
||||||
apiKey: { zh: 'API Key', en: 'API Key' },
|
|
||||||
apiKeyPlaceholder: { zh: '输入 NofxOS API Key', en: 'Enter NofxOS API Key' },
|
|
||||||
fillDefault: { zh: '填入默认', en: 'Fill Default' },
|
|
||||||
connected: { zh: '已配置', en: 'Configured' },
|
|
||||||
notConfigured: { zh: '未配置', en: 'Not Configured' },
|
|
||||||
nofxosDataSources: { zh: 'NofxOS 数据源', en: 'NofxOS Data Sources' },
|
|
||||||
}
|
|
||||||
return translations[key]?.[language] || key
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前选中的时间周期
|
|
||||||
const selectedTimeframes = config.klines.selected_timeframes || [config.klines.primary_timeframe]
|
const selectedTimeframes = config.klines.selected_timeframes || [config.klines.primary_timeframe]
|
||||||
|
|
||||||
// 切换时间周期选择
|
// Toggle timeframe selection
|
||||||
const toggleTimeframe = (tf: string) => {
|
const toggleTimeframe = (tf: string) => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
const current = [...selectedTimeframes]
|
const current = [...selectedTimeframes]
|
||||||
@@ -153,7 +72,7 @@ export function IndicatorEditor({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置主时间周期
|
// Set primary timeframe
|
||||||
const setPrimaryTimeframe = (tf: string) => {
|
const setPrimaryTimeframe = (tf: string) => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
onChange({
|
onChange({
|
||||||
@@ -218,10 +137,10 @@ export function IndicatorEditor({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<h3 className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
{t('nofxosTitle')}
|
{ts(indicator.nofxosTitle, language)}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>
|
<span className="text-[10px]" style={{ color: '#848E9C' }}>
|
||||||
{t('nofxosFeatures')}
|
{ts(indicator.nofxosFeatures, language)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,12 +150,12 @@ export function IndicatorEditor({
|
|||||||
{hasApiKey ? (
|
{hasApiKey ? (
|
||||||
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
|
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
|
||||||
<Check className="w-3 h-3" />
|
<Check className="w-3 h-3" />
|
||||||
{t('connected')}
|
{ts(indicator.connected, language)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}>
|
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}>
|
||||||
<AlertCircle className="w-3 h-3" />
|
<AlertCircle className="w-3 h-3" />
|
||||||
{t('notConfigured')}
|
{ts(indicator.notConfigured, language)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
@@ -250,7 +169,7 @@ export function IndicatorEditor({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-3 h-3" />
|
<ExternalLink className="w-3 h-3" />
|
||||||
{t('viewApiDocs')}
|
{ts(indicator.viewApiDocs, language)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -264,7 +183,7 @@ export function IndicatorEditor({
|
|||||||
value={config.nofxos_api_key || ''}
|
value={config.nofxos_api_key || ''}
|
||||||
onChange={(e) => !disabled && onChange({ ...config, nofxos_api_key: e.target.value })}
|
onChange={(e) => !disabled && onChange({ ...config, nofxos_api_key: e.target.value })}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={t('apiKeyPlaceholder')}
|
placeholder={ts(indicator.apiKeyPlaceholder, language)}
|
||||||
className="w-full pl-9 pr-3 py-2 rounded-lg text-sm font-mono"
|
className="w-full pl-9 pr-3 py-2 rounded-lg text-sm font-mono"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(30, 35, 41, 0.8)',
|
background: 'rgba(30, 35, 41, 0.8)',
|
||||||
@@ -283,7 +202,7 @@ export function IndicatorEditor({
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('fillDefault')}
|
{ts(indicator.fillDefault, language)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -291,7 +210,7 @@ export function IndicatorEditor({
|
|||||||
{/* NofxOS Data Sources Grid */}
|
{/* NofxOS Data Sources Grid */}
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="text-[10px] font-medium mb-2" style={{ color: '#848E9C' }}>
|
<div className="text-[10px] font-medium mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('nofxosDataSources')}
|
{ts(indicator.nofxosDataSources, language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{/* Quant Data */}
|
{/* Quant Data */}
|
||||||
@@ -307,7 +226,7 @@ export function IndicatorEditor({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full" style={{ background: '#60a5fa' }} />
|
<div className="w-2 h-2 rounded-full" style={{ background: '#60a5fa' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.quantData, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -317,7 +236,7 @@ export function IndicatorEditor({
|
|||||||
className="w-3.5 h-3.5 rounded accent-blue-500"
|
className="w-3.5 h-3.5 rounded accent-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('quantDataDesc')}</p>
|
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.quantDataDesc, language)}</p>
|
||||||
{config.enable_quant_data && (
|
{config.enable_quant_data && (
|
||||||
<div className="flex gap-3 mt-2">
|
<div className="flex gap-3 mt-2">
|
||||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||||
@@ -362,7 +281,7 @@ export function IndicatorEditor({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} />
|
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('oiRanking')}</span>
|
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.oiRanking, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -377,7 +296,7 @@ export function IndicatorEditor({
|
|||||||
className="w-3.5 h-3.5 rounded accent-green-500"
|
className="w-3.5 h-3.5 rounded accent-green-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('oiRankingDesc')}</p>
|
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.oiRankingDesc, language)}</p>
|
||||||
{config.enable_oi_ranking && (
|
{config.enable_oi_ranking && (
|
||||||
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||||
<select
|
<select
|
||||||
@@ -422,7 +341,7 @@ export function IndicatorEditor({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full" style={{ background: '#f59e0b' }} />
|
<div className="w-2 h-2 rounded-full" style={{ background: '#f59e0b' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('netflowRanking')}</span>
|
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.netflowRanking, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -437,7 +356,7 @@ export function IndicatorEditor({
|
|||||||
className="w-3.5 h-3.5 rounded accent-amber-500"
|
className="w-3.5 h-3.5 rounded accent-amber-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('netflowRankingDesc')}</p>
|
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.netflowRankingDesc, language)}</p>
|
||||||
{config.enable_netflow_ranking && (
|
{config.enable_netflow_ranking && (
|
||||||
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||||
<select
|
<select
|
||||||
@@ -482,7 +401,7 @@ export function IndicatorEditor({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full" style={{ background: '#ec4899' }} />
|
<div className="w-2 h-2 rounded-full" style={{ background: '#ec4899' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('priceRanking')}</span>
|
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.priceRanking, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -497,7 +416,7 @@ export function IndicatorEditor({
|
|||||||
className="w-3.5 h-3.5 rounded accent-pink-500"
|
className="w-3.5 h-3.5 rounded accent-pink-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('priceRankingDesc')}</p>
|
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{ts(indicator.priceRankingDesc, language)}</p>
|
||||||
{config.enable_price_ranking && (
|
{config.enable_price_ranking && (
|
||||||
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||||
<select
|
<select
|
||||||
@@ -510,7 +429,7 @@ export function IndicatorEditor({
|
|||||||
<option value="1h">1h</option>
|
<option value="1h">1h</option>
|
||||||
<option value="4h">4h</option>
|
<option value="4h">4h</option>
|
||||||
<option value="24h">24h</option>
|
<option value="24h">24h</option>
|
||||||
<option value="1h,4h,24h">{t('priceRankingMulti')}</option>
|
<option value="1h,4h,24h">{ts(indicator.priceRankingMulti, language)}</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={config.price_ranking_limit || 10}
|
value={config.price_ranking_limit || 10}
|
||||||
@@ -531,7 +450,7 @@ export function IndicatorEditor({
|
|||||||
<div className="flex items-center gap-2 mt-3 p-2 rounded-lg" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.2)' }}>
|
<div className="flex items-center gap-2 mt-3 p-2 rounded-lg" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.2)' }}>
|
||||||
<AlertCircle className="w-4 h-4 flex-shrink-0" style={{ color: '#F6465D' }} />
|
<AlertCircle className="w-4 h-4 flex-shrink-0" style={{ color: '#F6465D' }} />
|
||||||
<span className="text-[10px]" style={{ color: '#F6465D' }}>
|
<span className="text-[10px]" style={{ color: '#F6465D' }}>
|
||||||
{language === 'zh' ? '请配置 API Key 以启用 NofxOS 数据源' : 'Please configure API Key to enable NofxOS data sources'}
|
{ts(indicator.configureApiKey, language)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -545,8 +464,8 @@ export function IndicatorEditor({
|
|||||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||||
<BarChart2 className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
<BarChart2 className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('marketData')}</span>
|
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.marketData, language)}</span>
|
||||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('marketDataDesc')}</span>
|
<span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.marketDataDesc, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3 space-y-4">
|
<div className="p-3 space-y-4">
|
||||||
@@ -558,13 +477,13 @@ export function IndicatorEditor({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('rawKlines')}</span>
|
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.rawKlines, language)}</span>
|
||||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium flex items-center gap-1" style={{ background: 'rgba(240, 185, 11, 0.2)', color: '#F0B90B' }}>
|
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium flex items-center gap-1" style={{ background: 'rgba(240, 185, 11, 0.2)', color: '#F0B90B' }}>
|
||||||
<Lock className="w-2.5 h-2.5" />
|
<Lock className="w-2.5 h-2.5" />
|
||||||
{t('required')}
|
{ts(indicator.required, language)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs mt-0.5" style={{ color: '#848E9C' }}>{t('rawKlinesDesc')}</p>
|
<p className="text-xs mt-0.5" style={{ color: '#848E9C' }}>{ts(indicator.rawKlinesDesc, language)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -580,10 +499,10 @@ export function IndicatorEditor({
|
|||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="w-3.5 h-3.5" style={{ color: '#848E9C' }} />
|
<Clock className="w-3.5 h-3.5" style={{ color: '#848E9C' }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('timeframes')}</span>
|
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.timeframes, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>{t('klineCount')}:</span>
|
<span className="text-[10px]" style={{ color: '#848E9C' }}>{ts(indicator.klineCount, language)}:</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={config.klines.primary_count}
|
value={config.klines.primary_count}
|
||||||
@@ -602,7 +521,7 @@ export function IndicatorEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] mb-2" style={{ color: '#5E6673' }}>{t('timeframesDesc')}</p>
|
<p className="text-[10px] mb-2" style={{ color: '#5E6673' }}>{ts(indicator.timeframesDesc, language)}</p>
|
||||||
|
|
||||||
{/* Timeframe Grid */}
|
{/* Timeframe Grid */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@@ -611,7 +530,7 @@ export function IndicatorEditor({
|
|||||||
return (
|
return (
|
||||||
<div key={category} className="flex items-center gap-2">
|
<div key={category} className="flex items-center gap-2">
|
||||||
<span className="text-[10px] w-10 flex-shrink-0" style={{ color: categoryColors[category] }}>
|
<span className="text-[10px] w-10 flex-shrink-0" style={{ color: categoryColors[category] }}>
|
||||||
{t(category)}
|
{ts(indicator[category], language)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{categoryTfs.map((tf) => {
|
{categoryTfs.map((tf) => {
|
||||||
@@ -654,15 +573,15 @@ export function IndicatorEditor({
|
|||||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||||
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('technicalIndicators')}</span>
|
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.technicalIndicators, language)}</span>
|
||||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('technicalIndicatorsDesc')}</span>
|
<span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.technicalIndicatorsDesc, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
{/* Tip */}
|
{/* Tip */}
|
||||||
<div className="flex items-start gap-2 mb-3 p-2 rounded" style={{ background: 'rgba(14, 203, 129, 0.05)' }}>
|
<div className="flex items-start gap-2 mb-3 p-2 rounded" style={{ background: 'rgba(14, 203, 129, 0.05)' }}>
|
||||||
<Info className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" style={{ color: '#0ECB81' }} />
|
<Info className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" style={{ color: '#0ECB81' }} />
|
||||||
<p className="text-[10px]" style={{ color: '#848E9C' }}>{t('aiCanCalculate')}</p>
|
<p className="text-[10px]" style={{ color: '#848E9C' }}>{ts(indicator.aiCanCalculate, language)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Indicator Grid */}
|
{/* Indicator Grid */}
|
||||||
@@ -685,7 +604,7 @@ export function IndicatorEditor({
|
|||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
|
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t(label)}</span>
|
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator[label as keyof typeof indicator], language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -695,7 +614,7 @@ export function IndicatorEditor({
|
|||||||
className="w-4 h-4 rounded accent-yellow-500"
|
className="w-4 h-4 rounded accent-yellow-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] mb-1.5" style={{ color: '#5E6673' }}>{t(desc)}</p>
|
<p className="text-[10px] mb-1.5" style={{ color: '#5E6673' }}>{ts(indicator[desc as keyof typeof indicator], language)}</p>
|
||||||
{periodKey && config[key as keyof IndicatorConfig] && (
|
{periodKey && config[key as keyof IndicatorConfig] && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -726,8 +645,8 @@ export function IndicatorEditor({
|
|||||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||||
<TrendingUp className="w-4 h-4" style={{ color: '#22c55e' }} />
|
<TrendingUp className="w-4 h-4" style={{ color: '#22c55e' }} />
|
||||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('marketSentiment')}</span>
|
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{ts(indicator.marketSentiment, language)}</span>
|
||||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('marketSentimentDesc')}</span>
|
<span className="text-xs" style={{ color: '#848E9C' }}>- {ts(indicator.marketSentimentDesc, language)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
@@ -748,7 +667,7 @@ export function IndicatorEditor({
|
|||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
|
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t(label)}</span>
|
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{ts(indicator[label as keyof typeof indicator], language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -758,7 +677,7 @@ export function IndicatorEditor({
|
|||||||
className="w-4 h-4 rounded accent-yellow-500"
|
className="w-4 h-4 rounded accent-yellow-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px]" style={{ color: '#5E6673' }}>{t(desc)}</p>
|
<p className="text-[10px]" style={{ color: '#5E6673' }}>{ts(indicator[desc as keyof typeof indicator], language)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ChevronDown, ChevronRight, RotateCcw, FileText } from 'lucide-react'
|
import { ChevronDown, ChevronRight, RotateCcw, FileText } from 'lucide-react'
|
||||||
import type { PromptSectionsConfig } from '../../types'
|
import type { PromptSectionsConfig } from '../../types'
|
||||||
|
import { promptSections as promptSectionsI18n, ts } from '../../i18n/strategy-translations'
|
||||||
|
|
||||||
interface PromptSectionsEditorProps {
|
interface PromptSectionsEditorProps {
|
||||||
config: PromptSectionsConfig | undefined
|
config: PromptSectionsConfig | undefined
|
||||||
@@ -54,29 +55,11 @@ export function PromptSectionsEditor({
|
|||||||
decision_process: false,
|
decision_process: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const t = (key: string) => {
|
|
||||||
const translations: Record<string, Record<string, string>> = {
|
|
||||||
promptSections: { zh: 'System Prompt 自定义', en: 'System Prompt Customization' },
|
|
||||||
promptSectionsDesc: { zh: '自定义 AI 行为和决策逻辑(输出格式和风控规则不可修改)', en: 'Customize AI behavior and decision logic (output format and risk rules are fixed)' },
|
|
||||||
roleDefinition: { zh: '角色定义', en: 'Role Definition' },
|
|
||||||
roleDefinitionDesc: { zh: '定义 AI 的身份和核心目标', en: 'Define AI identity and core objectives' },
|
|
||||||
tradingFrequency: { zh: '交易频率', en: 'Trading Frequency' },
|
|
||||||
tradingFrequencyDesc: { zh: '设定交易频率预期和过度交易警告', en: 'Set trading frequency expectations and overtrading warnings' },
|
|
||||||
entryStandards: { zh: '开仓标准', en: 'Entry Standards' },
|
|
||||||
entryStandardsDesc: { zh: '定义开仓信号条件和避免事项', en: 'Define entry signal conditions and avoidances' },
|
|
||||||
decisionProcess: { zh: '决策流程', en: 'Decision Process' },
|
|
||||||
decisionProcessDesc: { zh: '设定决策步骤和思考流程', en: 'Set decision steps and thinking process' },
|
|
||||||
resetToDefault: { zh: '重置为默认', en: 'Reset to Default' },
|
|
||||||
chars: { zh: '字符', en: 'chars' },
|
|
||||||
}
|
|
||||||
return translations[key]?.[language] || key
|
|
||||||
}
|
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{ key: 'role_definition', label: t('roleDefinition'), desc: t('roleDefinitionDesc') },
|
{ key: 'role_definition', label: ts(promptSectionsI18n.roleDefinition, language), desc: ts(promptSectionsI18n.roleDefinitionDesc, language) },
|
||||||
{ key: 'trading_frequency', label: t('tradingFrequency'), desc: t('tradingFrequencyDesc') },
|
{ key: 'trading_frequency', label: ts(promptSectionsI18n.tradingFrequency, language), desc: ts(promptSectionsI18n.tradingFrequencyDesc, language) },
|
||||||
{ key: 'entry_standards', label: t('entryStandards'), desc: t('entryStandardsDesc') },
|
{ key: 'entry_standards', label: ts(promptSectionsI18n.entryStandards, language), desc: ts(promptSectionsI18n.entryStandardsDesc, language) },
|
||||||
{ key: 'decision_process', label: t('decisionProcess'), desc: t('decisionProcessDesc') },
|
{ key: 'decision_process', label: ts(promptSectionsI18n.decisionProcess, language), desc: ts(promptSectionsI18n.decisionProcessDesc, language) },
|
||||||
]
|
]
|
||||||
|
|
||||||
const currentConfig = config || {}
|
const currentConfig = config || {}
|
||||||
@@ -107,10 +90,10 @@ export function PromptSectionsEditor({
|
|||||||
<FileText className="w-5 h-5 mt-0.5" style={{ color: '#a855f7' }} />
|
<FileText className="w-5 h-5 mt-0.5" style={{ color: '#a855f7' }} />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('promptSections')}
|
{ts(promptSectionsI18n.promptSections, language)}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||||
{t('promptSectionsDesc')}
|
{ts(promptSectionsI18n.promptSectionsDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,12 +129,12 @@ export function PromptSectionsEditor({
|
|||||||
className="px-1.5 py-0.5 text-[10px] rounded"
|
className="px-1.5 py-0.5 text-[10px] rounded"
|
||||||
style={{ background: 'rgba(168, 85, 247, 0.15)', color: '#a855f7' }}
|
style={{ background: 'rgba(168, 85, 247, 0.15)', color: '#a855f7' }}
|
||||||
>
|
>
|
||||||
{language === 'zh' ? '已修改' : 'Modified'}
|
{ts(promptSectionsI18n.modified, language)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>
|
<span className="text-[10px]" style={{ color: '#848E9C' }}>
|
||||||
{value.length} {t('chars')}
|
{value.length} {ts(promptSectionsI18n.chars, language)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -181,7 +164,7 @@ export function PromptSectionsEditor({
|
|||||||
style={{ color: '#848E9C' }}
|
style={{ color: '#848E9C' }}
|
||||||
>
|
>
|
||||||
<RotateCcw className="w-3 h-3" />
|
<RotateCcw className="w-3 h-3" />
|
||||||
{t('resetToDefault')}
|
{ts(promptSectionsI18n.resetToDefault, language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Globe, Lock, Eye, EyeOff } from 'lucide-react'
|
import { Globe, Lock, Eye, EyeOff } from 'lucide-react'
|
||||||
|
import { publishSettings, ts } from '../../i18n/strategy-translations'
|
||||||
|
|
||||||
interface PublishSettingsEditorProps {
|
interface PublishSettingsEditorProps {
|
||||||
isPublic: boolean
|
isPublic: boolean
|
||||||
@@ -17,23 +18,9 @@ export function PublishSettingsEditor({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
language,
|
language,
|
||||||
}: PublishSettingsEditorProps) {
|
}: PublishSettingsEditorProps) {
|
||||||
const t = (key: string) => {
|
|
||||||
const translations: Record<string, Record<string, string>> = {
|
|
||||||
publishToMarket: { zh: '发布到策略市场', en: 'Publish to Market' },
|
|
||||||
publishDesc: { zh: '策略将在市场公开展示,其他用户可发现并使用', en: 'Strategy will be publicly visible in the marketplace' },
|
|
||||||
showConfig: { zh: '公开配置参数', en: 'Show Config' },
|
|
||||||
showConfigDesc: { zh: '允许他人查看和复制详细配置', en: 'Allow others to view and clone config details' },
|
|
||||||
private: { zh: '私有', en: 'PRIVATE' },
|
|
||||||
public: { zh: '公开', en: 'PUBLIC' },
|
|
||||||
hidden: { zh: '隐藏', en: 'HIDDEN' },
|
|
||||||
visible: { zh: '可见', en: 'VISIBLE' },
|
|
||||||
}
|
|
||||||
return translations[key]?.[language] || key
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* 发布开关 */}
|
{/* Publish toggle */}
|
||||||
<div
|
<div
|
||||||
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
style={{
|
style={{
|
||||||
@@ -73,10 +60,10 @@ export function PublishSettingsEditor({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('publishToMarket')}
|
{ts(publishSettings.publishToMarket, language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
||||||
{t('publishDesc')}
|
{ts(publishSettings.publishDesc, language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +74,7 @@ export function PublishSettingsEditor({
|
|||||||
className="text-[10px] font-mono font-bold tracking-wider"
|
className="text-[10px] font-mono font-bold tracking-wider"
|
||||||
style={{ color: isPublic ? '#0ECB81' : '#848E9C' }}
|
style={{ color: isPublic ? '#0ECB81' : '#848E9C' }}
|
||||||
>
|
>
|
||||||
{isPublic ? t('public') : t('private')}
|
{isPublic ? ts(publishSettings.public, language) : ts(publishSettings.private, language)}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
className="relative w-12 h-6 rounded-full transition-all duration-300"
|
className="relative w-12 h-6 rounded-full transition-all duration-300"
|
||||||
@@ -111,7 +98,7 @@ export function PublishSettingsEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 配置可见性开关 - 仅在公开时显示 */}
|
{/* Config visibility toggle - only shown when public */}
|
||||||
{isPublic && (
|
{isPublic && (
|
||||||
<div
|
<div
|
||||||
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
@@ -152,10 +139,10 @@ export function PublishSettingsEditor({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('showConfig')}
|
{ts(publishSettings.showConfig, language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
||||||
{t('showConfigDesc')}
|
{ts(publishSettings.showConfigDesc, language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,7 +153,7 @@ export function PublishSettingsEditor({
|
|||||||
className="text-[10px] font-mono font-bold tracking-wider"
|
className="text-[10px] font-mono font-bold tracking-wider"
|
||||||
style={{ color: configVisible ? '#a855f7' : '#848E9C' }}
|
style={{ color: configVisible ? '#a855f7' : '#848E9C' }}
|
||||||
>
|
>
|
||||||
{configVisible ? t('visible') : t('hidden')}
|
{configVisible ? ts(publishSettings.visible, language) : ts(publishSettings.hidden, language)}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
className="relative w-12 h-6 rounded-full transition-all duration-300"
|
className="relative w-12 h-6 rounded-full transition-all duration-300"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Shield, AlertTriangle } from 'lucide-react'
|
import { Shield, AlertTriangle } from 'lucide-react'
|
||||||
import type { RiskControlConfig } from '../../types'
|
import type { RiskControlConfig } from '../../types'
|
||||||
|
import { riskControl, ts } from '../../i18n/strategy-translations'
|
||||||
|
|
||||||
interface RiskControlEditorProps {
|
interface RiskControlEditorProps {
|
||||||
config: RiskControlConfig
|
config: RiskControlConfig
|
||||||
@@ -14,38 +15,6 @@ export function RiskControlEditor({
|
|||||||
disabled,
|
disabled,
|
||||||
language,
|
language,
|
||||||
}: RiskControlEditorProps) {
|
}: RiskControlEditorProps) {
|
||||||
const t = (key: string) => {
|
|
||||||
const translations: Record<string, Record<string, string>> = {
|
|
||||||
positionLimits: { zh: '仓位限制', en: 'Position Limits' },
|
|
||||||
maxPositions: { zh: '最大持仓数量', en: 'Max Positions' },
|
|
||||||
maxPositionsDesc: { zh: '同时持有的最大币种数量', en: 'Maximum coins held simultaneously' },
|
|
||||||
// Trading leverage (exchange leverage)
|
|
||||||
tradingLeverage: { zh: '交易杠杆(交易所杠杆)', en: 'Trading Leverage (Exchange)' },
|
|
||||||
btcEthLeverage: { zh: 'BTC/ETH 交易杠杆', en: 'BTC/ETH Trading Leverage' },
|
|
||||||
btcEthLeverageDesc: { zh: '交易所开仓使用的杠杆倍数', en: 'Exchange leverage for opening positions' },
|
|
||||||
altcoinLeverage: { zh: '山寨币交易杠杆', en: 'Altcoin Trading Leverage' },
|
|
||||||
altcoinLeverageDesc: { zh: '交易所开仓使用的杠杆倍数', en: 'Exchange leverage for opening positions' },
|
|
||||||
// Position value ratio (risk control) - CODE ENFORCED
|
|
||||||
positionValueRatio: { zh: '仓位价值比例(代码强制)', en: 'Position Value Ratio (CODE ENFORCED)' },
|
|
||||||
positionValueRatioDesc: { zh: '单仓位名义价值 / 账户净值,由代码强制执行', en: 'Position notional value / equity, enforced by code' },
|
|
||||||
btcEthPositionValueRatio: { zh: 'BTC/ETH 仓位价值比例', en: 'BTC/ETH Position Value Ratio' },
|
|
||||||
btcEthPositionValueRatioDesc: { zh: '单仓最大名义价值 = 净值 × 此值(代码强制)', en: 'Max position value = equity × this ratio (CODE ENFORCED)' },
|
|
||||||
altcoinPositionValueRatio: { zh: '山寨币仓位价值比例', en: 'Altcoin Position Value Ratio' },
|
|
||||||
altcoinPositionValueRatioDesc: { zh: '单仓最大名义价值 = 净值 × 此值(代码强制)', en: 'Max position value = equity × this ratio (CODE ENFORCED)' },
|
|
||||||
riskParameters: { zh: '风险参数', en: 'Risk Parameters' },
|
|
||||||
minRiskReward: { zh: '最小风险回报比', en: 'Min Risk/Reward Ratio' },
|
|
||||||
minRiskRewardDesc: { zh: '开仓要求的最低盈亏比', en: 'Minimum profit ratio for opening' },
|
|
||||||
maxMarginUsage: { zh: '最大保证金使用率(代码强制)', en: 'Max Margin Usage (CODE ENFORCED)' },
|
|
||||||
maxMarginUsageDesc: { zh: '保证金使用率上限,由代码强制执行', en: 'Maximum margin utilization, enforced by code' },
|
|
||||||
entryRequirements: { zh: '开仓要求', en: 'Entry Requirements' },
|
|
||||||
minPositionSize: { zh: '最小开仓金额', en: 'Min Position Size' },
|
|
||||||
minPositionSizeDesc: { zh: 'USDT 最小名义价值', en: 'Minimum notional value in USDT' },
|
|
||||||
minConfidence: { zh: '最小信心度', en: 'Min Confidence' },
|
|
||||||
minConfidenceDesc: { zh: 'AI 开仓信心度阈值', en: 'AI confidence threshold for entry' },
|
|
||||||
}
|
|
||||||
return translations[key]?.[language] || key
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateField = <K extends keyof RiskControlConfig>(
|
const updateField = <K extends keyof RiskControlConfig>(
|
||||||
key: K,
|
key: K,
|
||||||
value: RiskControlConfig[K]
|
value: RiskControlConfig[K]
|
||||||
@@ -62,7 +31,7 @@ export function RiskControlEditor({
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('positionLimits')}
|
{ts(riskControl.positionLimits, language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,10 +41,10 @@ export function RiskControlEditor({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('maxPositions')}
|
{ts(riskControl.maxPositions, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('maxPositionsDesc')}
|
{ts(riskControl.maxPositionsDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -99,7 +68,7 @@ export function RiskControlEditor({
|
|||||||
{/* Trading Leverage (Exchange) */}
|
{/* Trading Leverage (Exchange) */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
|
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
|
||||||
{t('tradingLeverage')}
|
{ts(riskControl.tradingLeverage, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
@@ -108,10 +77,10 @@ export function RiskControlEditor({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('btcEthLeverage')}
|
{ts(riskControl.btcEthLeverage, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('btcEthLeverageDesc')}
|
{ts(riskControl.btcEthLeverageDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -139,10 +108,10 @@ export function RiskControlEditor({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('altcoinLeverage')}
|
{ts(riskControl.altcoinLeverage, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('altcoinLeverageDesc')}
|
{ts(riskControl.altcoinLeverageDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -169,10 +138,10 @@ export function RiskControlEditor({
|
|||||||
{/* Position Value Ratio (Risk Control - CODE ENFORCED) */}
|
{/* Position Value Ratio (Risk Control - CODE ENFORCED) */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<p className="text-xs font-medium" style={{ color: '#0ECB81' }}>
|
<p className="text-xs font-medium" style={{ color: '#0ECB81' }}>
|
||||||
{t('positionValueRatio')}
|
{ts(riskControl.positionValueRatio, language)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||||
{t('positionValueRatioDesc')}
|
{ts(riskControl.positionValueRatioDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@@ -181,10 +150,10 @@ export function RiskControlEditor({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('btcEthPositionValueRatio')}
|
{ts(riskControl.btcEthPositionValueRatio, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('btcEthPositionValueRatioDesc')}
|
{ts(riskControl.btcEthPositionValueRatioDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -213,10 +182,10 @@ export function RiskControlEditor({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('altcoinPositionValueRatio')}
|
{ts(riskControl.altcoinPositionValueRatio, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('altcoinPositionValueRatioDesc')}
|
{ts(riskControl.altcoinPositionValueRatioDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -247,7 +216,7 @@ export function RiskControlEditor({
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<AlertTriangle className="w-5 h-5" style={{ color: '#F6465D' }} />
|
<AlertTriangle className="w-5 h-5" style={{ color: '#F6465D' }} />
|
||||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('riskParameters')}
|
{ts(riskControl.riskParameters, language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -257,10 +226,10 @@ export function RiskControlEditor({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('minRiskReward')}
|
{ts(riskControl.minRiskReward, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('minRiskRewardDesc')}
|
{ts(riskControl.minRiskRewardDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span style={{ color: '#848E9C' }}>1:</span>
|
<span style={{ color: '#848E9C' }}>1:</span>
|
||||||
@@ -289,10 +258,10 @@ export function RiskControlEditor({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('maxMarginUsage')}
|
{ts(riskControl.maxMarginUsage, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('maxMarginUsageDesc')}
|
{ts(riskControl.maxMarginUsageDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -319,7 +288,7 @@ export function RiskControlEditor({
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Shield className="w-5 h-5" style={{ color: '#0ECB81' }} />
|
<Shield className="w-5 h-5" style={{ color: '#0ECB81' }} />
|
||||||
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{t('entryRequirements')}
|
{ts(riskControl.entryRequirements, language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -329,10 +298,10 @@ export function RiskControlEditor({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('minPositionSize')}
|
{ts(riskControl.minPositionSize, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('minPositionSizeDesc')}
|
{ts(riskControl.minPositionSizeDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
@@ -362,10 +331,10 @@ export function RiskControlEditor({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||||
{t('minConfidence')}
|
{ts(riskControl.minConfidence, language)}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{t('minConfidenceDesc')}
|
{ts(riskControl.minConfidenceDesc, language)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -212,9 +212,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await toast.promise(api.createTrader(data), {
|
await toast.promise(api.createTrader(data), {
|
||||||
loading: '正在创建…',
|
loading: t('aiTradersToast.creating', language),
|
||||||
success: '创建成功',
|
success: t('aiTradersToast.created', language),
|
||||||
error: '创建失败',
|
error: t('aiTradersToast.createFailed', language),
|
||||||
})
|
})
|
||||||
setShowCreateModal(false)
|
setShowCreateModal(false)
|
||||||
await mutateTraders()
|
await mutateTraders()
|
||||||
@@ -269,9 +269,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
console.log('🔥 handleSaveEditTrader - request:', request)
|
console.log('🔥 handleSaveEditTrader - request:', request)
|
||||||
|
|
||||||
await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
|
await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
|
||||||
loading: '正在保存…',
|
loading: t('aiTradersToast.saving', language),
|
||||||
success: '保存成功',
|
success: t('aiTradersToast.saved', language),
|
||||||
error: '保存失败',
|
error: t('aiTradersToast.saveFailed', language),
|
||||||
})
|
})
|
||||||
setShowEditModal(false)
|
setShowEditModal(false)
|
||||||
setEditingTrader(null)
|
setEditingTrader(null)
|
||||||
@@ -290,9 +290,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await toast.promise(api.deleteTrader(traderId), {
|
await toast.promise(api.deleteTrader(traderId), {
|
||||||
loading: '正在删除…',
|
loading: t('aiTradersToast.deleting', language),
|
||||||
success: '删除成功',
|
success: t('aiTradersToast.deleted', language),
|
||||||
error: '删除失败',
|
error: t('aiTradersToast.deleteFailed', language),
|
||||||
})
|
})
|
||||||
|
|
||||||
await mutateTraders()
|
await mutateTraders()
|
||||||
@@ -306,15 +306,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
try {
|
try {
|
||||||
if (running) {
|
if (running) {
|
||||||
await toast.promise(api.stopTrader(traderId), {
|
await toast.promise(api.stopTrader(traderId), {
|
||||||
loading: '正在停止…',
|
loading: t('aiTradersToast.stopping', language),
|
||||||
success: '已停止',
|
success: t('aiTradersToast.stopped', language),
|
||||||
error: '停止失败',
|
error: t('aiTradersToast.stopFailed', language),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await toast.promise(api.startTrader(traderId), {
|
await toast.promise(api.startTrader(traderId), {
|
||||||
loading: '正在启动…',
|
loading: t('aiTradersToast.starting', language),
|
||||||
success: '已启动',
|
success: t('aiTradersToast.started', language),
|
||||||
error: '启动失败',
|
error: t('aiTradersToast.startFailed', language),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,9 +329,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
try {
|
try {
|
||||||
const newValue = !currentShowInCompetition
|
const newValue = !currentShowInCompetition
|
||||||
await toast.promise(api.toggleCompetition(traderId, newValue), {
|
await toast.promise(api.toggleCompetition(traderId, newValue), {
|
||||||
loading: '正在更新…',
|
loading: t('aiTradersToast.updating', language),
|
||||||
success: newValue ? '已在竞技场显示' : '已在竞技场隐藏',
|
success: newValue ? t('aiTradersToast.showInCompetition', language) : t('aiTradersToast.hideInCompetition', language),
|
||||||
error: '更新失败',
|
error: t('aiTradersToast.updateFailed', language),
|
||||||
})
|
})
|
||||||
|
|
||||||
await mutateTraders()
|
await mutateTraders()
|
||||||
@@ -393,9 +393,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
|
|
||||||
const request = config.buildRequest(updatedItems)
|
const request = config.buildRequest(updatedItems)
|
||||||
await toast.promise(config.updateApi(request), {
|
await toast.promise(config.updateApi(request), {
|
||||||
loading: '正在更新配置…',
|
loading: t('aiTradersToast.updatingConfig', language),
|
||||||
success: '配置已更新',
|
success: t('aiTradersToast.configUpdated', language),
|
||||||
error: '更新配置失败',
|
error: t('aiTradersToast.configUpdateFailed', language),
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshedItems = await config.refreshApi()
|
const refreshedItems = await config.refreshApi()
|
||||||
@@ -506,9 +506,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await toast.promise(api.updateModelConfigs(request), {
|
await toast.promise(api.updateModelConfigs(request), {
|
||||||
loading: '正在更新模型配置…',
|
loading: t('aiTradersToast.updatingModelConfig', language),
|
||||||
success: '模型配置已更新',
|
success: t('aiTradersToast.modelConfigUpdated', language),
|
||||||
error: '更新模型配置失败',
|
error: t('aiTradersToast.modelConfigUpdateFailed', language),
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshedModels = await api.getModelConfigs()
|
const refreshedModels = await api.getModelConfigs()
|
||||||
@@ -536,9 +536,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await toast.promise(api.deleteExchange(exchangeId), {
|
await toast.promise(api.deleteExchange(exchangeId), {
|
||||||
loading: language === 'zh' ? '正在删除交易所账户…' : 'Deleting exchange account...',
|
loading: t('aiTradersToast.deletingExchange', language),
|
||||||
success: language === 'zh' ? '交易所账户已删除' : 'Exchange account deleted',
|
success: t('aiTradersToast.exchangeDeleted', language),
|
||||||
error: language === 'zh' ? '删除交易所账户失败' : 'Failed to delete exchange account',
|
error: t('aiTradersToast.exchangeDeleteFailed', language),
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshedExchanges = await api.getExchangeConfigs()
|
const refreshedExchanges = await api.getExchangeConfigs()
|
||||||
@@ -598,9 +598,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await toast.promise(api.updateExchangeConfigsEncrypted(request), {
|
await toast.promise(api.updateExchangeConfigsEncrypted(request), {
|
||||||
loading: language === 'zh' ? '正在更新交易所配置…' : 'Updating exchange config...',
|
loading: t('aiTradersToast.updatingExchangeConfig', language),
|
||||||
success: language === 'zh' ? '交易所配置已更新' : 'Exchange config updated',
|
success: t('aiTradersToast.exchangeConfigUpdated', language),
|
||||||
error: language === 'zh' ? '更新交易所配置失败' : 'Failed to update exchange config',
|
error: t('aiTradersToast.exchangeConfigUpdateFailed', language),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const createRequest = {
|
const createRequest = {
|
||||||
@@ -622,9 +622,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await toast.promise(api.createExchangeEncrypted(createRequest), {
|
await toast.promise(api.createExchangeEncrypted(createRequest), {
|
||||||
loading: language === 'zh' ? '正在创建交易所账户…' : 'Creating exchange account...',
|
loading: t('aiTradersToast.creatingExchange', language),
|
||||||
success: language === 'zh' ? '交易所账户已创建' : 'Exchange account created',
|
success: t('aiTradersToast.exchangeCreated', language),
|
||||||
error: language === 'zh' ? '创建交易所账户失败' : 'Failed to create exchange account',
|
error: t('aiTradersToast.exchangeCreateFailed', language),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export function ExchangeConfigModal({
|
|||||||
toast.success(t('ipCopied', language))
|
toast.success(t('ipCopied', language))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('copyIPFailed', language) || `复制失败: ${ip}`)
|
toast.error(t('copyIPFailed', language))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ export function ExchangeConfigModal({
|
|||||||
|
|
||||||
const trimmedAccountName = accountName.trim()
|
const trimmedAccountName = accountName.trim()
|
||||||
if (!trimmedAccountName) {
|
if (!trimmedAccountName) {
|
||||||
toast.error(language === 'zh' ? '请输入账户名称' : 'Please enter account name')
|
toast.error(t('exchangeConfig.pleaseEnterAccountName', language))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@ export function ExchangeConfigModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepLabels = language === 'zh' ? ['选择交易所', '配置账户'] : ['Select Exchange', 'Configure']
|
const stepLabels = [t('exchangeConfig.selectExchange', language), t('exchangeConfig.configure', language)]
|
||||||
const cexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'cex')
|
const cexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'cex')
|
||||||
const dexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'dex')
|
const dexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'dex')
|
||||||
|
|
||||||
@@ -412,13 +412,13 @@ export function ExchangeConfigModal({
|
|||||||
{/* Exchange Grid */}
|
{/* Exchange Grid */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
{language === 'zh' ? '选择您的交易所' : 'Choose Your Exchange'}
|
{t('exchangeConfig.chooseExchange', language)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CEX */}
|
{/* CEX */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#F0B90B' }}>
|
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#F0B90B' }}>
|
||||||
{language === 'zh' ? '中心化交易所 (CEX)' : 'Centralized Exchanges'}
|
{t('exchangeConfig.centralizedExchanges', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
|
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
|
||||||
{cexExchanges.map((template) => (
|
{cexExchanges.map((template) => (
|
||||||
@@ -436,7 +436,7 @@ export function ExchangeConfigModal({
|
|||||||
{/* DEX */}
|
{/* DEX */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#A78BFA' }}>
|
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: '#A78BFA' }}>
|
||||||
{language === 'zh' ? '去中心化交易所 (DEX)' : 'Decentralized Exchanges'}
|
{t('exchangeConfig.decentralizedExchanges', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
|
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3">
|
||||||
{dexExchanges.map((template) => (
|
{dexExchanges.map((template) => (
|
||||||
@@ -477,11 +477,11 @@ export function ExchangeConfigModal({
|
|||||||
>
|
>
|
||||||
<UserPlus className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
<UserPlus className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-sm font-medium" style={{ color: '#F0B90B' }}>
|
<span className="text-sm font-medium" style={{ color: '#F0B90B' }}>
|
||||||
{language === 'zh' ? '注册' : 'Register'}
|
{t('exchangeConfig.register', language)}
|
||||||
</span>
|
</span>
|
||||||
{exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && (
|
{exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && (
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(14, 203, 129, 0.2)', color: '#0ECB81' }}>
|
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(14, 203, 129, 0.2)', color: '#0ECB81' }}>
|
||||||
{language === 'zh' ? '优惠' : 'Bonus'}
|
{t('exchangeConfig.bonus', language)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
@@ -491,13 +491,13 @@ export function ExchangeConfigModal({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
<Key className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
<Key className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
{language === 'zh' ? '账户名称' : 'Account Name'} *
|
{t('exchangeConfig.accountName', language)} *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={accountName}
|
value={accountName}
|
||||||
onChange={(e) => setAccountName(e.target.value)}
|
onChange={(e) => setAccountName(e.target.value)}
|
||||||
placeholder={language === 'zh' ? '例如:主账户、套利账户' : 'e.g., Main Account'}
|
placeholder={t('exchangeConfig.accountNamePlaceholder', language)}
|
||||||
className="w-full px-4 py-3 rounded-xl text-base"
|
className="w-full px-4 py-3 rounded-xl text-base"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||||
required
|
required
|
||||||
@@ -517,7 +517,7 @@ export function ExchangeConfigModal({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span style={{ color: '#58a6ff' }}>ℹ️</span>
|
<span style={{ color: '#58a6ff' }}>ℹ️</span>
|
||||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||||
{language === 'zh' ? '币安用户必读:使用「现货与合约交易」API' : 'Use "Spot & Futures Trading" API'}
|
{t('exchangeConfig.useBinanceFuturesApi', language)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ color: '#8b949e' }}>{showBinanceGuide ? '▲' : '▼'}</span>
|
<span style={{ color: '#8b949e' }}>{showBinanceGuide ? '▲' : '▼'}</span>
|
||||||
@@ -532,7 +532,7 @@ export function ExchangeConfigModal({
|
|||||||
style={{ color: '#58a6ff' }}
|
style={{ color: '#58a6ff' }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{language === 'zh' ? '查看官方教程' : 'View Tutorial'} <ExternalLink className="w-3 h-3" />
|
{t('exchangeConfig.viewTutorial', language)} <ExternalLink className="w-3 h-3" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -696,10 +696,10 @@ export function ExchangeConfigModal({
|
|||||||
<span style={{ fontSize: '16px' }}>🔐</span>
|
<span style={{ fontSize: '16px' }}>🔐</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold mb-1" style={{ color: '#3B82F6' }}>
|
<div className="text-sm font-semibold mb-1" style={{ color: '#3B82F6' }}>
|
||||||
{language === 'zh' ? 'Lighter API Key 配置' : 'Lighter API Key Setup'}
|
{t('exchangeConfig.lighterApiKeySetup', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '请在 Lighter 网站生成 API Key' : 'Generate an API Key on Lighter website'}
|
{t('exchangeConfig.lighterApiKeyDesc', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -717,8 +717,8 @@ export function ExchangeConfigModal({
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
{language === 'zh' ? 'API Key 索引' : 'API Key Index'}
|
{t('exchangeConfig.apiKeyIndex', language)}
|
||||||
<Tooltip content={language === 'zh' ? 'API Key 索引从0开始' : 'API Key index starts from 0'}>
|
<Tooltip content={t('exchangeConfig.apiKeyIndexTooltip', language)}>
|
||||||
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#3B82F6' }} />
|
<HelpCircle className="w-4 h-4 cursor-help" style={{ color: '#3B82F6' }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
@@ -730,7 +730,7 @@ export function ExchangeConfigModal({
|
|||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
<button type="button" onClick={handleBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
<button type="button" onClick={handleBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||||
{editingExchangeId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
|
{editingExchangeId ? t('cancel', language) : t('exchangeConfig.back', language)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -738,7 +738,7 @@ export function ExchangeConfigModal({
|
|||||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
style={{ background: '#F0B90B', color: '#000' }}
|
style={{ background: '#F0B90B', color: '#000' }}
|
||||||
>
|
>
|
||||||
{isSaving ? (t('saving', language) || '保存中...') : (
|
{isSaving ? t('saving', language) : (
|
||||||
<>{t('saveConfig', language)} <ArrowRight className="w-4 h-4" /></>
|
<>{t('saveConfig', language)} <ArrowRight className="w-4 h-4" /></>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function ModelConfigModal({
|
|||||||
|
|
||||||
const availableModels = allModels || []
|
const availableModels = allModels || []
|
||||||
const configuredIds = new Set(configuredModels?.map(m => m.id) || [])
|
const configuredIds = new Set(configuredModels?.map(m => m.id) || [])
|
||||||
const stepLabels = language === 'zh' ? ['选择模型', '配置 API'] : ['Select Model', 'Configure API']
|
const stepLabels = [t('modelConfig.selectModel', language), t('modelConfig.configureApi', language)]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
|
||||||
@@ -192,7 +192,7 @@ function ModelSelectionStep({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
{language === 'zh' ? '选择 AI 模型提供商' : 'Choose Your AI Provider'}
|
{t('modelConfig.chooseProvider', language)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Claw402 Featured Card */}
|
{/* Claw402 Featured Card */}
|
||||||
@@ -217,9 +217,7 @@ function ModelSelectionStep({
|
|||||||
<a href="https://claw402.ai" target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="ml-1.5 text-[10px] font-normal px-1.5 py-0.5 rounded" style={{ color: '#60A5FA', background: 'rgba(96, 165, 250, 0.1)' }}>↗ claw402.ai</a>
|
<a href="https://claw402.ai" target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="ml-1.5 text-[10px] font-normal px-1.5 py-0.5 rounded" style={{ color: '#60A5FA', background: 'rgba(96, 165, 250, 0.1)' }}>↗ claw402.ai</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs mt-0.5" style={{ color: '#A0AEC0' }}>
|
<div className="text-xs mt-0.5" style={{ color: '#A0AEC0' }}>
|
||||||
{language === 'zh'
|
{t('modelConfig.payPerCall', language)}
|
||||||
? 'USDC 按次付费 · 支持全部 AI 模型 · 无需 API Key'
|
|
||||||
: 'Pay-per-call USDC · All AI Models · No API Key'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,7 +226,7 @@ function ModelSelectionStep({
|
|||||||
<div className="w-2 h-2 rounded-full" style={{ background: '#00E096' }} />
|
<div className="w-2 h-2 rounded-full" style={{ background: '#00E096' }} />
|
||||||
)}
|
)}
|
||||||
<div className="px-3 py-1.5 rounded-full text-xs font-bold" style={{ background: 'linear-gradient(135deg, #2563EB, #7C3AED)', color: '#fff' }}>
|
<div className="px-3 py-1.5 rounded-full text-xs font-bold" style={{ background: 'linear-gradient(135deg, #2563EB, #7C3AED)', color: '#fff' }}>
|
||||||
{language === 'zh' ? '🔥 推荐' : '🔥 Best'}
|
{'🔥 ' + t('modelConfig.recommended', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,7 +254,7 @@ function ModelSelectionStep({
|
|||||||
<div className="flex items-center gap-3 pt-2">
|
<div className="flex items-center gap-3 pt-2">
|
||||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
||||||
<span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}>
|
<span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '通过钱包支付' : 'Via BlockRun Wallet'}
|
{t('modelConfig.viaBlockrunWallet', language)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +272,7 @@ function ModelSelectionStep({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}>
|
<div className="text-xs text-center pt-2" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '带金色标记的模型已配置' : 'Models with gold badge are already configured'}
|
{t('modelConfig.modelsConfigured', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -310,9 +308,7 @@ function Claw402ConfigForm({
|
|||||||
Claw402 <span className="text-xs font-normal" style={{ color: '#60A5FA' }}>↗</span>
|
Claw402 <span className="text-xs font-normal" style={{ color: '#60A5FA' }}>↗</span>
|
||||||
</a>
|
</a>
|
||||||
<div className="text-sm mt-1" style={{ color: '#A0AEC0' }}>
|
<div className="text-sm mt-1" style={{ color: '#A0AEC0' }}>
|
||||||
{language === 'zh'
|
{t('modelConfig.allModelsClaw', language)}
|
||||||
? '用 USDC 按次付费,支持所有主流 AI 模型'
|
|
||||||
: 'Pay-per-call with USDC — supports all major AI models'}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-3 mt-3 flex-wrap">
|
<div className="flex items-center justify-center gap-3 mt-3 flex-wrap">
|
||||||
{['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => (
|
{['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(name => (
|
||||||
@@ -327,12 +323,10 @@ function Claw402ConfigForm({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
<Brain className="w-4 h-4" style={{ color: '#2563EB' }} />
|
<Brain className="w-4 h-4" style={{ color: '#2563EB' }} />
|
||||||
{language === 'zh' ? '① 选择 AI 模型' : '① Choose AI Model'}
|
{t('modelConfig.selectAiModel', language)}
|
||||||
</label>
|
</label>
|
||||||
<div className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<div className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh'
|
{t('modelConfig.allModelsUnified', language)}
|
||||||
? '所有模型通过 Claw402 统一调用,创建后可随时切换'
|
|
||||||
: 'All models unified via Claw402. Switch anytime after setup.'}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
{CLAW402_MODELS.map((m) => {
|
{CLAW402_MODELS.map((m) => {
|
||||||
@@ -372,34 +366,28 @@ function Claw402ConfigForm({
|
|||||||
<svg className="w-4 h-4" style={{ color: '#2563EB' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" style={{ color: '#2563EB' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||||
</svg>
|
</svg>
|
||||||
{language === 'zh' ? '② 设置钱包' : '② Setup Wallet'}
|
{t('modelConfig.setupWallet', language)}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="p-3 rounded-xl" style={{ background: 'rgba(37, 99, 235, 0.06)', border: '1px solid rgba(37, 99, 235, 0.15)' }}>
|
<div className="p-3 rounded-xl" style={{ background: 'rgba(37, 99, 235, 0.06)', border: '1px solid rgba(37, 99, 235, 0.15)' }}>
|
||||||
<div className="text-xs mb-2" style={{ color: '#A0AEC0' }}>
|
<div className="text-xs mb-2" style={{ color: '#A0AEC0' }}>
|
||||||
{language === 'zh'
|
{t('modelConfig.walletInfo', language)}
|
||||||
? '💡 Claw402 使用 Base 链上的 USDC 付费,你需要一个 EVM 钱包'
|
|
||||||
: '💡 Claw402 uses USDC on Base chain. You need an EVM wallet.'}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span style={{ color: '#00E096' }}>•</span>
|
<span style={{ color: '#00E096' }}>•</span>
|
||||||
{language === 'zh'
|
{t('modelConfig.exportKey', language)}
|
||||||
? '可以用 MetaMask、Rabby 等钱包导出私钥'
|
|
||||||
: 'Export private key from MetaMask, Rabby, etc.'}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span style={{ color: '#00E096' }}>•</span>
|
<span style={{ color: '#00E096' }}>•</span>
|
||||||
{language === 'zh'
|
{t('modelConfig.dedicatedWallet', language)}
|
||||||
? '建议新建一个专用钱包,充入少量 USDC 即可'
|
|
||||||
: 'Recommended: create a dedicated wallet with a small USDC balance'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="text-xs font-medium" style={{ color: '#A0AEC0' }}>
|
<div className="text-xs font-medium" style={{ color: '#A0AEC0' }}>
|
||||||
{language === 'zh' ? '钱包私钥(Base 链 EVM)' : 'Wallet Private Key (Base Chain EVM)'}
|
{t('modelConfig.walletPrivateKey', language)}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -413,9 +401,7 @@ function Claw402ConfigForm({
|
|||||||
<div className="flex items-start gap-1.5 text-[11px]" style={{ color: '#848E9C' }}>
|
<div className="flex items-start gap-1.5 text-[11px]" style={{ color: '#848E9C' }}>
|
||||||
<span className="mt-px">🔒</span>
|
<span className="mt-px">🔒</span>
|
||||||
<span>
|
<span>
|
||||||
{language === 'zh'
|
{t('modelConfig.privateKeyNote', language)}
|
||||||
? '私钥仅在本地签名使用,不会上传或发送交易。无需 ETH,无 Gas 费用。'
|
|
||||||
: 'Private key is only used locally for signing. Never uploaded. No ETH or gas needed.'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -424,20 +410,20 @@ function Claw402ConfigForm({
|
|||||||
{/* USDC Recharge Guide */}
|
{/* USDC Recharge Guide */}
|
||||||
<div className="p-4 rounded-xl" style={{ background: 'rgba(0, 224, 150, 0.05)', border: '1px solid rgba(0, 224, 150, 0.15)' }}>
|
<div className="p-4 rounded-xl" style={{ background: 'rgba(0, 224, 150, 0.05)', border: '1px solid rgba(0, 224, 150, 0.15)' }}>
|
||||||
<div className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: '#00E096' }}>
|
<div className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: '#00E096' }}>
|
||||||
💰 {language === 'zh' ? '如何充值 USDC' : 'How to Fund USDC'}
|
{'💰 ' + t('modelConfig.howToFundUsdc', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs space-y-1.5" style={{ color: '#848E9C' }}>
|
<div className="text-xs space-y-1.5" style={{ color: '#848E9C' }}>
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<span className="font-bold" style={{ color: '#A0AEC0' }}>1.</span>
|
<span className="font-bold" style={{ color: '#A0AEC0' }}>1.</span>
|
||||||
<span>{language === 'zh' ? '从交易所(Binance / OKX / Coinbase)提 USDC 到你的钱包地址' : 'Withdraw USDC from exchange (Binance/OKX/Coinbase) to your wallet'}</span>
|
<span>{t('modelConfig.fundStep1', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<span className="font-bold" style={{ color: '#A0AEC0' }}>2.</span>
|
<span className="font-bold" style={{ color: '#A0AEC0' }}>2.</span>
|
||||||
<span>{language === 'zh' ? '选择 Base 网络(手续费极低)' : 'Select Base network (very low fees)'}</span>
|
<span>{t('modelConfig.fundStep2', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<span className="font-bold" style={{ color: '#A0AEC0' }}>3.</span>
|
<span className="font-bold" style={{ color: '#A0AEC0' }}>3.</span>
|
||||||
<span>{language === 'zh' ? '充入 $5-10 USDC 即可使用很长时间(约 $0.003/次调用)' : '$5-10 USDC lasts a long time (~$0.003/call)'}</span>
|
<span>{t('modelConfig.fundStep3', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,7 +431,7 @@ function Claw402ConfigForm({
|
|||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className="flex gap-3 pt-2">
|
<div className="flex gap-3 pt-2">
|
||||||
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||||
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
|
{editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -453,7 +439,7 @@ function Claw402ConfigForm({
|
|||||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
style={{ background: apiKey.trim() ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
|
style={{ background: apiKey.trim() ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
|
||||||
>
|
>
|
||||||
{language === 'zh' ? '🚀 开始交易' : '🚀 Start Trading'}
|
{'🚀 ' + t('modelConfig.startTrading', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -513,8 +499,8 @@ function StandardProviderConfigForm({
|
|||||||
<ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} />
|
<ExternalLink className="w-4 h-4" style={{ color: '#A78BFA' }} />
|
||||||
<span className="text-sm font-medium" style={{ color: '#A78BFA' }}>
|
<span className="text-sm font-medium" style={{ color: '#A78BFA' }}>
|
||||||
{selectedModel.provider?.startsWith('blockrun')
|
{selectedModel.provider?.startsWith('blockrun')
|
||||||
? (language === 'zh' ? '开始使用' : 'Get Started')
|
? t('modelConfig.getStarted', language)
|
||||||
: (language === 'zh' ? '获取 API Key' : 'Get API Key')}
|
: t('modelConfig.getApiKey', language)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -539,7 +525,7 @@ function StandardProviderConfigForm({
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||||
</svg>
|
</svg>
|
||||||
{selectedModel.provider?.startsWith('blockrun')
|
{selectedModel.provider?.startsWith('blockrun')
|
||||||
? (language === 'zh' ? '钱包私钥 *' : 'Wallet Private Key *')
|
? t('modelConfig.walletPrivateKeyLabel', language)
|
||||||
: 'API Key *'}
|
: 'API Key *'}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -612,7 +598,7 @@ function StandardProviderConfigForm({
|
|||||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
{language === 'zh' ? '选择模型' : 'Select Model'}
|
{t('modelConfig.selectModelLabel', language)}
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{BLOCKRUN_MODELS.map((m) => {
|
{BLOCKRUN_MODELS.map((m) => {
|
||||||
@@ -655,7 +641,7 @@ function StandardProviderConfigForm({
|
|||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
<button type="button" onClick={onBack} className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||||
{editingModelId ? t('cancel', language) : (language === 'zh' ? '返回' : 'Back')}
|
{editingModelId ? t('cancel', language) : t('modelConfig.back', language)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Check, ChevronLeft, ExternalLink, MessageCircle, Unlink, ArrowRight } f
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { api } from '../../lib/api'
|
import { api } from '../../lib/api'
|
||||||
import type { TelegramConfig, AIModel } from '../../types'
|
import type { TelegramConfig, AIModel } from '../../types'
|
||||||
import type { Language } from '../../i18n/translations'
|
import { t, type Language } from '../../i18n/translations'
|
||||||
|
|
||||||
// Step indicator (reused pattern from ExchangeConfigModal)
|
// Step indicator (reused pattern from ExchangeConfigModal)
|
||||||
function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) {
|
function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) {
|
||||||
@@ -55,8 +55,6 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [isUnbinding, setIsUnbinding] = useState(false)
|
const [isUnbinding, setIsUnbinding] = useState(false)
|
||||||
|
|
||||||
const zh = language === 'zh'
|
|
||||||
|
|
||||||
// Load current config and available models
|
// Load current config and available models
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -84,20 +82,20 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
|
|
||||||
// Basic format validation: looks like "123456789:ABCdef..."
|
// Basic format validation: looks like "123456789:ABCdef..."
|
||||||
if (!/^\d+:[A-Za-z0-9_-]{35,}$/.test(token.trim())) {
|
if (!/^\d+:[A-Za-z0-9_-]{35,}$/.test(token.trim())) {
|
||||||
toast.error(zh ? 'Bot Token 格式不正确,应为 "数字:字母数字串"' : 'Invalid Bot Token format. Expected "numbers:alphanumeric"')
|
toast.error(t('telegram.invalidTokenFormat', language))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSaving(true)
|
setIsSaving(true)
|
||||||
try {
|
try {
|
||||||
await api.updateTelegramConfig(token.trim(), selectedModelId || undefined)
|
await api.updateTelegramConfig(token.trim(), selectedModelId || undefined)
|
||||||
toast.success(zh ? 'Bot Token 已保存,等待绑定' : 'Bot Token saved, waiting for binding')
|
toast.success(t('telegram.tokenSaved', language))
|
||||||
const updated = await api.getTelegramConfig()
|
const updated = await api.getTelegramConfig()
|
||||||
setConfig(updated)
|
setConfig(updated)
|
||||||
setToken('')
|
setToken('')
|
||||||
setStep(1)
|
setStep(1)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(zh ? '保存失败,请检查 Token 是否正确' : 'Save failed, please verify the token')
|
toast.error(t('telegram.saveFailed', language))
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false)
|
setIsSaving(false)
|
||||||
}
|
}
|
||||||
@@ -108,33 +106,31 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
setIsUnbinding(true)
|
setIsUnbinding(true)
|
||||||
try {
|
try {
|
||||||
await api.unbindTelegram()
|
await api.unbindTelegram()
|
||||||
toast.success(zh ? '已解绑 Telegram 账号' : 'Telegram account unbound')
|
toast.success(t('telegram.unbound', language))
|
||||||
const updated = await api.getTelegramConfig()
|
const updated = await api.getTelegramConfig()
|
||||||
setConfig(updated)
|
setConfig(updated)
|
||||||
setStep(updated.token_masked ? 1 : 0)
|
setStep(updated.token_masked ? 1 : 0)
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(zh ? '解绑失败' : 'Unbind failed')
|
toast.error(t('telegram.unbindFailed', language))
|
||||||
} finally {
|
} finally {
|
||||||
setIsUnbinding(false)
|
setIsUnbinding(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepLabels = zh
|
const stepLabels = [t('telegram.createBot', language), t('telegram.bindAccount', language), t('telegram.done', language)]
|
||||||
? ['创建 Bot', '绑定账号', '完成']
|
|
||||||
: ['Create Bot', 'Bind Account', 'Done']
|
|
||||||
|
|
||||||
// Model selector shared between steps
|
// Model selector shared between steps
|
||||||
const ModelSelector = () => (
|
const ModelSelector = () => (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
{zh ? '选择 AI 模型(可选)' : 'Select AI Model (optional)'}
|
{t('telegram.selectAiModel', language)}
|
||||||
</label>
|
</label>
|
||||||
{models.length === 0 ? (
|
{models.length === 0 ? (
|
||||||
<div
|
<div
|
||||||
className="px-4 py-3 rounded-xl text-xs"
|
className="px-4 py-3 rounded-xl text-xs"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#848E9C' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#848E9C' }}
|
||||||
>
|
>
|
||||||
{zh ? '暂无启用的模型,请先在「AI 模型」中配置' : 'No enabled models. Configure one in AI Models first.'}
|
{t('telegram.noEnabledModels', language)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<select
|
<select
|
||||||
@@ -147,7 +143,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
color: selectedModelId ? '#EAECEF' : '#848E9C',
|
color: selectedModelId ? '#EAECEF' : '#848E9C',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{zh ? '— 自动选择(推荐)' : '— Auto-select (recommended)'}</option>
|
<option value="">{t('telegram.autoSelect', language)}</option>
|
||||||
{models.map((m) => (
|
{models.map((m) => (
|
||||||
<option key={m.id} value={m.id}>
|
<option key={m.id} value={m.id}>
|
||||||
{m.name} ({m.provider}{m.customModelName ? ` · ${m.customModelName}` : ''})
|
{m.name} ({m.provider}{m.customModelName ? ` · ${m.customModelName}` : ''})
|
||||||
@@ -156,9 +152,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{zh
|
{t('telegram.autoUseEnabled', language)}
|
||||||
? '不选则自动使用已启用的模型'
|
|
||||||
: 'Leave blank to auto-use any enabled model'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -184,7 +178,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MessageCircle className="w-6 h-6" style={{ color: '#2AABEE' }} />
|
<MessageCircle className="w-6 h-6" style={{ color: '#2AABEE' }} />
|
||||||
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||||
{zh ? 'Telegram Bot 配置' : 'Telegram Bot Setup'}
|
{t('telegram.botSetup', language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +201,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
<div className="px-6 pb-6 space-y-5">
|
<div className="px-6 pb-6 space-y-5">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-8 text-zinc-500 text-sm font-mono">
|
<div className="text-center py-8 text-zinc-500 text-sm font-mono">
|
||||||
{zh ? '加载中...' : 'Loading...'}
|
{t('telegram.loading', language)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -222,13 +216,13 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
<span className="text-2xl">🤖</span>
|
<span className="text-2xl">🤖</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold mb-1" style={{ color: '#2AABEE' }}>
|
<div className="font-semibold mb-1" style={{ color: '#2AABEE' }}>
|
||||||
{zh ? '第一步:在 Telegram 创建你的 Bot' : 'Step 1: Create your Bot in Telegram'}
|
{t('telegram.step1Title', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||||
<div>1. {zh ? '打开 Telegram,搜索' : 'Open Telegram, search for'} <code className="text-blue-400">@BotFather</code></div>
|
<div>1. {t('telegram.step1Desc1', language)} <code className="text-blue-400">@BotFather</code></div>
|
||||||
<div>2. {zh ? '发送' : 'Send'} <code className="text-blue-400">/newbot</code> {zh ? '命令' : 'command'}</div>
|
<div>2. {t('telegram.step1Desc2', language)} <code className="text-blue-400">/newbot</code> {t('telegram.step1Desc2Suffix', language)}</div>
|
||||||
<div>3. {zh ? '按提示输入 Bot 名称和用户名' : 'Follow prompts to set bot name and username'}</div>
|
<div>3. {t('telegram.step1Desc3', language)}</div>
|
||||||
<div>4. {zh ? 'BotFather 会返回一个 Token,复制它' : 'BotFather will return a Token, copy it'}</div>
|
<div>4. {t('telegram.step1Desc4', language)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,12 +236,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
style={{ background: '#2AABEE', color: '#000' }}
|
style={{ background: '#2AABEE', color: '#000' }}
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-4 h-4" />
|
<ExternalLink className="w-4 h-4" />
|
||||||
{zh ? '打开 @BotFather' : 'Open @BotFather'}
|
{t('telegram.openBotFather', language)}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
{zh ? '粘贴 Bot Token' : 'Paste Bot Token'}
|
{t('telegram.pasteToken', language)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -258,7 +252,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||||
/>
|
/>
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{zh ? 'Token 格式:数字:字母数字串,如 123456789:ABCdef...' : 'Format: numbers:alphanumeric, e.g. 123456789:ABCdef...'}
|
{t('telegram.tokenFormat', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -271,8 +265,8 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
style={{ background: '#2AABEE', color: '#000' }}
|
style={{ background: '#2AABEE', color: '#000' }}
|
||||||
>
|
>
|
||||||
{isSaving
|
{isSaving
|
||||||
? (zh ? '保存中...' : 'Saving...')
|
? t('telegram.savingToken', language)
|
||||||
: (<>{zh ? '保存并继续' : 'Save & Continue'} <ArrowRight className="w-4 h-4" /></>)
|
: (<>{t('telegram.saveAndContinue', language)} <ArrowRight className="w-4 h-4" /></>)
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,12 +283,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
<span className="text-2xl">📱</span>
|
<span className="text-2xl">📱</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold mb-1" style={{ color: '#0ECB81' }}>
|
<div className="font-semibold mb-1" style={{ color: '#0ECB81' }}>
|
||||||
{zh ? '第二步:向你的 Bot 发送 /start' : 'Step 2: Send /start to your Bot'}
|
{t('telegram.step2Title', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||||
<div>1. {zh ? '在 Telegram 中搜索你刚创建的 Bot' : 'Search for your newly created Bot in Telegram'}</div>
|
<div>1. {t('telegram.step2Desc1', language)}</div>
|
||||||
<div>2. {zh ? '点击 Start 或发送' : 'Click Start or send'} <code className="text-green-400">/start</code></div>
|
<div>2. {t('telegram.step2Desc2', language)} <code className="text-green-400">/start</code></div>
|
||||||
<div>3. {zh ? 'Bot 会自动绑定到你的账号' : 'Bot will automatically bind to your account'}</div>
|
<div>3. {t('telegram.step2Desc3', language)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,7 +302,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse flex-shrink-0" />
|
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
|
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
|
||||||
{zh ? '当前 Token' : 'Current Token'}
|
{t('telegram.currentToken', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-mono" style={{ color: '#EAECEF' }}>
|
<div className="text-sm font-mono" style={{ color: '#EAECEF' }}>
|
||||||
{config.token_masked}
|
{config.token_masked}
|
||||||
@@ -322,9 +316,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
style={{ background: 'rgba(240, 185, 11, 0.08)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
|
style={{ background: 'rgba(240, 185, 11, 0.08)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
|
||||||
>
|
>
|
||||||
<div className="text-xs" style={{ color: '#F0B90B' }}>
|
<div className="text-xs" style={{ color: '#F0B90B' }}>
|
||||||
{zh
|
{t('telegram.waitingForStart', language)}
|
||||||
? '⏳ 等待你发送 /start... 发送后刷新页面查看状态'
|
|
||||||
: '⏳ Waiting for you to send /start... Refresh page after sending'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -334,7 +326,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5"
|
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5"
|
||||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||||
>
|
>
|
||||||
{zh ? '重新配置 Token' : 'Reconfigure Token'}
|
{t('telegram.reconfigureToken', language)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -343,19 +335,19 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
setConfig(updated)
|
setConfig(updated)
|
||||||
if (updated.is_bound) {
|
if (updated.is_bound) {
|
||||||
setStep(2)
|
setStep(2)
|
||||||
toast.success(zh ? '绑定成功!' : 'Bound successfully!')
|
toast.success(t('telegram.bindSuccess', language))
|
||||||
} else {
|
} else {
|
||||||
toast.info(zh ? '尚未收到 /start,请先向 Bot 发送 /start' : 'No /start received yet. Please send /start to your Bot first')
|
toast.info(t('telegram.noStartReceived', language))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(zh ? '检查失败' : 'Check failed')
|
toast.error(t('telegram.checkFailed', language))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
|
||||||
style={{ background: '#0ECB81', color: '#000' }}
|
style={{ background: '#0ECB81', color: '#000' }}
|
||||||
>
|
>
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-4 h-4" />
|
||||||
{zh ? '检查绑定状态' : 'Check Status'}
|
{t('telegram.checkStatus', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -370,12 +362,10 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
>
|
>
|
||||||
<div className="text-4xl">🎉</div>
|
<div className="text-4xl">🎉</div>
|
||||||
<div className="font-bold text-lg" style={{ color: '#0ECB81' }}>
|
<div className="font-bold text-lg" style={{ color: '#0ECB81' }}>
|
||||||
{zh ? 'Telegram Bot 已绑定!' : 'Telegram Bot is Active!'}
|
{t('telegram.botActive', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{zh
|
{t('telegram.botActiveDesc', language)}
|
||||||
? '你现在可以通过 Telegram 用自然语言控制交易系统'
|
|
||||||
: 'You can now control the trading system via natural language in Telegram'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -387,7 +377,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
|
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
|
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
|
||||||
{zh ? 'Bot Token' : 'Bot Token'}
|
Bot Token
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-mono truncate" style={{ color: '#EAECEF' }}>
|
<div className="text-sm font-mono truncate" style={{ color: '#EAECEF' }}>
|
||||||
{config.token_masked}
|
{config.token_masked}
|
||||||
@@ -398,7 +388,7 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
|
|
||||||
{/* AI Model selector — works on active bot */}
|
{/* AI Model selector — works on active bot */}
|
||||||
<BoundModelSelector
|
<BoundModelSelector
|
||||||
zh={zh}
|
language={language}
|
||||||
models={models}
|
models={models}
|
||||||
currentModelId={config?.model_id ?? ''}
|
currentModelId={config?.model_id ?? ''}
|
||||||
onSaved={(modelId) => {
|
onSaved={(modelId) => {
|
||||||
@@ -412,14 +402,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#848E9C' }}>
|
<div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#848E9C' }}>
|
||||||
{zh ? '支持的命令' : 'Supported Commands'}
|
{t('telegram.supportedCommands', language)}
|
||||||
</div>
|
</div>
|
||||||
{[
|
{[
|
||||||
{ cmd: '/help', desc: zh ? '查看所有命令' : 'Show all commands' },
|
{ cmd: '/help', desc: t('telegram.cmdHelp', language) },
|
||||||
{ cmd: zh ? '查看交易员状态' : 'Show trader status', desc: zh ? '自然语言查询' : 'Natural language' },
|
{ cmd: t('telegram.cmdStatus', language), desc: t('telegram.cmdNaturalLang', language) },
|
||||||
{ cmd: zh ? '启动/停止交易员' : 'Start/stop trader', desc: zh ? '自然语言控制' : 'Natural language control' },
|
{ cmd: t('telegram.cmdStartStop', language), desc: t('telegram.cmdControl', language) },
|
||||||
{ cmd: zh ? '查看持仓' : 'View positions', desc: zh ? '实时持仓查询' : 'Real-time position query' },
|
{ cmd: t('telegram.cmdPositions', language), desc: t('telegram.cmdPositionsDesc', language) },
|
||||||
{ cmd: zh ? '配置策略' : 'Configure strategy', desc: zh ? '修改交易策略' : 'Modify trading strategy' },
|
{ cmd: t('telegram.cmdStrategy', language), desc: t('telegram.cmdStrategyDesc', language) },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} className="flex items-start gap-2 text-xs">
|
<div key={i} className="flex items-start gap-2 text-xs">
|
||||||
<code className="font-mono px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#1E2329', color: '#2AABEE' }}>
|
<code className="font-mono px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#1E2329', color: '#2AABEE' }}>
|
||||||
@@ -438,14 +428,14 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
|
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
|
||||||
>
|
>
|
||||||
<Unlink className="w-4 h-4" />
|
<Unlink className="w-4 h-4" />
|
||||||
{isUnbinding ? (zh ? '解绑中...' : 'Unbinding...') : (zh ? '解绑账号' : 'Unbind Account')}
|
{isUnbinding ? t('telegram.unbinding', language) : t('telegram.unbindAccount', language)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
|
className="flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
|
||||||
style={{ background: '#2AABEE', color: '#000' }}
|
style={{ background: '#2AABEE', color: '#000' }}
|
||||||
>
|
>
|
||||||
{zh ? '完成' : 'Done'}
|
{t('telegram.done', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -461,12 +451,12 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
|
|||||||
// BoundModelSelector — lets the user change the AI model when the bot is already active.
|
// BoundModelSelector — lets the user change the AI model when the bot is already active.
|
||||||
// It updates the model_id without requiring re-entry of the bot token.
|
// It updates the model_id without requiring re-entry of the bot token.
|
||||||
function BoundModelSelector({
|
function BoundModelSelector({
|
||||||
zh,
|
language,
|
||||||
models,
|
models,
|
||||||
currentModelId,
|
currentModelId,
|
||||||
onSaved,
|
onSaved,
|
||||||
}: {
|
}: {
|
||||||
zh: boolean
|
language: Language
|
||||||
models: AIModel[]
|
models: AIModel[]
|
||||||
currentModelId: string
|
currentModelId: string
|
||||||
onSaved: (modelId: string) => void
|
onSaved: (modelId: string) => void
|
||||||
@@ -483,9 +473,9 @@ function BoundModelSelector({
|
|||||||
// POST /api/telegram/model — lightweight endpoint for model-only update
|
// POST /api/telegram/model — lightweight endpoint for model-only update
|
||||||
await api.updateTelegramModel(modelId)
|
await api.updateTelegramModel(modelId)
|
||||||
onSaved(modelId)
|
onSaved(modelId)
|
||||||
toast.success(zh ? 'AI 模型已更新' : 'AI model updated')
|
toast.success(t('telegram.modelUpdated', language))
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(zh ? '更新失败' : 'Update failed')
|
toast.error(t('telegram.modelUpdateFailed', language))
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false)
|
setIsSaving(false)
|
||||||
}
|
}
|
||||||
@@ -496,7 +486,7 @@ function BoundModelSelector({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||||
{zh ? 'AI 模型(用于自然语言解析)' : 'AI Model (for natural language)'}
|
{t('telegram.aiModelLabel', language)}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<select
|
<select
|
||||||
@@ -509,7 +499,7 @@ function BoundModelSelector({
|
|||||||
color: modelId ? '#EAECEF' : '#848E9C',
|
color: modelId ? '#EAECEF' : '#848E9C',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">{zh ? '— 自动选择' : '— Auto-select'}</option>
|
<option value="">{t('telegram.aiModelAutoSelect', language)}</option>
|
||||||
{models.map((m) => (
|
{models.map((m) => (
|
||||||
<option key={m.id} value={m.id}>
|
<option key={m.id} value={m.id}>
|
||||||
{m.name}{m.customModelName ? ` · ${m.customModelName}` : ''}
|
{m.name}{m.customModelName ? ` · ${m.customModelName}` : ''}
|
||||||
@@ -522,7 +512,7 @@ function BoundModelSelector({
|
|||||||
className="px-4 py-2.5 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-40 disabled:cursor-not-allowed"
|
className="px-4 py-2.5 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
style={{ background: '#F0B90B', color: '#000', whiteSpace: 'nowrap' }}
|
style={{ background: '#F0B90B', color: '#000', whiteSpace: 'nowrap' }}
|
||||||
>
|
>
|
||||||
{isSaving ? '...' : (zh ? '保存' : 'Save')}
|
{isSaving ? '...' : t('telegram.save', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { TraderConfigData } from '../../types'
|
import type { TraderConfigData } from '../../types'
|
||||||
|
import { t } from '../../i18n/translations'
|
||||||
|
import { useLanguage } from '../../contexts/LanguageContext'
|
||||||
import { PunkAvatar, getTraderAvatar } from '../common/PunkAvatar'
|
import { PunkAvatar, getTraderAvatar } from '../common/PunkAvatar'
|
||||||
|
|
||||||
// 提取下划线后面的名称部分
|
// Extract the name part after the last underscore
|
||||||
function getShortName(fullName: string): string {
|
function getShortName(fullName: string): string {
|
||||||
const parts = fullName.split('_')
|
const parts = fullName.split('_')
|
||||||
return parts.length > 1 ? parts[parts.length - 1] : fullName
|
return parts.length > 1 ? parts[parts.length - 1] : fullName
|
||||||
@@ -18,6 +20,7 @@ export function TraderConfigViewModal({
|
|||||||
onClose,
|
onClose,
|
||||||
traderData,
|
traderData,
|
||||||
}: TraderConfigViewModalProps) {
|
}: TraderConfigViewModalProps) {
|
||||||
|
const { language } = useLanguage()
|
||||||
if (!isOpen || !traderData) return null
|
if (!isOpen || !traderData) return null
|
||||||
|
|
||||||
const InfoRow = ({
|
const InfoRow = ({
|
||||||
@@ -30,7 +33,7 @@ export function TraderConfigViewModal({
|
|||||||
<div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0">
|
<div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0">
|
||||||
<span className="text-sm text-[#848E9C] font-medium">{label}</span>
|
<span className="text-sm text-[#848E9C] font-medium">{label}</span>
|
||||||
<span className="text-sm text-[#EAECEF] font-mono text-right">
|
<span className="text-sm text-[#EAECEF] font-mono text-right">
|
||||||
{typeof value === 'boolean' ? (value ? '是' : '否') : value}
|
{typeof value === 'boolean' ? (value ? t('traderConfigView.yes', language) : t('traderConfigView.no', language)) : value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -50,9 +53,9 @@ export function TraderConfigViewModal({
|
|||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-[#EAECEF]">交易员配置</h2>
|
<h2 className="text-xl font-bold text-[#EAECEF]">{t('traderConfigView.traderConfig', language)}</h2>
|
||||||
<p className="text-sm text-[#848E9C] mt-1">
|
<p className="text-sm text-[#848E9C] mt-1">
|
||||||
{traderData.trader_name} 的配置信息
|
{t('traderConfigView.configInfo', language, { name: traderData.trader_name })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +70,7 @@ export function TraderConfigViewModal({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>{traderData.is_running ? '●' : '○'}</span>
|
<span>{traderData.is_running ? '●' : '○'}</span>
|
||||||
{traderData.is_running ? '运行中' : '已停止'}
|
{traderData.is_running ? t('traderConfigView.running', language) : t('traderConfigView.stopped', language)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -83,32 +86,32 @@ export function TraderConfigViewModal({
|
|||||||
{/* Basic Info */}
|
{/* Basic Info */}
|
||||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
||||||
🤖 基础信息
|
{'🤖 ' + t('traderConfigView.basicInfo', language)}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="交易员名称"
|
label={t('traderConfigView.traderName', language)}
|
||||||
value={traderData.trader_name}
|
value={traderData.trader_name}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="AI模型"
|
label={t('traderConfigView.aiModel', language)}
|
||||||
value={getShortName(traderData.ai_model).toUpperCase()}
|
value={getShortName(traderData.ai_model).toUpperCase()}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="交易所"
|
label={t('traderConfigView.exchange', language)}
|
||||||
value={getShortName(traderData.exchange_id).toUpperCase()}
|
value={getShortName(traderData.exchange_id).toUpperCase()}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="初始余额"
|
label={t('traderConfigView.initialBalance', language)}
|
||||||
value={`$${traderData.initial_balance.toLocaleString()}`}
|
value={`$${traderData.initial_balance.toLocaleString()}`}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="保证金模式"
|
label={t('traderConfigView.marginMode', language)}
|
||||||
value={traderData.is_cross_margin ? '全仓' : '逐仓'}
|
value={traderData.is_cross_margin ? t('traderConfigView.crossMargin', language) : t('traderConfigView.isolatedMargin', language)}
|
||||||
/>
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="扫描间隔"
|
label={t('traderConfigView.scanIntervalLabel', language)}
|
||||||
value={`${traderData.scan_interval_minutes || 3} 分钟`}
|
value={t('traderConfigView.scanInterval', language, { minutes: traderData.scan_interval_minutes || 3 })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,11 +120,11 @@ export function TraderConfigViewModal({
|
|||||||
{traderData.strategy_id && (
|
{traderData.strategy_id && (
|
||||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
||||||
📋 使用策略
|
{'📋 ' + t('traderConfigView.strategyUsed', language)}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="策略名称"
|
label={t('traderConfigView.strategyName', language)}
|
||||||
value={traderData.strategy_name || traderData.strategy_id}
|
value={traderData.strategy_name || traderData.strategy_id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,7 +138,7 @@ export function TraderConfigViewModal({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
|
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
|
||||||
>
|
>
|
||||||
关闭
|
{t('traderConfigView.close', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// Reset 401 flag on page load to allow fresh 401 handling
|
// Reset 401 flag on page load to allow fresh 401 handling
|
||||||
reset401Flag()
|
reset401Flag()
|
||||||
|
|
||||||
// 先检查是否为管理员模式(使用带缓存的系统配置获取)
|
// Check if admin mode is active (uses cached system config)
|
||||||
getSystemConfig()
|
getSystemConfig()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// 不再在管理员模式下模拟登录;统一检查本地存储
|
// No longer simulate login in admin mode; check local storage uniformly
|
||||||
const savedToken = localStorage.getItem('auth_token')
|
const savedToken = localStorage.getItem('auth_token')
|
||||||
const savedUser = localStorage.getItem('auth_user')
|
const savedUser = localStorage.getItem('auth_user')
|
||||||
if (savedToken && savedUser) {
|
if (savedToken && savedUser) {
|
||||||
@@ -60,7 +60,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Failed to fetch system config:', err)
|
console.error('Failed to fetch system config:', err)
|
||||||
// 发生错误时,继续检查本地存储
|
// On error, continue checking local storage
|
||||||
const savedToken = localStorage.getItem('auth_token')
|
const savedToken = localStorage.getItem('auth_token')
|
||||||
const savedUser = localStorage.getItem('auth_user')
|
const savedUser = localStorage.getItem('auth_user')
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
window.history.pushState({}, '', returnUrl)
|
window.history.pushState({}, '', returnUrl)
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||||
} else {
|
} else {
|
||||||
// 跳转到配置页面
|
// Redirect to config page
|
||||||
window.history.pushState({}, '', '/traders')
|
window.history.pushState({}, '', '/traders')
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unexpected success response
|
// Unexpected success response
|
||||||
return { success: false, message: data.message || '登录响应异常' }
|
return { success: false, message: data.message || 'Unexpected login response' }
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -136,7 +136,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, message: '登录失败,请重试' }
|
return { success: false, message: 'Login failed, please try again' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,16 +168,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
window.history.pushState({}, '', returnUrl)
|
window.history.pushState({}, '', returnUrl)
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||||
} else {
|
} else {
|
||||||
// 跳转到仪表盘
|
// Redirect to dashboard
|
||||||
window.history.pushState({}, '', '/dashboard')
|
window.history.pushState({}, '', '/dashboard')
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||||
}
|
}
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} else {
|
} else {
|
||||||
return { success: false, message: data.error || '登录失败' }
|
return { success: false, message: data.error || 'Login failed' }
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { success: false, message: '登录失败,请重试' }
|
return { success: false, message: 'Login failed, please try again' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
window.history.pushState({}, '', returnUrl)
|
window.history.pushState({}, '', returnUrl)
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||||
} else {
|
} else {
|
||||||
// 跳转到配置页面
|
// Redirect to config page
|
||||||
window.history.pushState({}, '', '/traders')
|
window.history.pushState({}, '', '/traders')
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||||
}
|
}
|
||||||
@@ -269,7 +269,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return { success: false, message: data.error }
|
return { success: false, message: data.error }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, message: '密码重置失败,请重试' }
|
return { success: false, message: 'Password reset failed, please try again' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ export const coinSource = {
|
|||||||
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded', es: 'Estas monedas serán excluidas de todas las fuentes' },
|
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded', es: 'Estas monedas serán excluidas de todas las fuentes' },
|
||||||
addExcludedCoin: { zh: '添加排除', en: 'Add Excluded', es: 'Agregar Excluida' },
|
addExcludedCoin: { zh: '添加排除', en: 'Add Excluded', es: 'Agregar Excluida' },
|
||||||
nofxosNote: { zh: '使用 NofxOS API Key(在指标配置中设置)', en: 'Uses NofxOS API Key (set in Indicators config)', es: 'Usa API Key de NofxOS' },
|
nofxosNote: { zh: '使用 NofxOS API Key(在指标配置中设置)', en: 'Uses NofxOS API Key (set in Indicators config)', es: 'Usa API Key de NofxOS' },
|
||||||
|
ai500Desc: { zh: '使用 AI500 智能筛选的热门币种', en: 'Use AI500 smart-filtered popular coins', es: 'Monedas filtradas por AI500' },
|
||||||
|
oiTopDesc: { zh: '持仓增加榜,适合做多', en: 'OI increase ranking, for long', es: 'Ranking OI creciente, para largo' },
|
||||||
|
oi_lowDesc: { zh: '持仓减少榜,适合做空', en: 'OI decrease ranking, for short', es: 'Ranking OI decreciente, para corto' },
|
||||||
|
mixedDesc: { zh: '组合多种数据源', en: 'Combine multiple sources', es: 'Combinar fuentes múltiples' },
|
||||||
|
oiIncreaseShort: { zh: 'OI增', en: 'OI↑', es: 'OI↑' },
|
||||||
|
oiDecreaseShort: { zh: 'OI减', en: 'OI↓', es: 'OI↓' },
|
||||||
|
custom: { zh: '自定义', en: 'Custom', es: 'Personalizado' },
|
||||||
|
excludedNone: { zh: '无', en: 'None', es: 'Ninguno' },
|
||||||
|
oiIncreaseTitle: { zh: 'OI 持仓增加榜', en: 'OI Increase', es: 'OI Aumento' },
|
||||||
|
oiDecreaseTitle: { zh: 'OI 持仓减少榜', en: 'OI Decrease', es: 'OI Disminución' },
|
||||||
|
oiIncreaseLabel: { zh: 'OI 增加', en: 'OI Increase', es: 'OI Aumento' },
|
||||||
|
forLong: { zh: '适合做多', en: 'For long', es: 'Para largo' },
|
||||||
|
oiDecreaseLabel: { zh: 'OI 减少', en: 'OI Decrease', es: 'OI Disminución' },
|
||||||
|
forShort: { zh: '适合做空', en: 'For short', es: 'Para corto' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -83,6 +97,8 @@ export const gridConfig = {
|
|||||||
modeLong: { zh: '全多:100%买 + 0%卖', en: 'Long: 100% buy + 0% sell', es: 'Largo: 100% compra' },
|
modeLong: { zh: '全多:100%买 + 0%卖', en: 'Long: 100% buy + 0% sell', es: 'Largo: 100% compra' },
|
||||||
modeShortBias: { zh: '偏空:(100-X)%买 + X%卖', en: 'Short Bias: (100-X)% buy + X% sell', es: 'Sesgo Corto: X% venta' },
|
modeShortBias: { zh: '偏空:(100-X)%买 + X%卖', en: 'Short Bias: (100-X)% buy + X% sell', es: 'Sesgo Corto: X% venta' },
|
||||||
modeShort: { zh: '全空:0%买 + 100%卖', en: 'Short: 0% buy + 100% sell', es: 'Corto: 100% venta' },
|
modeShort: { zh: '全空:0%买 + 100%卖', en: 'Short: 0% buy + 100% sell', es: 'Corto: 100% venta' },
|
||||||
|
buy: { zh: '买', en: 'buy', es: 'compra' },
|
||||||
|
sell: { zh: '卖', en: 'sell', es: 'venta' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -172,6 +188,7 @@ export const promptSections = {
|
|||||||
decisionProcessDesc: { zh: '设定决策步骤和思考流程', en: 'Set decision steps and thinking process', es: 'Establecer proceso' },
|
decisionProcessDesc: { zh: '设定决策步骤和思考流程', en: 'Set decision steps and thinking process', es: 'Establecer proceso' },
|
||||||
resetToDefault: { zh: '重置为默认', en: 'Reset to Default', es: 'Restablecer' },
|
resetToDefault: { zh: '重置为默认', en: 'Reset to Default', es: 'Restablecer' },
|
||||||
chars: { zh: '字符', en: 'chars', es: 'caracteres' },
|
chars: { zh: '字符', en: 'chars', es: 'caracteres' },
|
||||||
|
modified: { zh: '已修改', en: 'Modified', es: 'Modificado' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -235,6 +252,7 @@ export const indicator = {
|
|||||||
connected: { zh: '已配置', en: 'Configured', es: 'Configurado' },
|
connected: { zh: '已配置', en: 'Configured', es: 'Configurado' },
|
||||||
notConfigured: { zh: '未配置', en: 'Not Configured', es: 'No Configurado' },
|
notConfigured: { zh: '未配置', en: 'Not Configured', es: 'No Configurado' },
|
||||||
nofxosDataSources: { zh: 'NofxOS 数据源', en: 'NofxOS Data Sources', es: 'Fuentes NofxOS' },
|
nofxosDataSources: { zh: 'NofxOS 数据源', en: 'NofxOS Data Sources', es: 'Fuentes NofxOS' },
|
||||||
|
configureApiKey: { zh: '请配置 API Key 以启用 NofxOS 数据源', en: 'Please configure API Key to enable NofxOS data sources', es: 'Configure API Key para habilitar NofxOS' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -262,6 +280,14 @@ export const chartTabs = {
|
|||||||
hyperliquid: { zh: 'HL', en: 'HL', es: 'HL' },
|
hyperliquid: { zh: 'HL', en: 'HL', es: 'HL' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HELPER FUNCTION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export function ts(entry: { zh: string; en: string; [k: string]: string }, lang: string): string {
|
||||||
|
return entry[lang] ?? entry.en ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// AGGREGATED EXPORTS FOR TRANSLATIONS.TS
|
// AGGREGATED EXPORTS FOR TRANSLATIONS.TS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -183,13 +183,13 @@ export const backtestApi = {
|
|||||||
try {
|
try {
|
||||||
const data = text ? JSON.parse(text) : null
|
const data = text ? JSON.parse(text) : null
|
||||||
throw new Error(
|
throw new Error(
|
||||||
data?.error || data?.message || text || '导出失败,请稍后再试'
|
data?.error || data?.message || text || 'Export failed, please try again later'
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error && err.message) {
|
if (err instanceof Error && err.message) {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
throw new Error(text || '导出失败,请稍后再试')
|
throw new Error(text || 'Export failed, please try again later')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.blob()
|
return res.blob()
|
||||||
|
|||||||
+36
-36
@@ -10,7 +10,7 @@ import { API_BASE, httpClient, CryptoService } from './helpers'
|
|||||||
export const configApi = {
|
export const configApi = {
|
||||||
async getModelConfigs(): Promise<AIModel[]> {
|
async getModelConfigs(): Promise<AIModel[]> {
|
||||||
const result = await httpClient.get<AIModel[]>(`${API_BASE}/models`)
|
const result = await httpClient.get<AIModel[]>(`${API_BASE}/models`)
|
||||||
if (!result.success) throw new Error('获取模型配置失败')
|
if (!result.success) throw new Error('Failed to fetch model configs')
|
||||||
return Array.isArray(result.data) ? result.data : []
|
return Array.isArray(result.data) ? result.data : []
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -18,13 +18,13 @@ export const configApi = {
|
|||||||
const result = await httpClient.get<AIModel[]>(
|
const result = await httpClient.get<AIModel[]>(
|
||||||
`${API_BASE}/supported-models`
|
`${API_BASE}/supported-models`
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('获取支持的模型失败')
|
if (!result.success) throw new Error('Failed to fetch supported models')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async getPromptTemplates(): Promise<string[]> {
|
async getPromptTemplates(): Promise<string[]> {
|
||||||
const res = await fetch(`${API_BASE}/prompt-templates`)
|
const res = await fetch(`${API_BASE}/prompt-templates`)
|
||||||
if (!res.ok) throw new Error('获取提示词模板失败')
|
if (!res.ok) throw new Error('Failed to fetch prompt templates')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (Array.isArray(data.templates)) {
|
if (Array.isArray(data.templates)) {
|
||||||
return data.templates.map((item: { name: string }) => item.name)
|
return data.templates.map((item: { name: string }) => item.name)
|
||||||
@@ -33,41 +33,41 @@ export const configApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
|
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
|
||||||
// 检查是否启用了传输加密
|
// Check if transport encryption is enabled
|
||||||
const config = await CryptoService.fetchCryptoConfig()
|
const config = await CryptoService.fetchCryptoConfig()
|
||||||
|
|
||||||
if (!config.transport_encryption) {
|
if (!config.transport_encryption) {
|
||||||
// 传输加密禁用时,直接发送明文
|
// Transport encryption disabled, send plaintext
|
||||||
const result = await httpClient.put(`${API_BASE}/models`, request)
|
const result = await httpClient.put(`${API_BASE}/models`, request)
|
||||||
if (!result.success) throw new Error('更新模型配置失败')
|
if (!result.success) throw new Error('Failed to update model configs')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取RSA公钥
|
// Fetch RSA public key
|
||||||
const publicKey = await CryptoService.fetchPublicKey()
|
const publicKey = await CryptoService.fetchPublicKey()
|
||||||
|
|
||||||
// 初始化加密服务
|
// Initialize crypto service
|
||||||
await CryptoService.initialize(publicKey)
|
await CryptoService.initialize(publicKey)
|
||||||
|
|
||||||
// 获取用户信息(从localStorage或其他地方)
|
// Get user info from localStorage
|
||||||
const userId = localStorage.getItem('user_id') || ''
|
const userId = localStorage.getItem('user_id') || ''
|
||||||
const sessionId = sessionStorage.getItem('session_id') || ''
|
const sessionId = sessionStorage.getItem('session_id') || ''
|
||||||
|
|
||||||
// 加密敏感数据
|
// Encrypt sensitive data
|
||||||
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
||||||
JSON.stringify(request),
|
JSON.stringify(request),
|
||||||
userId,
|
userId,
|
||||||
sessionId
|
sessionId
|
||||||
)
|
)
|
||||||
|
|
||||||
// 发送加密数据
|
// Send encrypted data
|
||||||
const result = await httpClient.put(`${API_BASE}/models`, encryptedPayload)
|
const result = await httpClient.put(`${API_BASE}/models`, encryptedPayload)
|
||||||
if (!result.success) throw new Error('更新模型配置失败')
|
if (!result.success) throw new Error('Failed to update model configs')
|
||||||
},
|
},
|
||||||
|
|
||||||
async getExchangeConfigs(): Promise<Exchange[]> {
|
async getExchangeConfigs(): Promise<Exchange[]> {
|
||||||
const result = await httpClient.get<Exchange[]>(`${API_BASE}/exchanges`)
|
const result = await httpClient.get<Exchange[]>(`${API_BASE}/exchanges`)
|
||||||
if (!result.success) throw new Error('获取交易所配置失败')
|
if (!result.success) throw new Error('Failed to fetch exchange configs')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export const configApi = {
|
|||||||
const result = await httpClient.get<Exchange[]>(
|
const result = await httpClient.get<Exchange[]>(
|
||||||
`${API_BASE}/supported-exchanges`
|
`${API_BASE}/supported-exchanges`
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('获取支持的交易所失败')
|
if (!result.success) throw new Error('Failed to fetch supported exchanges')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -83,93 +83,93 @@ export const configApi = {
|
|||||||
request: UpdateExchangeConfigRequest
|
request: UpdateExchangeConfigRequest
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const result = await httpClient.put(`${API_BASE}/exchanges`, request)
|
const result = await httpClient.put(`${API_BASE}/exchanges`, request)
|
||||||
if (!result.success) throw new Error('更新交易所配置失败')
|
if (!result.success) throw new Error('Failed to update exchange configs')
|
||||||
},
|
},
|
||||||
|
|
||||||
async createExchange(request: CreateExchangeRequest): Promise<{ id: string }> {
|
async createExchange(request: CreateExchangeRequest): Promise<{ id: string }> {
|
||||||
const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request)
|
const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request)
|
||||||
if (!result.success) throw new Error('创建交易所账户失败')
|
if (!result.success) throw new Error('Failed to create exchange account')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async createExchangeEncrypted(request: CreateExchangeRequest): Promise<{ id: string }> {
|
async createExchangeEncrypted(request: CreateExchangeRequest): Promise<{ id: string }> {
|
||||||
// 检查是否启用了传输加密
|
// Check if transport encryption is enabled
|
||||||
const config = await CryptoService.fetchCryptoConfig()
|
const config = await CryptoService.fetchCryptoConfig()
|
||||||
|
|
||||||
if (!config.transport_encryption) {
|
if (!config.transport_encryption) {
|
||||||
// 传输加密禁用时,直接发送明文
|
// Transport encryption disabled, send plaintext
|
||||||
const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request)
|
const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request)
|
||||||
if (!result.success) throw new Error('创建交易所账户失败')
|
if (!result.success) throw new Error('Failed to create exchange account')
|
||||||
return result.data!
|
return result.data!
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取RSA公钥
|
// Fetch RSA public key
|
||||||
const publicKey = await CryptoService.fetchPublicKey()
|
const publicKey = await CryptoService.fetchPublicKey()
|
||||||
|
|
||||||
// 初始化加密服务
|
// Initialize crypto service
|
||||||
await CryptoService.initialize(publicKey)
|
await CryptoService.initialize(publicKey)
|
||||||
|
|
||||||
// 获取用户信息
|
// Get user info
|
||||||
const userId = localStorage.getItem('user_id') || ''
|
const userId = localStorage.getItem('user_id') || ''
|
||||||
const sessionId = sessionStorage.getItem('session_id') || ''
|
const sessionId = sessionStorage.getItem('session_id') || ''
|
||||||
|
|
||||||
// 加密敏感数据
|
// Encrypt sensitive data
|
||||||
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
||||||
JSON.stringify(request),
|
JSON.stringify(request),
|
||||||
userId,
|
userId,
|
||||||
sessionId
|
sessionId
|
||||||
)
|
)
|
||||||
|
|
||||||
// 发送加密数据
|
// Send encrypted data
|
||||||
const result = await httpClient.post<{ id: string }>(
|
const result = await httpClient.post<{ id: string }>(
|
||||||
`${API_BASE}/exchanges`,
|
`${API_BASE}/exchanges`,
|
||||||
encryptedPayload
|
encryptedPayload
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('创建交易所账户失败')
|
if (!result.success) throw new Error('Failed to create exchange account')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteExchange(exchangeId: string): Promise<void> {
|
async deleteExchange(exchangeId: string): Promise<void> {
|
||||||
const result = await httpClient.delete(`${API_BASE}/exchanges/${exchangeId}`)
|
const result = await httpClient.delete(`${API_BASE}/exchanges/${exchangeId}`)
|
||||||
if (!result.success) throw new Error('删除交易所账户失败')
|
if (!result.success) throw new Error('Failed to delete exchange account')
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateExchangeConfigsEncrypted(
|
async updateExchangeConfigsEncrypted(
|
||||||
request: UpdateExchangeConfigRequest
|
request: UpdateExchangeConfigRequest
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 检查是否启用了传输加密
|
// Check if transport encryption is enabled
|
||||||
const config = await CryptoService.fetchCryptoConfig()
|
const config = await CryptoService.fetchCryptoConfig()
|
||||||
|
|
||||||
if (!config.transport_encryption) {
|
if (!config.transport_encryption) {
|
||||||
// 传输加密禁用时,直接发送明文
|
// Transport encryption disabled, send plaintext
|
||||||
const result = await httpClient.put(`${API_BASE}/exchanges`, request)
|
const result = await httpClient.put(`${API_BASE}/exchanges`, request)
|
||||||
if (!result.success) throw new Error('更新交易所配置失败')
|
if (!result.success) throw new Error('Failed to update exchange configs')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取RSA公钥
|
// Fetch RSA public key
|
||||||
const publicKey = await CryptoService.fetchPublicKey()
|
const publicKey = await CryptoService.fetchPublicKey()
|
||||||
|
|
||||||
// 初始化加密服务
|
// Initialize crypto service
|
||||||
await CryptoService.initialize(publicKey)
|
await CryptoService.initialize(publicKey)
|
||||||
|
|
||||||
// 获取用户信息(从localStorage或其他地方)
|
// Get user info from localStorage
|
||||||
const userId = localStorage.getItem('user_id') || ''
|
const userId = localStorage.getItem('user_id') || ''
|
||||||
const sessionId = sessionStorage.getItem('session_id') || ''
|
const sessionId = sessionStorage.getItem('session_id') || ''
|
||||||
|
|
||||||
// 加密敏感数据
|
// Encrypt sensitive data
|
||||||
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
||||||
JSON.stringify(request),
|
JSON.stringify(request),
|
||||||
userId,
|
userId,
|
||||||
sessionId
|
sessionId
|
||||||
)
|
)
|
||||||
|
|
||||||
// 发送加密数据
|
// Send encrypted data
|
||||||
const result = await httpClient.put(
|
const result = await httpClient.put(
|
||||||
`${API_BASE}/exchanges`,
|
`${API_BASE}/exchanges`,
|
||||||
encryptedPayload
|
encryptedPayload
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('更新交易所配置失败')
|
if (!result.success) throw new Error('Failed to update exchange configs')
|
||||||
},
|
},
|
||||||
|
|
||||||
async getServerIP(): Promise<{
|
async getServerIP(): Promise<{
|
||||||
@@ -180,7 +180,7 @@ export const configApi = {
|
|||||||
public_ip: string
|
public_ip: string
|
||||||
message: string
|
message: string
|
||||||
}>(`${API_BASE}/server-ip`)
|
}>(`${API_BASE}/server-ip`)
|
||||||
if (!result.success) throw new Error('获取服务器IP失败')
|
if (!result.success) throw new Error('Failed to fetch server IP')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-13
@@ -15,7 +15,7 @@ export const dataApi = {
|
|||||||
? `${API_BASE}/status?trader_id=${traderId}`
|
? `${API_BASE}/status?trader_id=${traderId}`
|
||||||
: `${API_BASE}/status`
|
: `${API_BASE}/status`
|
||||||
const result = await httpClient.get<SystemStatus>(url)
|
const result = await httpClient.get<SystemStatus>(url)
|
||||||
if (!result.success) throw new Error('获取系统状态失败')
|
if (!result.success) throw new Error('Failed to fetch system status')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -24,8 +24,7 @@ export const dataApi = {
|
|||||||
? `${API_BASE}/account?trader_id=${traderId}`
|
? `${API_BASE}/account?trader_id=${traderId}`
|
||||||
: `${API_BASE}/account`
|
: `${API_BASE}/account`
|
||||||
const result = await httpClient.get<AccountInfo>(url)
|
const result = await httpClient.get<AccountInfo>(url)
|
||||||
if (!result.success) throw new Error('获取账户信息失败')
|
if (!result.success) throw new Error('Failed to fetch account info')
|
||||||
console.log('Account data fetched:', result.data)
|
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ export const dataApi = {
|
|||||||
? `${API_BASE}/positions?trader_id=${traderId}`
|
? `${API_BASE}/positions?trader_id=${traderId}`
|
||||||
: `${API_BASE}/positions`
|
: `${API_BASE}/positions`
|
||||||
const result = await httpClient.get<Position[]>(url)
|
const result = await httpClient.get<Position[]>(url)
|
||||||
if (!result.success) throw new Error('获取持仓列表失败')
|
if (!result.success) throw new Error('Failed to fetch positions')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ export const dataApi = {
|
|||||||
? `${API_BASE}/decisions?trader_id=${traderId}`
|
? `${API_BASE}/decisions?trader_id=${traderId}`
|
||||||
: `${API_BASE}/decisions`
|
: `${API_BASE}/decisions`
|
||||||
const result = await httpClient.get<DecisionRecord[]>(url)
|
const result = await httpClient.get<DecisionRecord[]>(url)
|
||||||
if (!result.success) throw new Error('获取决策日志失败')
|
if (!result.success) throw new Error('Failed to fetch decision logs')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ export const dataApi = {
|
|||||||
const result = await httpClient.get<DecisionRecord[]>(
|
const result = await httpClient.get<DecisionRecord[]>(
|
||||||
`${API_BASE}/decisions/latest?${params}`
|
`${API_BASE}/decisions/latest?${params}`
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('获取最新决策失败')
|
if (!result.success) throw new Error('Failed to fetch latest decisions')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -69,7 +68,7 @@ export const dataApi = {
|
|||||||
? `${API_BASE}/statistics?trader_id=${traderId}`
|
? `${API_BASE}/statistics?trader_id=${traderId}`
|
||||||
: `${API_BASE}/statistics`
|
: `${API_BASE}/statistics`
|
||||||
const result = await httpClient.get<Statistics>(url)
|
const result = await httpClient.get<Statistics>(url)
|
||||||
if (!result.success) throw new Error('获取统计信息失败')
|
if (!result.success) throw new Error('Failed to fetch statistics')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ export const dataApi = {
|
|||||||
? `${API_BASE}/equity-history?trader_id=${traderId}`
|
? `${API_BASE}/equity-history?trader_id=${traderId}`
|
||||||
: `${API_BASE}/equity-history`
|
: `${API_BASE}/equity-history`
|
||||||
const result = await httpClient.get<any[]>(url)
|
const result = await httpClient.get<any[]>(url)
|
||||||
if (!result.success) throw new Error('获取历史数据失败')
|
if (!result.success) throw new Error('Failed to fetch equity history')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -87,13 +86,13 @@ export const dataApi = {
|
|||||||
`${API_BASE}/equity-history-batch`,
|
`${API_BASE}/equity-history-batch`,
|
||||||
{ trader_ids: traderIds, hours: hours || 0 }
|
{ trader_ids: traderIds, hours: hours || 0 }
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('获取批量历史数据失败')
|
if (!result.success) throw new Error('Failed to fetch batch equity history')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTopTraders(): Promise<any[]> {
|
async getTopTraders(): Promise<any[]> {
|
||||||
const result = await httpClient.get<any[]>(`${API_BASE}/top-traders`)
|
const result = await httpClient.get<any[]>(`${API_BASE}/top-traders`)
|
||||||
if (!result.success) throw new Error('获取前5名交易员失败')
|
if (!result.success) throw new Error('Failed to fetch top traders')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -101,7 +100,7 @@ export const dataApi = {
|
|||||||
const result = await httpClient.get<any>(
|
const result = await httpClient.get<any>(
|
||||||
`${API_BASE}/trader/${traderId}/config`
|
`${API_BASE}/trader/${traderId}/config`
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('获取公开交易员配置失败')
|
if (!result.success) throw new Error('Failed to fetch public trader config')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -109,7 +108,7 @@ export const dataApi = {
|
|||||||
const result = await httpClient.get<CompetitionData>(
|
const result = await httpClient.get<CompetitionData>(
|
||||||
`${API_BASE}/competition`
|
`${API_BASE}/competition`
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('获取竞赛数据失败')
|
if (!result.success) throw new Error('Failed to fetch competition data')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ export const dataApi = {
|
|||||||
const result = await httpClient.get<PositionHistoryResponse>(
|
const result = await httpClient.get<PositionHistoryResponse>(
|
||||||
`${API_BASE}/positions/history?trader_id=${traderId}&limit=${limit}`
|
`${API_BASE}/positions/history?trader_id=${traderId}&limit=${limit}`
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('获取历史仓位失败')
|
if (!result.success) throw new Error('Failed to fetch position history')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export async function handleJSONResponse<T>(res: Response): Promise<T> {
|
|||||||
} catch {
|
} catch {
|
||||||
/* ignore JSON parse errors */
|
/* ignore JSON parse errors */
|
||||||
}
|
}
|
||||||
throw new Error(message || '请求失败')
|
throw new Error(message || 'Request failed')
|
||||||
}
|
}
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return {} as T
|
return {} as T
|
||||||
|
|||||||
@@ -7,26 +7,26 @@ import { API_BASE, httpClient } from './helpers'
|
|||||||
export const strategyApi = {
|
export const strategyApi = {
|
||||||
async getStrategies(): Promise<Strategy[]> {
|
async getStrategies(): Promise<Strategy[]> {
|
||||||
const result = await httpClient.get<{ strategies: Strategy[] }>(`${API_BASE}/strategies`)
|
const result = await httpClient.get<{ strategies: Strategy[] }>(`${API_BASE}/strategies`)
|
||||||
if (!result.success) throw new Error('获取策略列表失败')
|
if (!result.success) throw new Error('Failed to fetch strategy list')
|
||||||
const strategies = result.data?.strategies
|
const strategies = result.data?.strategies
|
||||||
return Array.isArray(strategies) ? strategies : []
|
return Array.isArray(strategies) ? strategies : []
|
||||||
},
|
},
|
||||||
|
|
||||||
async getStrategy(strategyId: string): Promise<Strategy> {
|
async getStrategy(strategyId: string): Promise<Strategy> {
|
||||||
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/${strategyId}`)
|
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/${strategyId}`)
|
||||||
if (!result.success) throw new Error('获取策略失败')
|
if (!result.success) throw new Error('Failed to fetch strategy')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async getActiveStrategy(): Promise<Strategy> {
|
async getActiveStrategy(): Promise<Strategy> {
|
||||||
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/active`)
|
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/active`)
|
||||||
if (!result.success) throw new Error('获取激活策略失败')
|
if (!result.success) throw new Error('Failed to fetch active strategy')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async getDefaultStrategyConfig(): Promise<StrategyConfig> {
|
async getDefaultStrategyConfig(): Promise<StrategyConfig> {
|
||||||
const result = await httpClient.get<StrategyConfig>(`${API_BASE}/strategies/default-config`)
|
const result = await httpClient.get<StrategyConfig>(`${API_BASE}/strategies/default-config`)
|
||||||
if (!result.success) throw new Error('获取默认策略配置失败')
|
if (!result.success) throw new Error('Failed to fetch default strategy config')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export const strategyApi = {
|
|||||||
config: StrategyConfig
|
config: StrategyConfig
|
||||||
}): Promise<Strategy> {
|
}): Promise<Strategy> {
|
||||||
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies`, data)
|
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies`, data)
|
||||||
if (!result.success) throw new Error('创建策略失败')
|
if (!result.success) throw new Error('Failed to create strategy')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -49,24 +49,24 @@ export const strategyApi = {
|
|||||||
}
|
}
|
||||||
): Promise<Strategy> {
|
): Promise<Strategy> {
|
||||||
const result = await httpClient.put<Strategy>(`${API_BASE}/strategies/${strategyId}`, data)
|
const result = await httpClient.put<Strategy>(`${API_BASE}/strategies/${strategyId}`, data)
|
||||||
if (!result.success) throw new Error('更新策略失败')
|
if (!result.success) throw new Error('Failed to update strategy')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteStrategy(strategyId: string): Promise<void> {
|
async deleteStrategy(strategyId: string): Promise<void> {
|
||||||
const result = await httpClient.delete(`${API_BASE}/strategies/${strategyId}`)
|
const result = await httpClient.delete(`${API_BASE}/strategies/${strategyId}`)
|
||||||
if (!result.success) throw new Error('删除策略失败')
|
if (!result.success) throw new Error('Failed to delete strategy')
|
||||||
},
|
},
|
||||||
|
|
||||||
async activateStrategy(strategyId: string): Promise<Strategy> {
|
async activateStrategy(strategyId: string): Promise<Strategy> {
|
||||||
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/activate`)
|
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/activate`)
|
||||||
if (!result.success) throw new Error('激活策略失败')
|
if (!result.success) throw new Error('Failed to activate strategy')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async duplicateStrategy(strategyId: string): Promise<Strategy> {
|
async duplicateStrategy(strategyId: string): Promise<Strategy> {
|
||||||
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/duplicate`)
|
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/duplicate`)
|
||||||
if (!result.success) throw new Error('复制策略失败')
|
if (!result.success) throw new Error('Failed to duplicate strategy')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,22 +4,22 @@ import { API_BASE, httpClient } from './helpers'
|
|||||||
export const telegramApi = {
|
export const telegramApi = {
|
||||||
async getTelegramConfig(): Promise<TelegramConfig> {
|
async getTelegramConfig(): Promise<TelegramConfig> {
|
||||||
const result = await httpClient.get<TelegramConfig>(`${API_BASE}/telegram`)
|
const result = await httpClient.get<TelegramConfig>(`${API_BASE}/telegram`)
|
||||||
if (!result.success) throw new Error('获取Telegram配置失败')
|
if (!result.success) throw new Error('Failed to fetch Telegram config')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTelegramConfig(token: string, modelId?: string): Promise<void> {
|
async updateTelegramConfig(token: string, modelId?: string): Promise<void> {
|
||||||
const result = await httpClient.post(`${API_BASE}/telegram`, { bot_token: token, model_id: modelId ?? '' })
|
const result = await httpClient.post(`${API_BASE}/telegram`, { bot_token: token, model_id: modelId ?? '' })
|
||||||
if (!result.success) throw new Error('保存Telegram配置失败')
|
if (!result.success) throw new Error('Failed to save Telegram config')
|
||||||
},
|
},
|
||||||
|
|
||||||
async unbindTelegram(): Promise<void> {
|
async unbindTelegram(): Promise<void> {
|
||||||
const result = await httpClient.delete(`${API_BASE}/telegram/binding`)
|
const result = await httpClient.delete(`${API_BASE}/telegram/binding`)
|
||||||
if (!result.success) throw new Error('解绑Telegram失败')
|
if (!result.success) throw new Error('Failed to unbind Telegram')
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTelegramModel(modelId: string): Promise<void> {
|
async updateTelegramModel(modelId: string): Promise<void> {
|
||||||
const result = await httpClient.post(`${API_BASE}/telegram/model`, { model_id: modelId })
|
const result = await httpClient.post(`${API_BASE}/telegram/model`, { model_id: modelId })
|
||||||
if (!result.success) throw new Error('更新Telegram模型失败')
|
if (!result.success) throw new Error('Failed to update Telegram model')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -8,13 +8,13 @@ import { API_BASE, httpClient } from './helpers'
|
|||||||
export const traderApi = {
|
export const traderApi = {
|
||||||
async getTraders(): Promise<TraderInfo[]> {
|
async getTraders(): Promise<TraderInfo[]> {
|
||||||
const result = await httpClient.get<TraderInfo[]>(`${API_BASE}/my-traders`)
|
const result = await httpClient.get<TraderInfo[]>(`${API_BASE}/my-traders`)
|
||||||
if (!result.success) throw new Error('获取trader列表失败')
|
if (!result.success) throw new Error('Failed to fetch trader list')
|
||||||
return Array.isArray(result.data) ? result.data : []
|
return Array.isArray(result.data) ? result.data : []
|
||||||
},
|
},
|
||||||
|
|
||||||
async getPublicTraders(): Promise<any[]> {
|
async getPublicTraders(): Promise<any[]> {
|
||||||
const result = await httpClient.get<any[]>(`${API_BASE}/traders`)
|
const result = await httpClient.get<any[]>(`${API_BASE}/traders`)
|
||||||
if (!result.success) throw new Error('获取公开trader列表失败')
|
if (!result.success) throw new Error('Failed to fetch public trader list')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -23,25 +23,25 @@ export const traderApi = {
|
|||||||
`${API_BASE}/traders`,
|
`${API_BASE}/traders`,
|
||||||
request
|
request
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('创建交易员失败')
|
if (!result.success) throw new Error('Failed to create trader')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteTrader(traderId: string): Promise<void> {
|
async deleteTrader(traderId: string): Promise<void> {
|
||||||
const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`)
|
const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`)
|
||||||
if (!result.success) throw new Error('删除交易员失败')
|
if (!result.success) throw new Error('Failed to delete trader')
|
||||||
},
|
},
|
||||||
|
|
||||||
async startTrader(traderId: string): Promise<void> {
|
async startTrader(traderId: string): Promise<void> {
|
||||||
const result = await httpClient.post(
|
const result = await httpClient.post(
|
||||||
`${API_BASE}/traders/${traderId}/start`
|
`${API_BASE}/traders/${traderId}/start`
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('启动交易员失败')
|
if (!result.success) throw new Error('Failed to start trader')
|
||||||
},
|
},
|
||||||
|
|
||||||
async stopTrader(traderId: string): Promise<void> {
|
async stopTrader(traderId: string): Promise<void> {
|
||||||
const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`)
|
const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`)
|
||||||
if (!result.success) throw new Error('停止交易员失败')
|
if (!result.success) throw new Error('Failed to stop trader')
|
||||||
},
|
},
|
||||||
|
|
||||||
async toggleCompetition(traderId: string, showInCompetition: boolean): Promise<void> {
|
async toggleCompetition(traderId: string, showInCompetition: boolean): Promise<void> {
|
||||||
@@ -49,7 +49,7 @@ export const traderApi = {
|
|||||||
`${API_BASE}/traders/${traderId}/competition`,
|
`${API_BASE}/traders/${traderId}/competition`,
|
||||||
{ show_in_competition: showInCompetition }
|
{ show_in_competition: showInCompetition }
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('更新竞技场显示设置失败')
|
if (!result.success) throw new Error('Failed to update competition visibility')
|
||||||
},
|
},
|
||||||
|
|
||||||
async closePosition(traderId: string, symbol: string, side: string): Promise<{ message: string }> {
|
async closePosition(traderId: string, symbol: string, side: string): Promise<{ message: string }> {
|
||||||
@@ -57,7 +57,7 @@ export const traderApi = {
|
|||||||
`${API_BASE}/traders/${traderId}/close-position`,
|
`${API_BASE}/traders/${traderId}/close-position`,
|
||||||
{ symbol, side }
|
{ symbol, side }
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('平仓失败')
|
if (!result.success) throw new Error('Failed to close position')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -69,14 +69,14 @@ export const traderApi = {
|
|||||||
`${API_BASE}/traders/${traderId}/prompt`,
|
`${API_BASE}/traders/${traderId}/prompt`,
|
||||||
{ custom_prompt: customPrompt }
|
{ custom_prompt: customPrompt }
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('更新自定义策略失败')
|
if (!result.success) throw new Error('Failed to update custom prompt')
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTraderConfig(traderId: string): Promise<TraderConfigData> {
|
async getTraderConfig(traderId: string): Promise<TraderConfigData> {
|
||||||
const result = await httpClient.get<TraderConfigData>(
|
const result = await httpClient.get<TraderConfigData>(
|
||||||
`${API_BASE}/traders/${traderId}/config`
|
`${API_BASE}/traders/${traderId}/config`
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('获取交易员配置失败')
|
if (!result.success) throw new Error('Failed to fetch trader config')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ export const traderApi = {
|
|||||||
`${API_BASE}/traders/${traderId}`,
|
`${API_BASE}/traders/${traderId}`,
|
||||||
request
|
request
|
||||||
)
|
)
|
||||||
if (!result.success) throw new Error('更新交易员失败')
|
if (!result.success) throw new Error('Failed to update trader')
|
||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { notify } from './notify'
|
import { notify } from './notify'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 复制文本到剪贴板,并显示轻量提示。
|
* Copy text to clipboard and show a toast notification.
|
||||||
*/
|
*/
|
||||||
export async function copyWithToast(text: string, successMsg = '已复制') {
|
export async function copyWithToast(text: string, successMsg = 'Copied') {
|
||||||
try {
|
try {
|
||||||
if (navigator?.clipboard?.writeText) {
|
if (navigator?.clipboard?.writeText) {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
} else {
|
} else {
|
||||||
// 兼容降级:创建临时文本域执行复制
|
// Fallback: create temporary textarea for copy
|
||||||
const el = document.createElement('textarea')
|
const el = document.createElement('textarea')
|
||||||
el.value = text
|
el.value = text
|
||||||
el.style.position = 'fixed'
|
el.style.position = 'fixed'
|
||||||
@@ -22,7 +22,7 @@ export async function copyWithToast(text: string, successMsg = '已复制') {
|
|||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Clipboard copy failed:', err)
|
console.error('Clipboard copy failed:', err)
|
||||||
notify.error('复制失败')
|
notify.error('Copy failed')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
|
import { t } from '../i18n/translations'
|
||||||
|
|
||||||
export function DataPage() {
|
export function DataPage() {
|
||||||
const { language } = useLanguage()
|
const { language } = useLanguage()
|
||||||
@@ -7,7 +8,7 @@ export function DataPage() {
|
|||||||
<div className="w-full h-[calc(100vh-64px)]">
|
<div className="w-full h-[calc(100vh-64px)]">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://nofxos.ai/dashboard"
|
src="https://nofxos.ai/dashboard"
|
||||||
title={language === 'zh' ? '数据中心' : 'Data Center'}
|
title={t('dataCenter', language)}
|
||||||
className="w-full h-full border-0"
|
className="w-full h-full border-0"
|
||||||
allow="fullscreen"
|
allow="fullscreen"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { t } from '../i18n/translations'
|
||||||
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
|
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
|
||||||
|
|
||||||
interface PublicStrategy {
|
interface PublicStrategy {
|
||||||
@@ -106,88 +107,7 @@ export function StrategyMarketPage() {
|
|||||||
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||||
|
|
||||||
const texts = {
|
const tr = (key: string) => t(`strategyMarket.${key}`, language)
|
||||||
zh: {
|
|
||||||
title: '策略市场',
|
|
||||||
subtitle: 'STRATEGY MARKETPLACE',
|
|
||||||
description: '发现、学习并复用社区精英交易员的策略配置',
|
|
||||||
search: '搜索参数...',
|
|
||||||
all: '全部协议',
|
|
||||||
popular: '热门配置',
|
|
||||||
recent: '最新提交',
|
|
||||||
myStrategies: '我的库',
|
|
||||||
noStrategies: '无信号',
|
|
||||||
noStrategiesDesc: '当前频段未检测到策略信号',
|
|
||||||
author: 'OPERATOR',
|
|
||||||
createdAt: 'TIMESTAMP',
|
|
||||||
viewConfig: 'DECRYPT CONFIG',
|
|
||||||
hideConfig: 'ENCRYPT',
|
|
||||||
copyConfig: 'CLONE CONFIG',
|
|
||||||
copied: 'COPIED',
|
|
||||||
configHidden: 'ENCRYPTED',
|
|
||||||
configHiddenDesc: '配置参数已加密',
|
|
||||||
indicators: 'INDICATORS',
|
|
||||||
maxPositions: 'POS_LIMIT',
|
|
||||||
maxLeverage: 'LEV_MAX',
|
|
||||||
shareYours: 'UPLOAD_STRATEGY',
|
|
||||||
makePublic: 'PUBLISH',
|
|
||||||
loading: 'INITIALIZING...'
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
title: 'STRATEGY MARKET',
|
|
||||||
subtitle: 'GLOBAL STRATEGY DATABASE',
|
|
||||||
description: 'Discover, analyze, and clone high-performance trading algorithms',
|
|
||||||
search: 'SEARCH PARAMETERS...',
|
|
||||||
all: 'ALL PROTOCOLS',
|
|
||||||
popular: 'TRENDING',
|
|
||||||
recent: 'LATEST',
|
|
||||||
myStrategies: 'MY LIBRARY',
|
|
||||||
noStrategies: 'NO SIGNAL',
|
|
||||||
noStrategiesDesc: 'No strategic signals detected in this frequency',
|
|
||||||
author: 'OPERATOR',
|
|
||||||
createdAt: 'TIMESTAMP',
|
|
||||||
viewConfig: 'DECRYPT CONFIG',
|
|
||||||
hideConfig: 'ENCRYPT',
|
|
||||||
copyConfig: 'CLONE CONFIG',
|
|
||||||
copied: 'COPIED',
|
|
||||||
configHidden: 'ENCRYPTED',
|
|
||||||
configHiddenDesc: 'Configuration parameters encrypted',
|
|
||||||
indicators: 'INDICATORS',
|
|
||||||
maxPositions: 'POS_LIMIT',
|
|
||||||
maxLeverage: 'LEV_MAX',
|
|
||||||
shareYours: 'UPLOAD_STRATEGY',
|
|
||||||
makePublic: 'PUBLISH',
|
|
||||||
loading: 'INITIALIZING...'
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
title: 'PASAR STRATEGI',
|
|
||||||
subtitle: 'DATABASE STRATEGI GLOBAL',
|
|
||||||
description: 'Temukan, analisis, dan kloning algoritma trading berperforma tinggi',
|
|
||||||
search: 'CARI PARAMETER...',
|
|
||||||
all: 'SEMUA PROTOKOL',
|
|
||||||
popular: 'TREN',
|
|
||||||
recent: 'TERBARU',
|
|
||||||
myStrategies: 'PERPUSTAKAAN SAYA',
|
|
||||||
noStrategies: 'TIDAK ADA SINYAL',
|
|
||||||
noStrategiesDesc: 'Tidak ada sinyal strategis terdeteksi pada frekuensi ini',
|
|
||||||
author: 'OPERATOR',
|
|
||||||
createdAt: 'TIMESTAMP',
|
|
||||||
viewConfig: 'DEKRIPSI CONFIG',
|
|
||||||
hideConfig: 'ENKRIPSI',
|
|
||||||
copyConfig: 'KLON CONFIG',
|
|
||||||
copied: 'DISALIN',
|
|
||||||
configHidden: 'TERENKRIPSI',
|
|
||||||
configHiddenDesc: 'Parameter konfigurasi terenkripsi',
|
|
||||||
indicators: 'INDIKATOR',
|
|
||||||
maxPositions: 'BATAS_POS',
|
|
||||||
maxLeverage: 'LEV_MAKS',
|
|
||||||
shareYours: 'UNGGAH_STRATEGI',
|
|
||||||
makePublic: 'PUBLIKASI',
|
|
||||||
loading: 'MENGINISIALISASI...'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = texts[language]
|
|
||||||
|
|
||||||
// Fetch public strategies
|
// Fetch public strategies
|
||||||
const { data: strategies, isLoading } = useSWR<PublicStrategy[]>(
|
const { data: strategies, isLoading } = useSWR<PublicStrategy[]>(
|
||||||
@@ -218,7 +138,7 @@ export function StrategyMarketPage() {
|
|||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(JSON.stringify(strategy.config, null, 2))
|
await navigator.clipboard.writeText(JSON.stringify(strategy.config, null, 2))
|
||||||
setCopiedId(strategy.id)
|
setCopiedId(strategy.id)
|
||||||
toast.success(t.copied)
|
toast.success(tr('copied'))
|
||||||
setTimeout(() => setCopiedId(null), 2000)
|
setTimeout(() => setCopiedId(null), 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy:', err)
|
console.error('Failed to copy:', err)
|
||||||
@@ -271,16 +191,16 @@ export function StrategyMarketPage() {
|
|||||||
<Database className="w-8 h-8 text-nofx-gold relative z-10" />
|
<Database className="w-8 h-8 text-nofx-gold relative z-10" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={t.title}>
|
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={tr('title')}>
|
||||||
{t.title}
|
{tr('title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
|
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
|
||||||
// {t.subtitle}
|
// {tr('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
|
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
|
||||||
{t.description}
|
{tr('description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -295,7 +215,7 @@ export function StrategyMarketPage() {
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t.search}
|
placeholder={tr('search')}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
|
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
|
||||||
@@ -324,7 +244,7 @@ export function StrategyMarketPage() {
|
|||||||
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
|
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="relative z-10">{t[cat as keyof typeof t]}</span>
|
<span className="relative z-10">{tr(cat)}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -340,7 +260,7 @@ export function StrategyMarketPage() {
|
|||||||
<Cpu size={24} className="text-nofx-gold/50" />
|
<Cpu size={24} className="text-nofx-gold/50" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{t.loading}</p>
|
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{tr('loading')}</p>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
|
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
|
||||||
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||||
@@ -357,9 +277,9 @@ export function StrategyMarketPage() {
|
|||||||
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
|
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
|
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
|
||||||
[{t.noStrategies}]
|
[{tr('noStrategies')}]
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-zinc-600 text-xs tracking-wide uppercase">{t.noStrategiesDesc}</p>
|
<p className="text-zinc-600 text-xs tracking-wide uppercase">{tr('noStrategiesDesc')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -423,11 +343,11 @@ export function StrategyMarketPage() {
|
|||||||
{/* Meta Data */}
|
{/* Meta Data */}
|
||||||
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
|
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-zinc-700 uppercase">{t.author}</span>
|
<span className="text-zinc-700 uppercase">{tr('author')}</span>
|
||||||
<span className="text-zinc-400 group-hover:text-white transition-colors">@{strategy.author_email?.split('@')[0] || 'UNKNOWN'}</span>
|
<span className="text-zinc-400 group-hover:text-white transition-colors">@{strategy.author_email?.split('@')[0] || 'UNKNOWN'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col text-right">
|
<div className="flex flex-col text-right">
|
||||||
<span className="text-zinc-700 uppercase">{t.createdAt}</span>
|
<span className="text-zinc-700 uppercase">{tr('createdAt')}</span>
|
||||||
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
|
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -468,7 +388,7 @@ export function StrategyMarketPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
|
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
|
||||||
<EyeOff size={16} className="mb-1 opacity-50" />
|
<EyeOff size={16} className="mb-1 opacity-50" />
|
||||||
<span className="text-[9px] uppercase tracking-widest">{t.configHiddenDesc}</span>
|
<span className="text-[9px] uppercase tracking-widest">{tr('configHiddenDesc')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -483,19 +403,19 @@ export function StrategyMarketPage() {
|
|||||||
{copiedId === strategy.id ? (
|
{copiedId === strategy.id ? (
|
||||||
<>
|
<>
|
||||||
<Check className="w-3 h-3 text-emerald-500" />
|
<Check className="w-3 h-3 text-emerald-500" />
|
||||||
<span className="text-emerald-500">{t.copied}</span>
|
<span className="text-emerald-500">{tr('copied')}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
|
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
|
||||||
{t.copyConfig}
|
{tr('copyConfig')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button disabled className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2">
|
<button disabled className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2">
|
||||||
<Shield size={12} />
|
<Shield size={12} />
|
||||||
{t.hideConfig}
|
{tr('hideConfig')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -521,7 +441,7 @@ export function StrategyMarketPage() {
|
|||||||
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
|
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
|
||||||
<Hexagon className="text-nofx-gold animate-spin-slow" size={24} />
|
<Hexagon className="text-nofx-gold animate-spin-slow" size={24} />
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{t.shareYours}</div>
|
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{tr('shareYours')}</div>
|
||||||
<div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div>
|
<div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[1px] h-8 bg-zinc-800 mx-2"></div>
|
<div className="w-[1px] h-8 bg-zinc-800 mx-2"></div>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEdito
|
|||||||
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
|
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
|
||||||
import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor'
|
import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor'
|
||||||
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
|
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
|
||||||
|
import { t } from '../i18n/translations'
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ export function StrategyStudioPage() {
|
|||||||
})
|
})
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
// 后端返回的是数组,不是 { models: [] }
|
// Backend returns an array, not { models: [] }
|
||||||
const allModels = Array.isArray(data) ? data : (data.models || [])
|
const allModels = Array.isArray(data) ? data : (data.models || [])
|
||||||
const enabledModels = allModels.filter((m: AIModel) => m.enabled)
|
const enabledModels = allModels.filter((m: AIModel) => m.enabled)
|
||||||
setAiModels(enabledModels)
|
setAiModels(enabledModels)
|
||||||
@@ -209,7 +210,7 @@ export function StrategyStudioPage() {
|
|||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: language === 'zh' ? '新策略' : 'New Strategy',
|
name: tr('newStrategyName'),
|
||||||
description: '',
|
description: '',
|
||||||
config: defaultConfig,
|
config: defaultConfig,
|
||||||
}),
|
}),
|
||||||
@@ -222,7 +223,7 @@ export function StrategyStudioPage() {
|
|||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const newStrategy = {
|
const newStrategy = {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
name: language === 'zh' ? '新策略' : 'New Strategy',
|
name: tr('newStrategyName'),
|
||||||
description: '',
|
description: '',
|
||||||
is_active: false,
|
is_active: false,
|
||||||
is_default: false,
|
is_default: false,
|
||||||
@@ -246,11 +247,11 @@ export function StrategyStudioPage() {
|
|||||||
if (!token) return
|
if (!token) return
|
||||||
|
|
||||||
const confirmed = await confirmToast(
|
const confirmed = await confirmToast(
|
||||||
language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?',
|
tr('confirmDeleteStrategy'),
|
||||||
{
|
{
|
||||||
title: language === 'zh' ? '确认删除' : 'Confirm Delete',
|
title: tr('confirmDelete'),
|
||||||
okText: language === 'zh' ? '删除' : 'Delete',
|
okText: tr('delete'),
|
||||||
cancelText: language === 'zh' ? '取消' : 'Cancel',
|
cancelText: tr('cancel'),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
@@ -261,7 +262,7 @@ export function StrategyStudioPage() {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
if (!response.ok) throw new Error('Failed to delete strategy')
|
if (!response.ok) throw new Error('Failed to delete strategy')
|
||||||
notify.success(language === 'zh' ? '策略已删除' : 'Strategy deleted')
|
notify.success(tr('strategyDeleted'))
|
||||||
// Clear selection if deleted strategy was selected
|
// Clear selection if deleted strategy was selected
|
||||||
if (selectedStrategy?.id === id) {
|
if (selectedStrategy?.id === id) {
|
||||||
setSelectedStrategy(null)
|
setSelectedStrategy(null)
|
||||||
@@ -287,7 +288,7 @@ export function StrategyStudioPage() {
|
|||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: language === 'zh' ? '策略副本' : 'Strategy Copy',
|
name: tr('strategyCopy'),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if (!response.ok) throw new Error('Failed to duplicate strategy')
|
if (!response.ok) throw new Error('Failed to duplicate strategy')
|
||||||
@@ -330,7 +331,7 @@ export function StrategyStudioPage() {
|
|||||||
a.click()
|
a.click()
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
notify.success(language === 'zh' ? '策略已导出' : 'Strategy exported')
|
notify.success(tr('strategyExported'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import strategy from JSON file
|
// Import strategy from JSON file
|
||||||
@@ -344,7 +345,7 @@ export function StrategyStudioPage() {
|
|||||||
|
|
||||||
// Validate imported data
|
// Validate imported data
|
||||||
if (!importData.config || !importData.name) {
|
if (!importData.config || !importData.name) {
|
||||||
throw new Error(language === 'zh' ? '无效的策略文件' : 'Invalid strategy file')
|
throw new Error(tr('invalidStrategyFile'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new strategy with imported config
|
// Create new strategy with imported config
|
||||||
@@ -355,14 +356,14 @@ export function StrategyStudioPage() {
|
|||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: `${importData.name} (${language === 'zh' ? '导入' : 'Imported'})`,
|
name: `${importData.name} (${tr('imported')})`,
|
||||||
description: importData.description || '',
|
description: importData.description || '',
|
||||||
config: importData.config,
|
config: importData.config,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if (!response.ok) throw new Error('Failed to import strategy')
|
if (!response.ok) throw new Error('Failed to import strategy')
|
||||||
|
|
||||||
notify.success(language === 'zh' ? '策略已导入' : 'Strategy imported')
|
notify.success(tr('strategyImported'))
|
||||||
await fetchStrategies()
|
await fetchStrategies()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMsg = err instanceof Error ? err.message : 'Unknown error'
|
const errorMsg = err instanceof Error ? err.message : 'Unknown error'
|
||||||
@@ -402,7 +403,7 @@ export function StrategyStudioPage() {
|
|||||||
)
|
)
|
||||||
if (!response.ok) throw new Error('Failed to save strategy')
|
if (!response.ok) throw new Error('Failed to save strategy')
|
||||||
setHasChanges(false)
|
setHasChanges(false)
|
||||||
notify.success(language === 'zh' ? '策略已保存' : 'Strategy saved')
|
notify.success(tr('strategySaved'))
|
||||||
await fetchStrategies()
|
await fetchStrategies()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||||
@@ -482,51 +483,7 @@ export function StrategyStudioPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = (key: string) => {
|
const tr = (key: string) => t(`strategyStudio.${key}`, language)
|
||||||
const translations: Record<string, Record<string, string>> = {
|
|
||||||
strategyStudio: { zh: '策略工作室', en: 'Strategy Studio' },
|
|
||||||
subtitle: { zh: '可视化配置和测试交易策略', en: 'Configure and test trading strategies' },
|
|
||||||
strategies: { zh: '策略', en: 'Strategies' },
|
|
||||||
newStrategy: { zh: '新建', en: 'New' },
|
|
||||||
strategyType: { zh: '策略类型', en: 'Strategy Type' },
|
|
||||||
aiTrading: { zh: 'AI 智能交易', en: 'AI Trading' },
|
|
||||||
aiTradingDesc: { zh: 'AI 分析市场并自主决策买卖', en: 'AI analyzes market and makes trading decisions' },
|
|
||||||
gridTrading: { zh: 'AI 网格交易', en: 'AI Grid Trading' },
|
|
||||||
gridTradingDesc: { zh: 'AI 控制网格策略,在震荡市场获利', en: 'AI-controlled grid strategy for ranging markets' },
|
|
||||||
gridConfig: { zh: '网格配置', en: 'Grid Configuration' },
|
|
||||||
coinSource: { zh: '币种来源', en: 'Coin Source' },
|
|
||||||
indicators: { zh: '技术指标', en: 'Indicators' },
|
|
||||||
riskControl: { zh: '风控参数', en: 'Risk Control' },
|
|
||||||
promptSections: { zh: 'Prompt 编辑', en: 'Prompt Editor' },
|
|
||||||
customPrompt: { zh: '附加提示', en: 'Extra Prompt' },
|
|
||||||
save: { zh: '保存', en: 'Save' },
|
|
||||||
saving: { zh: '保存中...', en: 'Saving...' },
|
|
||||||
activate: { zh: '激活', en: 'Activate' },
|
|
||||||
active: { zh: '激活中', en: 'Active' },
|
|
||||||
default: { zh: '默认', en: 'Default' },
|
|
||||||
promptPreview: { zh: 'Prompt 预览', en: 'Prompt Preview' },
|
|
||||||
aiTestRun: { zh: 'AI 测试', en: 'AI Test' },
|
|
||||||
systemPrompt: { zh: 'System Prompt', en: 'System Prompt' },
|
|
||||||
userPrompt: { zh: 'User Prompt', en: 'User Prompt' },
|
|
||||||
loadPrompt: { zh: '生成 Prompt', en: 'Generate Prompt' },
|
|
||||||
refreshPrompt: { zh: '刷新', en: 'Refresh' },
|
|
||||||
promptVariant: { zh: '风格', en: 'Style' },
|
|
||||||
balanced: { zh: '平衡', en: 'Balanced' },
|
|
||||||
aggressive: { zh: '激进', en: 'Aggressive' },
|
|
||||||
conservative: { zh: '保守', en: 'Conservative' },
|
|
||||||
selectModel: { zh: '选择 AI 模型', en: 'Select AI Model' },
|
|
||||||
runTest: { zh: '运行 AI 测试', en: 'Run AI Test' },
|
|
||||||
running: { zh: '运行中...', en: 'Running...' },
|
|
||||||
aiOutput: { zh: 'AI 输出', en: 'AI Output' },
|
|
||||||
reasoning: { zh: '思维链', en: 'Reasoning' },
|
|
||||||
decisions: { zh: '决策', en: 'Decisions' },
|
|
||||||
duration: { zh: '耗时', en: 'Duration' },
|
|
||||||
noModel: { zh: '请先配置 AI 模型', en: 'Please configure AI model first' },
|
|
||||||
testNote: { zh: '使用真实 AI 模型测试,不执行交易', en: 'Test with real AI, no trading' },
|
|
||||||
publishSettings: { zh: '发布设置', en: 'Publish' },
|
|
||||||
}
|
|
||||||
return translations[key]?.[language] || key
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -550,7 +507,7 @@ export function StrategyStudioPage() {
|
|||||||
key: 'gridConfig' as const,
|
key: 'gridConfig' as const,
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
color: '#0ECB81',
|
color: '#0ECB81',
|
||||||
title: t('gridConfig'),
|
title: tr('gridConfig'),
|
||||||
forStrategyType: 'grid_trading' as const,
|
forStrategyType: 'grid_trading' as const,
|
||||||
content: editingConfig?.grid_config && (
|
content: editingConfig?.grid_config && (
|
||||||
<GridConfigEditor
|
<GridConfigEditor
|
||||||
@@ -566,7 +523,7 @@ export function StrategyStudioPage() {
|
|||||||
key: 'coinSource' as const,
|
key: 'coinSource' as const,
|
||||||
icon: Target,
|
icon: Target,
|
||||||
color: '#F0B90B',
|
color: '#F0B90B',
|
||||||
title: t('coinSource'),
|
title: tr('coinSource'),
|
||||||
forStrategyType: 'ai_trading' as const,
|
forStrategyType: 'ai_trading' as const,
|
||||||
content: editingConfig && (
|
content: editingConfig && (
|
||||||
<CoinSourceEditor
|
<CoinSourceEditor
|
||||||
@@ -581,7 +538,7 @@ export function StrategyStudioPage() {
|
|||||||
key: 'indicators' as const,
|
key: 'indicators' as const,
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
color: '#0ECB81',
|
color: '#0ECB81',
|
||||||
title: t('indicators'),
|
title: tr('indicators'),
|
||||||
forStrategyType: 'ai_trading' as const,
|
forStrategyType: 'ai_trading' as const,
|
||||||
content: editingConfig && (
|
content: editingConfig && (
|
||||||
<IndicatorEditor
|
<IndicatorEditor
|
||||||
@@ -596,7 +553,7 @@ export function StrategyStudioPage() {
|
|||||||
key: 'riskControl' as const,
|
key: 'riskControl' as const,
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
color: '#F6465D',
|
color: '#F6465D',
|
||||||
title: t('riskControl'),
|
title: tr('riskControl'),
|
||||||
forStrategyType: 'ai_trading' as const,
|
forStrategyType: 'ai_trading' as const,
|
||||||
content: editingConfig && (
|
content: editingConfig && (
|
||||||
<RiskControlEditor
|
<RiskControlEditor
|
||||||
@@ -611,7 +568,7 @@ export function StrategyStudioPage() {
|
|||||||
key: 'promptSections' as const,
|
key: 'promptSections' as const,
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
color: '#a855f7',
|
color: '#a855f7',
|
||||||
title: t('promptSections'),
|
title: tr('promptSections'),
|
||||||
forStrategyType: 'ai_trading' as const,
|
forStrategyType: 'ai_trading' as const,
|
||||||
content: editingConfig && (
|
content: editingConfig && (
|
||||||
<PromptSectionsEditor
|
<PromptSectionsEditor
|
||||||
@@ -626,18 +583,18 @@ export function StrategyStudioPage() {
|
|||||||
key: 'customPrompt' as const,
|
key: 'customPrompt' as const,
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
color: '#60a5fa',
|
color: '#60a5fa',
|
||||||
title: t('customPrompt'),
|
title: tr('customPrompt'),
|
||||||
forStrategyType: 'ai_trading' as const,
|
forStrategyType: 'ai_trading' as const,
|
||||||
content: editingConfig && (
|
content: editingConfig && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||||
{language === 'zh' ? '附加在 System Prompt 末尾的额外提示,用于补充个性化交易风格' : 'Extra prompt appended to System Prompt for personalized trading style'}
|
{tr('customPromptDesc')}
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
value={editingConfig.custom_prompt || ''}
|
value={editingConfig.custom_prompt || ''}
|
||||||
onChange={(e) => updateConfig('custom_prompt', e.target.value)}
|
onChange={(e) => updateConfig('custom_prompt', e.target.value)}
|
||||||
disabled={selectedStrategy?.is_default}
|
disabled={selectedStrategy?.is_default}
|
||||||
placeholder={language === 'zh' ? '输入自定义提示词...' : 'Enter custom prompt...'}
|
placeholder={tr('customPromptPlaceholder')}
|
||||||
className="w-full h-32 px-3 py-2 rounded-lg resize-none font-mono text-xs"
|
className="w-full h-32 px-3 py-2 rounded-lg resize-none font-mono text-xs"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||||
/>
|
/>
|
||||||
@@ -648,7 +605,7 @@ export function StrategyStudioPage() {
|
|||||||
key: 'publishSettings' as const,
|
key: 'publishSettings' as const,
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
color: '#0ECB81',
|
color: '#0ECB81',
|
||||||
title: t('publishSettings'),
|
title: tr('publishSettings'),
|
||||||
forStrategyType: 'both' as const,
|
forStrategyType: 'both' as const,
|
||||||
content: selectedStrategy && (
|
content: selectedStrategy && (
|
||||||
<PublishSettingsEditor
|
<PublishSettingsEditor
|
||||||
@@ -683,8 +640,8 @@ export function StrategyStudioPage() {
|
|||||||
<Sparkles className="w-5 h-5 text-black" />
|
<Sparkles className="w-5 h-5 text-black" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-bold text-nofx-text">{t('strategyStudio')}</h1>
|
<h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
|
||||||
<p className="text-xs text-nofx-text-muted">{t('subtitle')}</p>
|
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
@@ -702,10 +659,10 @@ export function StrategyStudioPage() {
|
|||||||
<div className="w-48 flex-shrink-0 border-r border-nofx-gold/20 overflow-y-auto bg-nofx-bg/30 backdrop-blur-sm z-10">
|
<div className="w-48 flex-shrink-0 border-r border-nofx-gold/20 overflow-y-auto bg-nofx-bg/30 backdrop-blur-sm z-10">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<div className="flex items-center justify-between mb-2 px-2">
|
<div className="flex items-center justify-between mb-2 px-2">
|
||||||
<span className="text-xs font-medium text-nofx-text-muted">{t('strategies')}</span>
|
<span className="text-xs font-medium text-nofx-text-muted">{tr('strategies')}</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{/* Import button with hidden file input */}
|
{/* Import button with hidden file input */}
|
||||||
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer text-nofx-text-muted hover:text-white" title={language === 'zh' ? '导入策略' : 'Import Strategy'}>
|
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer text-nofx-text-muted hover:text-white" title={tr('importStrategy')}>
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-4 h-4" />
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -717,7 +674,7 @@ export function StrategyStudioPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleCreateStrategy}
|
onClick={handleCreateStrategy}
|
||||||
className="p-1 rounded hover:bg-white/10 transition-colors text-nofx-gold"
|
className="p-1 rounded hover:bg-white/10 transition-colors text-nofx-gold"
|
||||||
title={language === 'zh' ? '新建策略' : 'New Strategy'}
|
title={tr('newStrategyTooltip')}
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -745,7 +702,7 @@ export function StrategyStudioPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
|
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
|
||||||
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
|
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
|
||||||
title={language === 'zh' ? '导出' : 'Export'}
|
title={tr('export')}
|
||||||
>
|
>
|
||||||
<Download className="w-3 h-3" />
|
<Download className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -754,14 +711,14 @@ export function StrategyStudioPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
|
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
|
||||||
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
|
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
|
||||||
title={language === 'zh' ? '复制' : 'Duplicate'}
|
title={tr('duplicate')}
|
||||||
>
|
>
|
||||||
<Copy className="w-3 h-3" />
|
<Copy className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
||||||
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
|
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
|
||||||
title={language === 'zh' ? '删除' : 'Delete'}
|
title={tr('deleteTooltip')}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -772,18 +729,18 @@ export function StrategyStudioPage() {
|
|||||||
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
||||||
{strategy.is_active && (
|
{strategy.is_active && (
|
||||||
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-success/15 text-nofx-success">
|
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-success/15 text-nofx-success">
|
||||||
{t('active')}
|
{tr('active')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{strategy.is_default && (
|
{strategy.is_default && (
|
||||||
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-gold/15 text-nofx-gold">
|
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-gold/15 text-nofx-gold">
|
||||||
{t('default')}
|
{tr('default')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{strategy.is_public && (
|
{strategy.is_public && (
|
||||||
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5 bg-blue-400/15 text-blue-400">
|
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5 bg-blue-400/15 text-blue-400">
|
||||||
<Globe className="w-2.5 h-2.5" />
|
<Globe className="w-2.5 h-2.5" />
|
||||||
{language === 'zh' ? '公开' : 'Public'}
|
{tr('public')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -818,11 +775,11 @@ export function StrategyStudioPage() {
|
|||||||
setHasChanges(true)
|
setHasChanges(true)
|
||||||
}}
|
}}
|
||||||
disabled={selectedStrategy.is_default}
|
disabled={selectedStrategy.is_default}
|
||||||
placeholder={language === 'zh' ? '添加策略简介...' : 'Add strategy description...'}
|
placeholder={tr('addDescription')}
|
||||||
className="text-xs bg-transparent border-none outline-none w-full text-nofx-text-muted placeholder-nofx-text-muted/50 mt-1"
|
className="text-xs bg-transparent border-none outline-none w-full text-nofx-text-muted placeholder-nofx-text-muted/50 mt-1"
|
||||||
/>
|
/>
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<span className="text-xs text-nofx-gold">● {language === 'zh' ? '未保存' : 'Unsaved'}</span>
|
<span className="text-xs text-nofx-gold">● {tr('unsaved')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
@@ -832,7 +789,7 @@ export function StrategyStudioPage() {
|
|||||||
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors bg-nofx-success/10 border border-nofx-success/30 text-nofx-success hover:bg-nofx-success/20"
|
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors bg-nofx-success/10 border border-nofx-success/30 text-nofx-success hover:bg-nofx-success/20"
|
||||||
>
|
>
|
||||||
<Check className="w-3 h-3" />
|
<Check className="w-3 h-3" />
|
||||||
{t('activate')}
|
{tr('activate')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!selectedStrategy.is_default && (
|
{!selectedStrategy.is_default && (
|
||||||
@@ -843,7 +800,7 @@ export function StrategyStudioPage() {
|
|||||||
${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`}
|
${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`}
|
||||||
>
|
>
|
||||||
<Save className="w-3 h-3" />
|
<Save className="w-3 h-3" />
|
||||||
{isSaving ? t('saving') : t('save')}
|
{isSaving ? tr('saving') : tr('save')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -854,7 +811,7 @@ export function StrategyStudioPage() {
|
|||||||
<div className="mb-4 p-4 rounded-lg bg-nofx-bg-lighter border border-nofx-gold/20">
|
<div className="mb-4 p-4 rounded-lg bg-nofx-bg-lighter border border-nofx-gold/20">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-sm font-medium text-nofx-text">{t('strategyType')}</span>
|
<span className="text-sm font-medium text-nofx-text">{tr('strategyType')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
@@ -874,9 +831,9 @@ export function StrategyStudioPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Bot className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
<Bot className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
<span className="text-sm font-medium text-nofx-text">{t('aiTrading')}</span>
|
<span className="text-sm font-medium text-nofx-text">{tr('aiTrading')}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-nofx-text-muted text-left">{t('aiTradingDesc')}</p>
|
<p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -897,9 +854,9 @@ export function StrategyStudioPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||||
<span className="text-sm font-medium text-nofx-text">{t('gridTrading')}</span>
|
<span className="text-sm font-medium text-nofx-text">{tr('gridTrading')}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-nofx-text-muted text-left">{t('gridTradingDesc')}</p>
|
<p className="text-xs text-nofx-text-muted text-left">{tr('gridTradingDesc')}</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -940,7 +897,7 @@ export function StrategyStudioPage() {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" />
|
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" />
|
||||||
<p className="text-sm text-nofx-text-muted">
|
<p className="text-sm text-nofx-text-muted">
|
||||||
{language === 'zh' ? '选择或创建策略' : 'Select or create a strategy'}
|
{tr('selectOrCreate')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -957,7 +914,7 @@ export function StrategyStudioPage() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
{t('promptPreview')}
|
{tr('promptPreview')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveRightTab('test')}
|
onClick={() => setActiveRightTab('test')}
|
||||||
@@ -965,7 +922,7 @@ export function StrategyStudioPage() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Play className="w-4 h-4" />
|
<Play className="w-4 h-4" />
|
||||||
{t('aiTestRun')}
|
{tr('aiTestRun')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -981,9 +938,9 @@ export function StrategyStudioPage() {
|
|||||||
onChange={(e) => setSelectedVariant(e.target.value)}
|
onChange={(e) => setSelectedVariant(e.target.value)}
|
||||||
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text outline-none focus:border-nofx-gold"
|
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text outline-none focus:border-nofx-gold"
|
||||||
>
|
>
|
||||||
<option value="balanced">{t('balanced')}</option>
|
<option value="balanced">{tr('balanced')}</option>
|
||||||
<option value="aggressive">{t('aggressive')}</option>
|
<option value="aggressive">{tr('aggressive')}</option>
|
||||||
<option value="conservative">{t('conservative')}</option>
|
<option value="conservative">{tr('conservative')}</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={fetchPromptPreview}
|
onClick={fetchPromptPreview}
|
||||||
@@ -991,7 +948,7 @@ export function StrategyStudioPage() {
|
|||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 bg-purple-600 hover:bg-purple-700 text-white"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 bg-purple-600 hover:bg-purple-700 text-white"
|
||||||
>
|
>
|
||||||
{isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
|
{isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
|
||||||
{promptPreview ? t('refreshPrompt') : t('loadPrompt')}
|
{promptPreview ? tr('refreshPrompt') : tr('loadPrompt')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1018,7 +975,7 @@ export function StrategyStudioPage() {
|
|||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<FileText className="w-3 h-3 text-purple-500" />
|
<FileText className="w-3 h-3 text-purple-500" />
|
||||||
<span className="text-xs font-medium text-nofx-text">{t('systemPrompt')}</span>
|
<span className="text-xs font-medium text-nofx-text">{tr('systemPrompt')}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-lighter text-nofx-text-muted">
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-lighter text-nofx-text-muted">
|
||||||
{promptPreview.system_prompt.length.toLocaleString()} chars
|
{promptPreview.system_prompt.length.toLocaleString()} chars
|
||||||
@@ -1035,7 +992,7 @@ export function StrategyStudioPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
|
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
|
||||||
<Eye className="w-10 h-10 mb-2 opacity-30" />
|
<Eye className="w-10 h-10 mb-2 opacity-30" />
|
||||||
<p className="text-sm">{language === 'zh' ? '点击生成 Prompt 预览' : 'Click to generate prompt preview'}</p>
|
<p className="text-sm">{tr('generatePromptPreview')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1046,7 +1003,7 @@ export function StrategyStudioPage() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Bot className="w-4 h-4 text-green-500" />
|
<Bot className="w-4 h-4 text-green-500" />
|
||||||
<span className="text-xs font-medium text-nofx-text">{t('selectModel')}</span>
|
<span className="text-xs font-medium text-nofx-text">{tr('selectModel')}</span>
|
||||||
</div>
|
</div>
|
||||||
{aiModels.length > 0 ? (
|
{aiModels.length > 0 ? (
|
||||||
<select
|
<select
|
||||||
@@ -1062,7 +1019,7 @@ export function StrategyStudioPage() {
|
|||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
<div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger">
|
<div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger">
|
||||||
{t('noModel')}
|
{tr('noModel')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1072,9 +1029,9 @@ export function StrategyStudioPage() {
|
|||||||
onChange={(e) => setSelectedVariant(e.target.value)}
|
onChange={(e) => setSelectedVariant(e.target.value)}
|
||||||
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
>
|
>
|
||||||
<option value="balanced">{t('balanced')}</option>
|
<option value="balanced">{tr('balanced')}</option>
|
||||||
<option value="aggressive">{t('aggressive')}</option>
|
<option value="aggressive">{tr('aggressive')}</option>
|
||||||
<option value="conservative">{t('conservative')}</option>
|
<option value="conservative">{tr('conservative')}</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={runAiTest}
|
onClick={runAiTest}
|
||||||
@@ -1084,17 +1041,17 @@ export function StrategyStudioPage() {
|
|||||||
{isRunningAiTest ? (
|
{isRunningAiTest ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
{t('running')}
|
{tr('running')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Send className="w-4 h-4" />
|
<Send className="w-4 h-4" />
|
||||||
{t('runTest')}
|
{tr('runTest')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-nofx-text-muted">{t('testNote')}</p>
|
<p className="text-[10px] text-nofx-text-muted">{tr('testNote')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Test Results */}
|
{/* Test Results */}
|
||||||
@@ -1110,7 +1067,7 @@ export function StrategyStudioPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="w-3 h-3 text-nofx-text-muted" />
|
<Clock className="w-3 h-3 text-nofx-text-muted" />
|
||||||
<span className="text-xs text-nofx-text-muted">
|
<span className="text-xs text-nofx-text-muted">
|
||||||
{t('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
|
{tr('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1120,7 +1077,7 @@ export function StrategyStudioPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<Terminal className="w-3 h-3 text-blue-400" />
|
<Terminal className="w-3 h-3 text-blue-400" />
|
||||||
<span className="text-xs font-medium text-nofx-text">{t('userPrompt')} (Input)</span>
|
<span className="text-xs font-medium text-nofx-text">{tr('userPrompt')} (Input)</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
@@ -1136,7 +1093,7 @@ export function StrategyStudioPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<Sparkles className="w-3 h-3 text-nofx-gold" />
|
<Sparkles className="w-3 h-3 text-nofx-gold" />
|
||||||
<span className="text-xs font-medium text-nofx-text">{t('reasoning')}</span>
|
<span className="text-xs font-medium text-nofx-text">{tr('reasoning')}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/30 text-nofx-text"
|
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/30 text-nofx-text"
|
||||||
@@ -1152,7 +1109,7 @@ export function StrategyStudioPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<Activity className="w-3 h-3 text-green-500" />
|
<Activity className="w-3 h-3 text-green-500" />
|
||||||
<span className="text-xs font-medium text-nofx-text">{t('decisions')}</span>
|
<span className="text-xs font-medium text-nofx-text">{tr('decisions')}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-green-500/30 text-nofx-text"
|
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-green-500/30 text-nofx-text"
|
||||||
@@ -1168,7 +1125,7 @@ export function StrategyStudioPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<FileText className="w-3 h-3 text-nofx-text-muted" />
|
<FileText className="w-3 h-3 text-nofx-text-muted" />
|
||||||
<span className="text-xs font-medium text-nofx-text">{t('aiOutput')} (Raw)</span>
|
<span className="text-xs font-medium text-nofx-text">{tr('aiOutput')} (Raw)</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
@@ -1184,7 +1141,7 @@ export function StrategyStudioPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
|
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
|
||||||
<Play className="w-10 h-10 mb-2 opacity-30" />
|
<Play className="w-10 h-10 mb-2 opacity-30" />
|
||||||
<p className="text-sm">{language === 'zh' ? '点击运行 AI 测试' : 'Click to run AI test'}</p>
|
<p className="text-sm">{tr('runAiTestHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import type {
|
|||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
|
|
||||||
// 获取友好的AI模型名称
|
// Get friendly AI model display name
|
||||||
function getModelDisplayName(modelId: string): string {
|
function getModelDisplayName(modelId: string): string {
|
||||||
switch (modelId.toLowerCase()) {
|
switch (modelId.toLowerCase()) {
|
||||||
case 'deepseek':
|
case 'deepseek':
|
||||||
@@ -189,19 +189,17 @@ export function TraderDashboardPage({
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 平仓操作
|
// Close position handler
|
||||||
const handleClosePosition = async (symbol: string, side: string) => {
|
const handleClosePosition = async (symbol: string, side: string) => {
|
||||||
if (!selectedTraderId) return
|
if (!selectedTraderId) return
|
||||||
|
|
||||||
const confirmMsg =
|
const sideLabel = side === 'LONG' ? 'LONG' : 'SHORT'
|
||||||
language === 'zh'
|
const confirmMsg = t('traderDashboard.confirmClosePosition', language, { symbol, side: sideLabel })
|
||||||
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
|
|
||||||
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
|
|
||||||
|
|
||||||
const confirmed = await confirmToast(confirmMsg, {
|
const confirmed = await confirmToast(confirmMsg, {
|
||||||
title: language === 'zh' ? '确认平仓' : 'Confirm Close',
|
title: t('traderDashboard.confirmClose', language),
|
||||||
okText: language === 'zh' ? '确认' : 'Confirm',
|
okText: t('traderDashboard.confirm', language),
|
||||||
cancelText: language === 'zh' ? '取消' : 'Cancel',
|
cancelText: t('traderDashboard.cancel', language),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
@@ -209,10 +207,8 @@ export function TraderDashboardPage({
|
|||||||
setClosingPosition(symbol)
|
setClosingPosition(symbol)
|
||||||
try {
|
try {
|
||||||
await api.closePosition(selectedTraderId, symbol, side)
|
await api.closePosition(selectedTraderId, symbol, side)
|
||||||
notify.success(
|
notify.success(t('traderDashboard.positionClosed', language))
|
||||||
language === 'zh' ? '平仓成功' : 'Position closed successfully'
|
// Use SWR mutate to refresh data instead of reloading page
|
||||||
)
|
|
||||||
// 使用 SWR mutate 刷新数据而非重新加载页面
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
mutate(`positions-${selectedTraderId}`),
|
mutate(`positions-${selectedTraderId}`),
|
||||||
mutate(`account-${selectedTraderId}`),
|
mutate(`account-${selectedTraderId}`),
|
||||||
@@ -221,9 +217,7 @@ export function TraderDashboardPage({
|
|||||||
const errorMsg =
|
const errorMsg =
|
||||||
err instanceof Error
|
err instanceof Error
|
||||||
? err.message
|
? err.message
|
||||||
: language === 'zh'
|
: t('traderDashboard.closeFailed', language)
|
||||||
? '平仓失败'
|
|
||||||
: 'Failed to close position'
|
|
||||||
notify.error(errorMsg)
|
notify.error(errorMsg)
|
||||||
} finally {
|
} finally {
|
||||||
setClosingPosition(null)
|
setClosingPosition(null)
|
||||||
@@ -257,18 +251,16 @@ export function TraderDashboardPage({
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
|
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
|
||||||
{language === 'zh' ? '无法连接到服务器' : 'Connection Failed'}
|
{t('traderDashboard.connectionFailed', language)}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-base mb-6 text-nofx-text-muted">
|
<p className="text-base mb-6 text-nofx-text-muted">
|
||||||
{language === 'zh'
|
{t('traderDashboard.connectionFailedDesc', language)}
|
||||||
? '请确认后端服务已启动。'
|
|
||||||
: 'Please check if the backend service is running.'}
|
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10"
|
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10"
|
||||||
>
|
>
|
||||||
{language === 'zh' ? '重试' : 'Retry'}
|
{t('traderDashboard.retry', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -414,12 +406,8 @@ export function TraderDashboardPage({
|
|||||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||||
title={
|
title={
|
||||||
showWalletAddress
|
showWalletAddress
|
||||||
? language === 'zh'
|
? t('traderDashboard.hideAddress', language)
|
||||||
? '隐藏地址'
|
: t('traderDashboard.showFullAddress', language)
|
||||||
: 'Hide address'
|
|
||||||
: language === 'zh'
|
|
||||||
? '显示完整地址'
|
|
||||||
: 'Show full address'
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{showWalletAddress ? (
|
{showWalletAddress ? (
|
||||||
@@ -432,7 +420,7 @@ export function TraderDashboardPage({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleCopyAddress}
|
onClick={handleCopyAddress}
|
||||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||||
title={language === 'zh' ? '复制地址' : 'Copy address'}
|
title={t('traderDashboard.copyAddress', language)}
|
||||||
>
|
>
|
||||||
{copiedAddress ? (
|
{copiedAddress ? (
|
||||||
<Check className="w-3.5 h-3.5 text-nofx-green" />
|
<Check className="w-3.5 h-3.5 text-nofx-green" />
|
||||||
@@ -443,7 +431,7 @@ export function TraderDashboardPage({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-nofx-text-muted">
|
<span className="text-xs text-nofx-text-muted">
|
||||||
{language === 'zh' ? '未配置地址' : 'No address configured'}
|
{t('traderDashboard.noAddressConfigured', language)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -599,14 +587,14 @@ export function TraderDashboardPage({
|
|||||||
<tr>
|
<tr>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left">{t('symbol', language)}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left">{t('symbol', language)}</th>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('side', language)}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('side', language)}</th>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{language === 'zh' ? '操作' : 'Action'}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('traderDashboard.action', language)}</th>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('entryPrice', language)}>{language === 'zh' ? '入场价' : 'Entry'}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('entryPrice', language)}>{t('traderDashboard.entry', language)}</th>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('markPrice', language)}>{language === 'zh' ? '标记价' : 'Mark'}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('markPrice', language)}>{t('traderDashboard.mark', language)}</th>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{language === 'zh' ? '数量' : 'Qty'}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{t('traderDashboard.qty', language)}</th>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('positionValue', language)}>{language === 'zh' ? '价值' : 'Value'}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('positionValue', language)}>{t('traderDashboard.value', language)}</th>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center hidden md:table-cell" title={t('leverage', language)}>{language === 'zh' ? '杠杆' : 'Lev.'}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center hidden md:table-cell" title={t('leverage', language)}>{t('traderDashboard.lev', language)}</th>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{language === 'zh' ? '未实现盈亏' : 'uPnL'}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{t('traderDashboard.uPnL', language)}</th>
|
||||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('liqPrice', language)}>{language === 'zh' ? '强平价' : 'Liq.'}</th>
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell" title={t('liqPrice', language)}>{t('traderDashboard.liq', language)}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -644,14 +632,14 @@ export function TraderDashboardPage({
|
|||||||
}}
|
}}
|
||||||
disabled={closingPosition === pos.symbol}
|
disabled={closingPosition === pos.symbol}
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto bg-nofx-red/10 text-nofx-red border border-nofx-red/30 hover:bg-nofx-red/20"
|
className="inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto bg-nofx-red/10 text-nofx-red border border-nofx-red/30 hover:bg-nofx-red/20"
|
||||||
title={language === 'zh' ? '平仓' : 'Close Position'}
|
title={t('traderDashboard.closePosition', language)}
|
||||||
>
|
>
|
||||||
{closingPosition === pos.symbol ? (
|
{closingPosition === pos.symbol ? (
|
||||||
<Loader2 className="w-3 h-3 animate-spin" />
|
<Loader2 className="w-3 h-3 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<LogOut className="w-3 h-3" />
|
<LogOut className="w-3 h-3" />
|
||||||
)}
|
)}
|
||||||
{language === 'zh' ? '平仓' : 'Close'}
|
{t('traderDashboard.close', language)}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.entry_price)}</td>
|
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.entry_price)}</td>
|
||||||
@@ -678,13 +666,11 @@ export function TraderDashboardPage({
|
|||||||
{totalPositions > 10 && (
|
{totalPositions > 10 && (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 pt-4 mt-4 text-xs border-t border-white/5 text-nofx-text-muted">
|
<div className="flex flex-wrap items-center justify-between gap-3 pt-4 mt-4 text-xs border-t border-white/5 text-nofx-text-muted">
|
||||||
<span>
|
<span>
|
||||||
{language === 'zh'
|
{t('traderDashboard.showingPositions', language, { shown: paginatedPositions.length, total: totalPositions })}
|
||||||
? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓`
|
|
||||||
: `Showing ${paginatedPositions.length} of ${totalPositions} positions`}
|
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{language === 'zh' ? '每页' : 'Per page'}:</span>
|
<span>{t('traderDashboard.perPage', language)}:</span>
|
||||||
<select
|
<select
|
||||||
value={positionsPageSize}
|
value={positionsPageSize}
|
||||||
onChange={(e) => setPositionsPageSize(Number(e.target.value))}
|
onChange={(e) => setPositionsPageSize(Number(e.target.value))}
|
||||||
|
|||||||
Reference in New Issue
Block a user