mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
Merge pull request #17 from PorunC/main
引入了可配置的杠杆倍数设置、Docker 部署优化以及前端关键 Bug 修复
This commit is contained in:
+3
-2
@@ -40,8 +40,9 @@ coin_pool_cache/
|
||||
# Config files (should be mounted)
|
||||
config.json
|
||||
|
||||
# Web directory (has its own Dockerfile)
|
||||
web/
|
||||
# Web build artifacts (but include source for multi-stage build)
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
|
||||
+78
-34
@@ -1,59 +1,103 @@
|
||||
# 构建阶段
|
||||
FROM golang:1.21-alpine AS builder
|
||||
# Multi-stage build for NOFX AI Trading System
|
||||
FROM golang:1.24-alpine AS backend-builder
|
||||
|
||||
# 安装必要的构建工具
|
||||
RUN apk add --no-cache git gcc musl-dev
|
||||
# Install build dependencies including TA-Lib
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
make \
|
||||
gcc \
|
||||
g++ \
|
||||
musl-dev \
|
||||
wget \
|
||||
tar
|
||||
|
||||
# 设置工作目录
|
||||
# Install TA-Lib
|
||||
RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz && \
|
||||
tar -xzf ta-lib-0.4.0-src.tar.gz && \
|
||||
cd ta-lib && \
|
||||
./configure --prefix=/usr && \
|
||||
make && \
|
||||
make install && \
|
||||
cd .. && \
|
||||
rm -rf ta-lib ta-lib-0.4.0-src.tar.gz
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# 复制 go mod 文件
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
# Copy backend source code
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o nofx .
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o nofx .
|
||||
|
||||
# 运行阶段
|
||||
# Frontend build stage
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /app/web
|
||||
|
||||
# Copy package files
|
||||
COPY web/package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy frontend source
|
||||
COPY web/ ./
|
||||
|
||||
# Build frontend
|
||||
RUN npm run build
|
||||
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装 ca-certificates(HTTPS 请求需要)
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
wget \
|
||||
tar \
|
||||
make \
|
||||
gcc \
|
||||
g++ \
|
||||
musl-dev
|
||||
|
||||
# 设置时区为上海
|
||||
ENV TZ=Asia/Shanghai
|
||||
# Install TA-Lib runtime
|
||||
RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz && \
|
||||
tar -xzf ta-lib-0.4.0-src.tar.gz && \
|
||||
cd ta-lib && \
|
||||
./configure --prefix=/usr && \
|
||||
make && \
|
||||
make install && \
|
||||
cd .. && \
|
||||
rm -rf ta-lib ta-lib-0.4.0-src.tar.gz
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN addgroup -g 1000 nofx && \
|
||||
adduser -D -u 1000 -G nofx nofx
|
||||
# Set timezone to UTC
|
||||
ENV TZ=UTC
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /app/nofx .
|
||||
# Copy backend binary from builder
|
||||
COPY --from=backend-builder /app/nofx .
|
||||
|
||||
# 复制配置文件示例
|
||||
COPY config.json.example ./config.json.example
|
||||
# Copy frontend build from builder
|
||||
COPY --from=frontend-builder /app/web/dist ./web/dist
|
||||
|
||||
# 创建必要的目录
|
||||
RUN mkdir -p decision_logs coin_pool_cache && \
|
||||
chown -R nofx:nofx /app
|
||||
# Create directories for logs and data
|
||||
RUN mkdir -p /app/decision_logs
|
||||
|
||||
# 切换到非 root 用户
|
||||
USER nofx
|
||||
|
||||
# 暴露 API 端口
|
||||
# Expose ports
|
||||
# 8080 for backend API
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
# 启动应用
|
||||
# Run the application
|
||||
CMD ["./nofx"]
|
||||
|
||||
@@ -79,7 +79,11 @@ See [Configuration Guide](#-alternative-using-hyperliquid-exchange) for details.
|
||||
- **Per-Coin Position Limit**:
|
||||
- Altcoins ≤ 1.5x account equity
|
||||
- BTC/ETH ≤ 10x account equity
|
||||
- **Fixed Leverage**: Altcoins 20x | BTC/ETH 50x
|
||||
- **Configurable Leverage** (v2.0.3+):
|
||||
- Set maximum leverage in config.json
|
||||
- Default: 5x for all coins (safe for subaccounts)
|
||||
- Main accounts can increase: Altcoins up to 20x, BTC/ETH up to 50x
|
||||
- ⚠️ Binance subaccounts restricted to ≤5x leverage
|
||||
- **Margin Management**: Total usage ≤90%, AI autonomous decision on usage rate
|
||||
- **Risk-Reward Ratio**: Mandatory ≥1:2 (stop-loss:take-profit)
|
||||
- **Prevent Position Stacking**: No duplicate opening of same coin/direction
|
||||
@@ -360,6 +364,10 @@ cp config.json.example config.json
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
},
|
||||
"use_default_coins": true,
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
@@ -494,6 +502,9 @@ For running multiple AI traders competing against each other:
|
||||
| `qwen_key` | Qwen API key | `"sk-xxx"` | If using Qwen |
|
||||
| `initial_balance` | Starting balance for P/L calculation | `1000.0` | ✅ Yes |
|
||||
| `scan_interval_minutes` | How often to make decisions | `3` (3-5 recommended) | ✅ Yes |
|
||||
| **`leverage`** | **Leverage configuration (v2.0.3+)** | See below | ✅ Yes |
|
||||
| `btc_eth_leverage` | Maximum leverage for BTC/ETH<br>⚠️ Subaccounts: ≤5x | `5` (default, safe)<br>`50` (main account max) | ✅ Yes |
|
||||
| `altcoin_leverage` | Maximum leverage for altcoins<br>⚠️ Subaccounts: ≤5x | `5` (default, safe)<br>`20` (main account max) | ✅ Yes |
|
||||
| `use_default_coins` | Use built-in coin list<br>**✨ Smart Default: `true`** (v2.0.2+)<br>Auto-enabled if no API URL provided | `true` or omit | ❌ No<br>(Optional, auto-defaults) |
|
||||
| `coin_pool_api_url` | Custom coin pool API<br>*Only needed when `use_default_coins: false`* | `""` (empty) | ❌ No |
|
||||
| `oi_top_api_url` | Open interest API<br>*Optional supplement data* | `""` (empty) | ❌ No |
|
||||
@@ -504,6 +515,63 @@ For running multiple AI traders competing against each other:
|
||||
|
||||
---
|
||||
|
||||
#### ⚙️ Leverage Configuration (v2.0.3+)
|
||||
|
||||
**What is leverage configuration?**
|
||||
|
||||
The leverage settings control the maximum leverage the AI can use for each trade. This is crucial for risk management, especially for Binance subaccounts which have leverage restrictions.
|
||||
|
||||
**Configuration format:**
|
||||
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5, // Maximum leverage for BTC and ETH
|
||||
"altcoin_leverage": 5 // Maximum leverage for all other coins
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Important: Binance Subaccount Restrictions**
|
||||
|
||||
- **Subaccounts**: Limited to **≤5x leverage** by Binance
|
||||
- **Main accounts**: Can use up to 20x (altcoins) or 50x (BTC/ETH)
|
||||
- If you're using a subaccount and set leverage >5x, trades will **fail** with error: `Subaccounts are restricted from using leverage greater than 5x`
|
||||
|
||||
**Recommended settings:**
|
||||
|
||||
| Account Type | BTC/ETH Leverage | Altcoin Leverage | Risk Level |
|
||||
|-------------|------------------|------------------|------------|
|
||||
| **Subaccount** | `5` | `5` | ✅ Safe (default) |
|
||||
| **Main (Conservative)** | `10` | `10` | 🟡 Medium |
|
||||
| **Main (Aggressive)** | `20` | `15` | 🔴 High |
|
||||
| **Main (Maximum)** | `50` | `20` | 🔴🔴 Very High |
|
||||
|
||||
**Examples:**
|
||||
|
||||
**Safe configuration (subaccount or conservative):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Aggressive configuration (main account only):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 20,
|
||||
"altcoin_leverage": 15
|
||||
}
|
||||
```
|
||||
|
||||
**How AI uses leverage:**
|
||||
|
||||
- AI can choose **any leverage from 1x up to your configured maximum**
|
||||
- For example, with `altcoin_leverage: 20`, AI might decide to use 5x, 10x, or 20x based on market conditions
|
||||
- The configuration sets the **upper limit**, not a fixed value
|
||||
- AI considers volatility, risk-reward ratio, and account balance when choosing leverage
|
||||
|
||||
---
|
||||
|
||||
#### ⚠️ Important: `use_default_coins` Field
|
||||
|
||||
**Smart Default Behavior (v2.0.2+):**
|
||||
|
||||
+69
-1
@@ -50,7 +50,11 @@
|
||||
- **Лимит позиции по монете**:
|
||||
- Альткоины ≤ 1.5x капитал счета
|
||||
- BTC/ETH ≤ 10x капитал счета
|
||||
- **Фиксированное плечо**: Альткоины 20x | BTC/ETH 50x
|
||||
- **Настраиваемое плечо** (v2.0.3+):
|
||||
- Установите максимальное плечо в config.json
|
||||
- По умолчанию: 5x для всех монет (безопасно для субаккаунтов)
|
||||
- Основные аккаунты могут увеличить: Альткоины до 20x, BTC/ETH до 50x
|
||||
- ⚠️ Субаккаунты Binance ограничены ≤5x плечом
|
||||
- **Управление маржой**: Общее использование ≤90%, AI принимает автономные решения
|
||||
- **Соотношение риск/доход**: Обязательное ≥1:2 (стоп-лосс:тейк-профит)
|
||||
- **Предотвращение накопления позиций**: Запрет дублирования открытия той же монеты/направления
|
||||
@@ -268,6 +272,10 @@ cp config.json.example config.json
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
},
|
||||
"use_default_coins": true,
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
@@ -360,6 +368,9 @@ cp config.json.example config.json
|
||||
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen |
|
||||
| `initial_balance` | Начальный баланс для расчета P/L | `1000.0` | ✅ Да |
|
||||
| `scan_interval_minutes` | Частота решений (минуты) | `3` (рекомендуется 3-5) | ✅ Да |
|
||||
| **`leverage`** | **Конфигурация плеча (v2.0.3+)** | См. ниже | ✅ Да |
|
||||
| `btc_eth_leverage` | Максимальное плечо для BTC/ETH<br>⚠️ Субаккаунты: ≤5x | `5` (по умолчанию, безопасно)<br>`50` (максимум для основного аккаунта) | ✅ Да |
|
||||
| `altcoin_leverage` | Максимальное плечо для альткоинов<br>⚠️ Субаккаунты: ≤5x | `5` (по умолчанию, безопасно)<br>`20` (максимум для основного аккаунта) | ✅ Да |
|
||||
| `use_default_coins` | Использовать встроенный список монет<br>**✨ Умное значение по умолчанию: `true`** (v2.0.2+)<br>Автоматически включается без API | `true` или опустить | ❌ Нет<br>(Опционально, авто) |
|
||||
| `coin_pool_api_url` | API пользовательского пула монет<br>*Требуется только при `use_default_coins: false`* | `""` (пусто) | ❌ Нет |
|
||||
| `oi_top_api_url` | API открытого интереса<br>*Опциональные дополнительные данные* | `""` (пусто) | ❌ Нет |
|
||||
@@ -370,6 +381,63 @@ cp config.json.example config.json
|
||||
|
||||
---
|
||||
|
||||
#### ⚙️ Конфигурация плеча (v2.0.3+)
|
||||
|
||||
**Что такое конфигурация плеча?**
|
||||
|
||||
Настройки плеча контролируют максимальное плечо, которое AI может использовать для каждой сделки. Это критически важно для управления рисками, особенно для субаккаунтов Binance, которые имеют ограничения по плечу.
|
||||
|
||||
**Формат конфигурации:**
|
||||
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5, // Максимальное плечо для BTC и ETH
|
||||
"altcoin_leverage": 5 // Максимальное плечо для всех других монет
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Важно: Ограничения субаккаунтов Binance**
|
||||
|
||||
- **Субаккаунты**: Ограничены **≤5x плечом** от Binance
|
||||
- **Основные аккаунты**: Могут использовать до 20x (альткоины) или 50x (BTC/ETH)
|
||||
- Если вы используете субаккаунт и установите плечо >5x, сделки будут **завершаться с ошибкой**: `Subaccounts are restricted from using leverage greater than 5x`
|
||||
|
||||
**Рекомендуемые настройки:**
|
||||
|
||||
| Тип аккаунта | Плечо BTC/ETH | Плечо альткоинов | Уровень риска |
|
||||
|--------------|---------------|------------------|---------------|
|
||||
| **Субаккаунт** | `5` | `5` | ✅ Безопасно (по умолчанию) |
|
||||
| **Основной (Консервативно)** | `10` | `10` | 🟡 Средний |
|
||||
| **Основной (Агрессивно)** | `20` | `15` | 🔴 Высокий |
|
||||
| **Основной (Максимум)** | `50` | `20` | 🔴🔴 Очень высокий |
|
||||
|
||||
**Примеры:**
|
||||
|
||||
**Безопасная конфигурация (субаккаунт или консервативная):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Агрессивная конфигурация (только основной аккаунт):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 20,
|
||||
"altcoin_leverage": 15
|
||||
}
|
||||
```
|
||||
|
||||
**Как AI использует плечо:**
|
||||
|
||||
- AI может выбрать **любое плечо от 1x до вашего настроенного максимума**
|
||||
- Например, с `altcoin_leverage: 20`, AI может решить использовать 5x, 10x или 20x в зависимости от рыночных условий
|
||||
- Конфигурация устанавливает **верхний лимит**, а не фиксированное значение
|
||||
- AI учитывает волатильность, соотношение риск/доход и баланс аккаунта при выборе плеча
|
||||
|
||||
---
|
||||
|
||||
#### ⚠️ Важно: Поле `use_default_coins`
|
||||
|
||||
**Умное поведение по умолчанию (v2.0.2+):**
|
||||
|
||||
+69
-1
@@ -50,7 +50,11 @@
|
||||
- **Ліміт позиції по монеті**:
|
||||
- Альткоїни ≤ 1.5x капітал рахунку
|
||||
- BTC/ETH ≤ 10x капітал рахунку
|
||||
- **Фіксоване плече**: Альткоїни 20x | BTC/ETH 50x
|
||||
- **Налаштовуване плече** (v2.0.3+):
|
||||
- Встановіть максимальне плече в config.json
|
||||
- За замовчуванням: 5x для всіх монет (безпечно для субакаунтів)
|
||||
- Основні акаунти можуть збільшити: Альткоїни до 20x, BTC/ETH до 50x
|
||||
- ⚠️ Субакаунти Binance обмежені ≤5x плечем
|
||||
- **Управління маржею**: Загальне використання ≤90%, AI приймає автономні рішення
|
||||
- **Співвідношення ризик/дохід**: Обов'язкове ≥1:2 (стоп-лосс:тейк-профіт)
|
||||
- **Запобігання накопиченню позицій**: Заборона дублювання відкриття тієї ж монети/напрямку
|
||||
@@ -268,6 +272,10 @@ cp config.json.example config.json
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
},
|
||||
"use_default_coins": true,
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
@@ -360,6 +368,9 @@ cp config.json.example config.json
|
||||
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen |
|
||||
| `initial_balance` | Початковий баланс для розрахунку P/L | `1000.0` | ✅ Так |
|
||||
| `scan_interval_minutes` | Частота рішень (хвилини) | `3` (рекомендується 3-5) | ✅ Так |
|
||||
| **`leverage`** | **Конфігурація плеча (v2.0.3+)** | Див. нижче | ✅ Так |
|
||||
| `btc_eth_leverage` | Максимальне плече для BTC/ETH<br>⚠️ Субакаунти: ≤5x | `5` (за замовчуванням, безпечно)<br>`50` (максимум для основного акаунта) | ✅ Так |
|
||||
| `altcoin_leverage` | Максимальне плече для альткоїнів<br>⚠️ Субакаунти: ≤5x | `5` (за замовчуванням, безпечно)<br>`20` (максимум для основного акаунта) | ✅ Так |
|
||||
| `use_default_coins` | Використовувати вбудований список монет<br>**✨ Розумне значення за замовчуванням: `true`** (v2.0.2+)<br>Автоматично включається без API | `true` або опустити | ❌ Ні<br>(Опціонально, авто) |
|
||||
| `coin_pool_api_url` | API користувацького пулу монет<br>*Потрібно лише при `use_default_coins: false`* | `""` (пусто) | ❌ Ні |
|
||||
| `oi_top_api_url` | API відкритого інтересу<br>*Опціональні додаткові дані* | `""` (пусто) | ❌ Ні |
|
||||
@@ -370,6 +381,63 @@ cp config.json.example config.json
|
||||
|
||||
---
|
||||
|
||||
#### ⚙️ Конфігурація плеча (v2.0.3+)
|
||||
|
||||
**Що таке конфігурація плеча?**
|
||||
|
||||
Налаштування плеча контролюють максимальне плече, яке AI може використовувати для кожної угоди. Це критично важливо для управління ризиками, особливо для субакаунтів Binance, які мають обмеження по плечу.
|
||||
|
||||
**Формат конфігурації:**
|
||||
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5, // Максимальне плече для BTC та ETH
|
||||
"altcoin_leverage": 5 // Максимальне плече для всіх інших монет
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Важливо: Обмеження субакаунтів Binance**
|
||||
|
||||
- **Субакаунти**: Обмежені **≤5x плечем** від Binance
|
||||
- **Основні акаунти**: Можуть використовувати до 20x (альткоїни) або 50x (BTC/ETH)
|
||||
- Якщо ви використовуєте субакаунт і встановите плече >5x, угоди будуть **завершуватися з помилкою**: `Subaccounts are restricted from using leverage greater than 5x`
|
||||
|
||||
**Рекомендовані налаштування:**
|
||||
|
||||
| Тип акаунта | Плече BTC/ETH | Плече альткоїнів | Рівень ризику |
|
||||
|-------------|---------------|------------------|---------------|
|
||||
| **Субакаунт** | `5` | `5` | ✅ Безпечно (за замовчуванням) |
|
||||
| **Основний (Консервативно)** | `10` | `10` | 🟡 Середній |
|
||||
| **Основний (Агресивно)** | `20` | `15` | 🔴 Високий |
|
||||
| **Основний (Максимум)** | `50` | `20` | 🔴🔴 Дуже високий |
|
||||
|
||||
**Приклади:**
|
||||
|
||||
**Безпечна конфігурація (субакаунт або консервативна):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Агресивна конфігурація (тільки основний акаунт):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 20,
|
||||
"altcoin_leverage": 15
|
||||
}
|
||||
```
|
||||
|
||||
**Як AI використовує плече:**
|
||||
|
||||
- AI може вибрати **будь-яке плече від 1x до вашого налаштованого максимуму**
|
||||
- Наприклад, з `altcoin_leverage: 20`, AI може вирішити використовувати 5x, 10x або 20x залежно від ринкових умов
|
||||
- Конфігурація встановлює **верхню межу**, а не фіксоване значення
|
||||
- AI враховує волатильність, співвідношення ризик/дохід та баланс акаунта при виборі плеча
|
||||
|
||||
---
|
||||
|
||||
#### ⚠️ Важливо: Поле `use_default_coins`
|
||||
|
||||
**Розумна поведінка за замовчуванням (v2.0.2+):**
|
||||
|
||||
+69
-1
@@ -50,7 +50,11 @@
|
||||
- **单币种仓位上限**:
|
||||
- 山寨币 ≤ 1.5倍账户净值
|
||||
- BTC/ETH ≤ 10倍账户净值
|
||||
- **固定杠杆**: 山寨币20倍 | BTC/ETH 50倍
|
||||
- **可配置杠杆** (v2.0.3+):
|
||||
- 在config.json中设置最大杠杆
|
||||
- 默认:所有币种5倍(子账户安全)
|
||||
- 主账户可增加:山寨币最高20倍,BTC/ETH最高50倍
|
||||
- ⚠️ 币安子账户限制≤5倍杠杆
|
||||
- **保证金管理**: 总使用率≤90%,AI自主决策使用率
|
||||
- **风险回报比**: 强制≥1:2(止损:止盈)
|
||||
- **防止仓位叠加**: 同币种同方向不允许重复开仓
|
||||
@@ -331,6 +335,10 @@ cp config.json.example config.json
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
},
|
||||
"use_default_coins": true,
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
@@ -423,6 +431,9 @@ cp config.json.example config.json
|
||||
| `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 |
|
||||
| `initial_balance` | 用于P/L计算的起始余额 | `1000.0` | ✅ 是 |
|
||||
| `scan_interval_minutes` | 决策频率(分钟) | `3`(建议3-5) | ✅ 是 |
|
||||
| **`leverage`** | **杠杆配置 (v2.0.3+)** | 见下文 | ✅ 是 |
|
||||
| `btc_eth_leverage` | BTC/ETH最大杠杆<br>⚠️ 子账户:≤5倍 | `5`(默认,安全)<br>`50`(主账户最大) | ✅ 是 |
|
||||
| `altcoin_leverage` | 山寨币最大杠杆<br>⚠️ 子账户:≤5倍 | `5`(默认,安全)<br>`20`(主账户最大) | ✅ 是 |
|
||||
| `use_default_coins` | 使用内置币种列表<br>**✨ 智能默认:`true`** (v2.0.2+)<br>未提供API时自动启用 | `true` 或省略 | ❌ 否<br>(可选,自动默认) |
|
||||
| `coin_pool_api_url` | 自定义币种池API<br>*仅当`use_default_coins: false`时需要* | `""`(空) | ❌ 否 |
|
||||
| `oi_top_api_url` | 持仓量API<br>*可选补充数据* | `""`(空) | ❌ 否 |
|
||||
@@ -433,6 +444,63 @@ cp config.json.example config.json
|
||||
|
||||
---
|
||||
|
||||
#### ⚙️ 杠杆配置 (v2.0.3+)
|
||||
|
||||
**什么是杠杆配置?**
|
||||
|
||||
杠杆设置控制AI每次交易可以使用的最大杠杆。这对于风险管理至关重要,特别是对于有杠杆限制的币安子账户。
|
||||
|
||||
**配置格式:**
|
||||
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5, // BTC和ETH的最大杠杆
|
||||
"altcoin_leverage": 5 // 所有其他币种的最大杠杆
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ 重要:币安子账户限制**
|
||||
|
||||
- **子账户**:币安限制为**≤5倍杠杆**
|
||||
- **主账户**:可使用最高20倍(山寨币)或50倍(BTC/ETH)
|
||||
- 如果您使用子账户并设置杠杆>5倍,交易将**失败**,错误信息:`Subaccounts are restricted from using leverage greater than 5x`
|
||||
|
||||
**推荐设置:**
|
||||
|
||||
| 账户类型 | BTC/ETH杠杆 | 山寨币杠杆 | 风险级别 |
|
||||
|---------|------------|-----------|---------|
|
||||
| **子账户** | `5` | `5` | ✅ 安全(默认) |
|
||||
| **主账户(保守)** | `10` | `10` | 🟡 中等 |
|
||||
| **主账户(激进)** | `20` | `15` | 🔴 高 |
|
||||
| **主账户(最大)** | `50` | `20` | 🔴🔴 非常高 |
|
||||
|
||||
**示例:**
|
||||
|
||||
**安全配置(子账户或保守):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
}
|
||||
```
|
||||
|
||||
**激进配置(仅主账户):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 20,
|
||||
"altcoin_leverage": 15
|
||||
}
|
||||
```
|
||||
|
||||
**AI如何使用杠杆:**
|
||||
|
||||
- AI可以选择**从1倍到您配置的最大值之间的任何杠杆**
|
||||
- 例如,当`altcoin_leverage: 20`时,AI可能根据市场情况决定使用5倍、10倍或20倍
|
||||
- 配置设置的是**上限**,而不是固定值
|
||||
- AI在选择杠杆时会考虑波动性、风险回报比和账户余额
|
||||
|
||||
---
|
||||
|
||||
#### ⚠️ 重要:`use_default_coins` 字段
|
||||
|
||||
**智能默认行为(v2.0.2+):**
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
},
|
||||
"use_default_coins": true,
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
|
||||
@@ -32,6 +32,12 @@ type TraderConfig struct {
|
||||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
||||
}
|
||||
|
||||
// LeverageConfig 杠杆配置
|
||||
type LeverageConfig struct {
|
||||
BTCETHLeverage int `json:"btc_eth_leverage"` // BTC和ETH的杠杆倍数(主账户建议5-50,子账户≤5)
|
||||
AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币的杠杆倍数(主账户建议5-20,子账户≤5)
|
||||
}
|
||||
|
||||
// Config 总配置
|
||||
type Config struct {
|
||||
Traders []TraderConfig `json:"traders"`
|
||||
@@ -42,6 +48,7 @@ type Config struct {
|
||||
MaxDailyLoss float64 `json:"max_daily_loss"`
|
||||
MaxDrawdown float64 `json:"max_drawdown"`
|
||||
StopTradingMinutes int `json:"stop_trading_minutes"`
|
||||
Leverage LeverageConfig `json:"leverage"` // 杠杆配置
|
||||
}
|
||||
|
||||
// LoadConfig 从文件加载配置
|
||||
@@ -129,6 +136,20 @@ func (c *Config) Validate() error {
|
||||
c.APIServerPort = 8080 // 默认8080端口
|
||||
}
|
||||
|
||||
// 设置杠杆默认值(适配币安子账户限制,最大5倍)
|
||||
if c.Leverage.BTCETHLeverage <= 0 {
|
||||
c.Leverage.BTCETHLeverage = 5 // 默认5倍(安全值,适配子账户)
|
||||
}
|
||||
if c.Leverage.BTCETHLeverage > 5 {
|
||||
fmt.Printf("⚠️ 警告: BTC/ETH杠杆设置为%dx,如果使用子账户可能会失败(子账户限制≤5x)\n", c.Leverage.BTCETHLeverage)
|
||||
}
|
||||
if c.Leverage.AltcoinLeverage <= 0 {
|
||||
c.Leverage.AltcoinLeverage = 5 // 默认5倍(安全值,适配子账户)
|
||||
}
|
||||
if c.Leverage.AltcoinLeverage > 5 {
|
||||
fmt.Printf("⚠️ 警告: 山寨币杠杆设置为%dx,如果使用子账户可能会失败(子账户限制≤5x)\n", c.Leverage.AltcoinLeverage)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+22
-20
@@ -55,15 +55,17 @@ type OITopData struct {
|
||||
|
||||
// Context 交易上下文(传递给AI的完整信息)
|
||||
type Context struct {
|
||||
CurrentTime string `json:"current_time"`
|
||||
RuntimeMinutes int `json:"runtime_minutes"`
|
||||
CallCount int `json:"call_count"`
|
||||
Account AccountInfo `json:"account"`
|
||||
Positions []PositionInfo `json:"positions"`
|
||||
CandidateCoins []CandidateCoin `json:"candidate_coins"`
|
||||
MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用
|
||||
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
|
||||
Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis)
|
||||
CurrentTime string `json:"current_time"`
|
||||
RuntimeMinutes int `json:"runtime_minutes"`
|
||||
CallCount int `json:"call_count"`
|
||||
Account AccountInfo `json:"account"`
|
||||
Positions []PositionInfo `json:"positions"`
|
||||
CandidateCoins []CandidateCoin `json:"candidate_coins"`
|
||||
MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用
|
||||
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
|
||||
Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis)
|
||||
BTCETHLeverage int `json:"-"` // BTC/ETH杠杆倍数(从配置读取)
|
||||
AltcoinLeverage int `json:"-"` // 山寨币杠杆倍数(从配置读取)
|
||||
}
|
||||
|
||||
// Decision AI的交易决策
|
||||
@@ -105,7 +107,7 @@ func GetFullDecision(ctx *Context) (*FullDecision, error) {
|
||||
}
|
||||
|
||||
// 4. 解析AI响应
|
||||
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity)
|
||||
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析AI响应失败: %w", err)
|
||||
}
|
||||
@@ -415,7 +417,7 @@ func buildUserPrompt(ctx *Context) string {
|
||||
}
|
||||
|
||||
// parseFullDecisionResponse 解析AI的完整决策响应
|
||||
func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullDecision, error) {
|
||||
func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthLeverage, altcoinLeverage int) (*FullDecision, error) {
|
||||
// 1. 提取思维链
|
||||
cotTrace := extractCoTTrace(aiResponse)
|
||||
|
||||
@@ -429,7 +431,7 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullD
|
||||
}
|
||||
|
||||
// 3. 验证决策
|
||||
if err := validateDecisions(decisions, accountEquity); err != nil {
|
||||
if err := validateDecisions(decisions, accountEquity, btcEthLeverage, altcoinLeverage); err != nil {
|
||||
return &FullDecision{
|
||||
CoTTrace: cotTrace,
|
||||
Decisions: decisions,
|
||||
@@ -496,10 +498,10 @@ func fixMissingQuotes(jsonStr string) string {
|
||||
return jsonStr
|
||||
}
|
||||
|
||||
// validateDecisions 验证所有决策(需要账户信息)
|
||||
func validateDecisions(decisions []Decision, accountEquity float64) error {
|
||||
// validateDecisions 验证所有决策(需要账户信息和杠杆配置)
|
||||
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
|
||||
for i, decision := range decisions {
|
||||
if err := validateDecision(&decision, accountEquity); err != nil {
|
||||
if err := validateDecision(&decision, accountEquity, btcEthLeverage, altcoinLeverage); err != nil {
|
||||
return fmt.Errorf("决策 #%d 验证失败: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
@@ -529,7 +531,7 @@ func findMatchingBracket(s string, start int) int {
|
||||
}
|
||||
|
||||
// validateDecision 验证单个决策的有效性
|
||||
func validateDecision(d *Decision, accountEquity float64) error {
|
||||
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
|
||||
// 验证action
|
||||
validActions := map[string]bool{
|
||||
"open_long": true,
|
||||
@@ -546,16 +548,16 @@ func validateDecision(d *Decision, accountEquity float64) error {
|
||||
|
||||
// 开仓操作必须提供完整参数
|
||||
if d.Action == "open_long" || d.Action == "open_short" {
|
||||
// 根据币种判断杠杆上限和仓位价值上限
|
||||
maxLeverage := 20 // 山寨币固定20倍
|
||||
// 根据币种使用配置的杠杆上限
|
||||
maxLeverage := altcoinLeverage // 山寨币使用配置的杠杆
|
||||
maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值
|
||||
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
|
||||
maxLeverage = 50 // BTC和ETH固定50倍
|
||||
maxLeverage = btcEthLeverage // BTC和ETH使用配置的杠杆
|
||||
maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值
|
||||
}
|
||||
|
||||
if d.Leverage <= 0 || d.Leverage > maxLeverage {
|
||||
return fmt.Errorf("杠杆必须在1-%d之间(%s): %d", maxLeverage, d.Symbol, d.Leverage)
|
||||
return fmt.Errorf("杠杆必须在1-%d之间(%s,当前配置上限%d倍): %d", maxLeverage, d.Symbol, maxLeverage, d.Leverage)
|
||||
}
|
||||
if d.PositionSizeUSD <= 0 {
|
||||
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
|
||||
|
||||
+14
-39
@@ -1,24 +1,21 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# 后端服务
|
||||
backend:
|
||||
# NOFX Trading Backend
|
||||
nofx:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: nofx-backend
|
||||
container_name: nofx-trading
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
# 挂载配置文件(必须)
|
||||
- ./config.json:/app/config.json:ro
|
||||
# 持久化决策日志
|
||||
- ./decision_logs:/app/decision_logs
|
||||
# 持久化币种池缓存
|
||||
- ./coin_pool_cache:/app/coin_pool_cache
|
||||
- /etc/localtime:/etc/localtime:ro # 同步主机时间
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- TZ=Asia/Shanghai # 使用中国时区
|
||||
networks:
|
||||
- nofx-network
|
||||
healthcheck:
|
||||
@@ -26,45 +23,23 @@ services:
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
start_period: 60s
|
||||
|
||||
# 前端服务
|
||||
frontend:
|
||||
build:
|
||||
context: ./web
|
||||
dockerfile: Dockerfile
|
||||
# Frontend (Nginx)
|
||||
nofx-frontend:
|
||||
image: nginx:alpine
|
||||
container_name: nofx-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./web/dist:/usr/share/nginx/html:ro
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
networks:
|
||||
- nofx-network
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
depends_on:
|
||||
- nofx
|
||||
|
||||
networks:
|
||||
nofx-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
decision_logs:
|
||||
coin_pool_cache:
|
||||
|
||||
@@ -64,6 +64,7 @@ func main() {
|
||||
cfg.MaxDailyLoss,
|
||||
cfg.MaxDrawdown,
|
||||
cfg.StopTradingMinutes,
|
||||
cfg.Leverage, // 传递杠杆配置
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 初始化trader失败: %v", err)
|
||||
@@ -79,7 +80,8 @@ func main() {
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("🤖 AI全权决策模式:")
|
||||
fmt.Println(" • AI将自主决定每笔交易的杠杆倍数(山寨币1-20倍,BTC/ETH最高50倍)")
|
||||
fmt.Printf(" • AI将自主决定每笔交易的杠杆倍数(山寨币最高%d倍,BTC/ETH最高%d倍)\n",
|
||||
cfg.Leverage.AltcoinLeverage, cfg.Leverage.BTCETHLeverage)
|
||||
fmt.Println(" • AI将自主决定每笔交易的仓位大小")
|
||||
fmt.Println(" • AI将自主设置止损和止盈价格")
|
||||
fmt.Println(" • AI将基于市场数据、技术指标、账户状态做出全面分析")
|
||||
|
||||
@@ -23,7 +23,7 @@ func NewTraderManager() *TraderManager {
|
||||
}
|
||||
|
||||
// AddTrader 添加一个trader
|
||||
func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int) error {
|
||||
func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, leverage config.LeverageConfig) error {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
@@ -47,6 +47,8 @@ func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string,
|
||||
QwenKey: cfg.QwenKey,
|
||||
ScanInterval: cfg.GetScanInterval(),
|
||||
InitialBalance: cfg.InitialBalance,
|
||||
BTCETHLeverage: leverage.BTCETHLeverage, // 使用配置的杠杆倍数
|
||||
AltcoinLeverage: leverage.AltcoinLeverage, // 使用配置的杠杆倍数
|
||||
MaxDailyLoss: maxDailyLoss,
|
||||
MaxDrawdown: maxDrawdown,
|
||||
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Frontend root
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
|
||||
|
||||
# Frontend routes (SPA)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy API requests to backend
|
||||
location /api/ {
|
||||
proxy_pass http://nofx-trading:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Increase timeout for long-running API calls
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
return 200 "OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,10 @@ type AutoTraderConfig struct {
|
||||
// 账户配置
|
||||
InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置)
|
||||
|
||||
// 杠杆配置
|
||||
BTCETHLeverage int // BTC和ETH的杠杆倍数
|
||||
AltcoinLeverage int // 山寨币的杠杆倍数
|
||||
|
||||
// 风险控制(仅作为提示,AI可自主决定)
|
||||
MaxDailyLoss float64 // 最大日亏损百分比(提示)
|
||||
MaxDrawdown float64 // 最大回撤百分比(提示)
|
||||
@@ -489,9 +493,11 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
|
||||
// 6. 构建上下文
|
||||
ctx := &decision.Context{
|
||||
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
|
||||
CallCount: at.callCount,
|
||||
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
|
||||
CallCount: at.callCount,
|
||||
BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数
|
||||
AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数
|
||||
Account: decision.AccountInfo{
|
||||
TotalEquity: totalEquity,
|
||||
AvailableBalance: availableBalance,
|
||||
|
||||
+10
-31
@@ -290,7 +290,6 @@ function TraderDetailsPage({
|
||||
account,
|
||||
positions,
|
||||
decisions,
|
||||
stats,
|
||||
lastUpdate,
|
||||
language,
|
||||
}: {
|
||||
@@ -358,9 +357,9 @@ function TraderDetailsPage({
|
||||
{account && (
|
||||
<div className="mb-4 p-3 rounded text-xs font-mono" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<div style={{ color: '#848E9C' }}>
|
||||
🔄 Last Update: {lastUpdate} | Total Equity: {account.total_equity.toFixed(2)} |
|
||||
Available: {account.available_balance.toFixed(2)} | P&L: {account.total_pnl.toFixed(2)}{' '}
|
||||
({account.total_pnl_pct.toFixed(2)}%)
|
||||
🔄 Last Update: {lastUpdate} | Total Equity: {account.total_equity?.toFixed(2) || '0.00'} |
|
||||
Available: {account.available_balance?.toFixed(2) || '0.00'} | P&L: {account.total_pnl?.toFixed(2) || '0.00'}{' '}
|
||||
({account.total_pnl_pct?.toFixed(2) || '0.00'}%)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -369,25 +368,25 @@ function TraderDetailsPage({
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<StatCard
|
||||
title={t('totalEquity', language)}
|
||||
value={`${account?.total_equity.toFixed(2) || '0.00'} USDT`}
|
||||
value={`${account?.total_equity?.toFixed(2) || '0.00'} USDT`}
|
||||
change={account?.total_pnl_pct || 0}
|
||||
positive={account ? account.total_pnl > 0 : false}
|
||||
positive={(account?.total_pnl ?? 0) > 0}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('availableBalance', language)}
|
||||
value={`${account?.available_balance.toFixed(2) || '0.00'} USDT`}
|
||||
subtitle={`${((account?.available_balance / account?.total_equity) * 100 || 0).toFixed(1)}% ${t('free', language)}`}
|
||||
value={`${account?.available_balance?.toFixed(2) || '0.00'} USDT`}
|
||||
subtitle={`${(account?.available_balance && account?.total_equity ? ((account.available_balance / account.total_equity) * 100).toFixed(1) : '0.0')}% ${t('free', language)}`}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('totalPnL', language)}
|
||||
value={`${account?.total_pnl >= 0 ? '+' : ''}${account?.total_pnl.toFixed(2) || '0.00'} USDT`}
|
||||
value={`${account?.total_pnl !== undefined && account.total_pnl >= 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`}
|
||||
change={account?.total_pnl_pct || 0}
|
||||
positive={account ? account.total_pnl >= 0 : false}
|
||||
positive={(account?.total_pnl ?? 0) >= 0}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('positions', language)}
|
||||
value={`${account?.position_count || 0}`}
|
||||
subtitle={`${t('margin', language)}: ${account?.margin_used_pct.toFixed(1) || '0.0'}%`}
|
||||
subtitle={`${t('margin', language)}: ${account?.margin_used_pct?.toFixed(1) || '0.0'}%`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -559,7 +558,6 @@ function StatCard({
|
||||
|
||||
// Decision Card Component with CoT Trace - Binance Style
|
||||
function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) {
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [showCoT, setShowCoT] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -583,25 +581,6 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Input Prompt - Collapsible */}
|
||||
{decision.input_prompt && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowInput(!showInput)}
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: '#8B5CF6' }}
|
||||
>
|
||||
<span className="font-semibold">📥 {t('inputPrompt', language)}</span>
|
||||
<span className="text-xs">{showInput ? t('collapse', language) : t('expand', language)}</span>
|
||||
</button>
|
||||
{showInput && (
|
||||
<div className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}>
|
||||
{decision.input_prompt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Chain of Thought - Collapsible */}
|
||||
{decision.cot_trace && (
|
||||
<div className="mb-3">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
@@ -19,14 +19,24 @@ interface ComparisonChartProps {
|
||||
}
|
||||
|
||||
export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
// 获取所有trader的历史数据
|
||||
const traderHistories = traders.map((trader) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useSWR(`equity-history-${trader.trader_id}`, () =>
|
||||
api.getEquityHistory(trader.trader_id),
|
||||
{ refreshInterval: 10000 }
|
||||
);
|
||||
});
|
||||
// 获取所有trader的历史数据 - 修复: 使用固定数量的Hook调用
|
||||
// 始终调用最多2个trader的useSWR,即使实际trader数量不同
|
||||
const trader1 = traders[0];
|
||||
const trader2 = traders[1];
|
||||
|
||||
const history1 = useSWR(
|
||||
trader1 ? `equity-history-${trader1.trader_id}` : null,
|
||||
trader1 ? () => api.getEquityHistory(trader1.trader_id) : null,
|
||||
{ refreshInterval: 10000 }
|
||||
);
|
||||
|
||||
const history2 = useSWR(
|
||||
trader2 ? `equity-history-${trader2.trader_id}` : null,
|
||||
trader2 ? () => api.getEquityHistory(trader2.trader_id) : null,
|
||||
{ refreshInterval: 10000 }
|
||||
);
|
||||
|
||||
const traderHistories = [history1, history2].slice(0, traders.length);
|
||||
|
||||
// 使用useMemo自动处理数据合并,直接使用data对象作为依赖
|
||||
const combinedData = useMemo(() => {
|
||||
@@ -275,7 +285,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
{traders.map((trader, index) => (
|
||||
{traders.map((trader) => (
|
||||
<Line
|
||||
key={trader.trader_id}
|
||||
type="monotone"
|
||||
|
||||
@@ -73,8 +73,8 @@ export function CompetitionPage() {
|
||||
<div className="text-right">
|
||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('leader', language)}</div>
|
||||
<div className="text-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div>
|
||||
<div className="text-sm font-semibold" style={{ color: leader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||
{leader.total_pnl >= 0 ? '+' : ''}{leader.total_pnl_pct.toFixed(2)}%
|
||||
<div className="text-sm font-semibold" style={{ color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +139,7 @@ export function CompetitionPage() {
|
||||
<div className="text-right">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('equity', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{trader.total_equity.toFixed(2)}
|
||||
{trader.total_equity?.toFixed(2) || '0.00'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -148,13 +148,13 @@ export function CompetitionPage() {
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pnl', language)}</div>
|
||||
<div
|
||||
className="text-lg font-bold mono"
|
||||
style={{ color: trader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}
|
||||
style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}
|
||||
>
|
||||
{trader.total_pnl >= 0 ? '+' : ''}
|
||||
{trader.total_pnl_pct.toFixed(2)}%
|
||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
|
||||
{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||
</div>
|
||||
<div className="text-xs mono" style={{ color: '#848E9C' }}>
|
||||
{trader.total_pnl >= 0 ? '+' : ''}{trader.total_pnl.toFixed(2)}
|
||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl?.toFixed(2) || '0.00'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -226,8 +226,8 @@ export function CompetitionPage() {
|
||||
>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
<div className="text-2xl font-bold mono mb-1" style={{ color: trader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||
{trader.total_pnl >= 0 ? '+' : ''}{trader.total_pnl_pct.toFixed(2)}%
|
||||
<div className="text-2xl font-bold mono mb-1" style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||
</div>
|
||||
{isWinning && gap > 0 && (
|
||||
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
|
||||
|
||||
Reference in New Issue
Block a user