mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
Merge pull request #17 from PorunC/main
引入了可配置的杠杆倍数设置、Docker 部署优化以及前端关键 Bug 修复
This commit is contained in:
@@ -40,8 +40,9 @@ coin_pool_cache/
|
|||||||
# Config files (should be mounted)
|
# Config files (should be mounted)
|
||||||
config.json
|
config.json
|
||||||
|
|
||||||
# Web directory (has its own Dockerfile)
|
# Web build artifacts (but include source for multi-stage build)
|
||||||
web/
|
web/node_modules/
|
||||||
|
web/dist/
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
tmp/
|
tmp/
|
||||||
|
|||||||
112
Dockerfile
112
Dockerfile
@@ -1,59 +1,103 @@
|
|||||||
# 构建阶段
|
# Multi-stage build for NOFX AI Trading System
|
||||||
FROM golang:1.21-alpine AS builder
|
FROM golang:1.24-alpine AS backend-builder
|
||||||
|
|
||||||
# 安装必要的构建工具
|
# Install build dependencies including TA-Lib
|
||||||
RUN apk add --no-cache git gcc musl-dev
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
# 复制 go mod 文件
|
# Copy go mod files
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
# 下载依赖
|
# Download dependencies
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# 复制源代码
|
# Copy backend source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# 构建应用
|
# Build the application
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o nofx .
|
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
|
FROM alpine:latest
|
||||||
|
|
||||||
# 安装 ca-certificates(HTTPS 请求需要)
|
# Install runtime dependencies
|
||||||
RUN apk --no-cache add ca-certificates tzdata
|
RUN apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
wget \
|
||||||
|
tar \
|
||||||
|
make \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
musl-dev
|
||||||
|
|
||||||
# 设置时区为上海
|
# Install TA-Lib runtime
|
||||||
ENV TZ=Asia/Shanghai
|
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 用户
|
# Set timezone to UTC
|
||||||
RUN addgroup -g 1000 nofx && \
|
ENV TZ=UTC
|
||||||
adduser -D -u 1000 -G nofx nofx
|
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 从构建阶段复制二进制文件
|
# Copy backend binary from builder
|
||||||
COPY --from=builder /app/nofx .
|
COPY --from=backend-builder /app/nofx .
|
||||||
|
|
||||||
# 复制配置文件示例
|
# Copy frontend build from builder
|
||||||
COPY config.json.example ./config.json.example
|
COPY --from=frontend-builder /app/web/dist ./web/dist
|
||||||
|
|
||||||
# 创建必要的目录
|
# Create directories for logs and data
|
||||||
RUN mkdir -p decision_logs coin_pool_cache && \
|
RUN mkdir -p /app/decision_logs
|
||||||
chown -R nofx:nofx /app
|
|
||||||
|
|
||||||
# 切换到非 root 用户
|
# Expose ports
|
||||||
USER nofx
|
# 8080 for backend API
|
||||||
|
|
||||||
# 暴露 API 端口
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# 健康检查
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
# 启动应用
|
# Run the application
|
||||||
CMD ["./nofx"]
|
CMD ["./nofx"]
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -79,7 +79,11 @@ See [Configuration Guide](#-alternative-using-hyperliquid-exchange) for details.
|
|||||||
- **Per-Coin Position Limit**:
|
- **Per-Coin Position Limit**:
|
||||||
- Altcoins ≤ 1.5x account equity
|
- Altcoins ≤ 1.5x account equity
|
||||||
- BTC/ETH ≤ 10x 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
|
- **Margin Management**: Total usage ≤90%, AI autonomous decision on usage rate
|
||||||
- **Risk-Reward Ratio**: Mandatory ≥1:2 (stop-loss:take-profit)
|
- **Risk-Reward Ratio**: Mandatory ≥1:2 (stop-loss:take-profit)
|
||||||
- **Prevent Position Stacking**: No duplicate opening of same coin/direction
|
- **Prevent Position Stacking**: No duplicate opening of same coin/direction
|
||||||
@@ -360,6 +364,10 @@ cp config.json.example config.json
|
|||||||
"scan_interval_minutes": 3
|
"scan_interval_minutes": 3
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"leverage": {
|
||||||
|
"btc_eth_leverage": 5,
|
||||||
|
"altcoin_leverage": 5
|
||||||
|
},
|
||||||
"use_default_coins": true,
|
"use_default_coins": true,
|
||||||
"coin_pool_api_url": "",
|
"coin_pool_api_url": "",
|
||||||
"oi_top_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 |
|
| `qwen_key` | Qwen API key | `"sk-xxx"` | If using Qwen |
|
||||||
| `initial_balance` | Starting balance for P/L calculation | `1000.0` | ✅ Yes |
|
| `initial_balance` | Starting balance for P/L calculation | `1000.0` | ✅ Yes |
|
||||||
| `scan_interval_minutes` | How often to make decisions | `3` (3-5 recommended) | ✅ 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) |
|
| `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 |
|
| `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 |
|
| `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
|
#### ⚠️ Important: `use_default_coins` Field
|
||||||
|
|
||||||
**Smart Default Behavior (v2.0.2+):**
|
**Smart Default Behavior (v2.0.2+):**
|
||||||
|
|||||||
70
README.ru.md
70
README.ru.md
@@ -50,7 +50,11 @@
|
|||||||
- **Лимит позиции по монете**:
|
- **Лимит позиции по монете**:
|
||||||
- Альткоины ≤ 1.5x капитал счета
|
- Альткоины ≤ 1.5x капитал счета
|
||||||
- BTC/ETH ≤ 10x капитал счета
|
- BTC/ETH ≤ 10x капитал счета
|
||||||
- **Фиксированное плечо**: Альткоины 20x | BTC/ETH 50x
|
- **Настраиваемое плечо** (v2.0.3+):
|
||||||
|
- Установите максимальное плечо в config.json
|
||||||
|
- По умолчанию: 5x для всех монет (безопасно для субаккаунтов)
|
||||||
|
- Основные аккаунты могут увеличить: Альткоины до 20x, BTC/ETH до 50x
|
||||||
|
- ⚠️ Субаккаунты Binance ограничены ≤5x плечом
|
||||||
- **Управление маржой**: Общее использование ≤90%, AI принимает автономные решения
|
- **Управление маржой**: Общее использование ≤90%, AI принимает автономные решения
|
||||||
- **Соотношение риск/доход**: Обязательное ≥1:2 (стоп-лосс:тейк-профит)
|
- **Соотношение риск/доход**: Обязательное ≥1:2 (стоп-лосс:тейк-профит)
|
||||||
- **Предотвращение накопления позиций**: Запрет дублирования открытия той же монеты/направления
|
- **Предотвращение накопления позиций**: Запрет дублирования открытия той же монеты/направления
|
||||||
@@ -268,6 +272,10 @@ cp config.json.example config.json
|
|||||||
"scan_interval_minutes": 3
|
"scan_interval_minutes": 3
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"leverage": {
|
||||||
|
"btc_eth_leverage": 5,
|
||||||
|
"altcoin_leverage": 5
|
||||||
|
},
|
||||||
"use_default_coins": true,
|
"use_default_coins": true,
|
||||||
"coin_pool_api_url": "",
|
"coin_pool_api_url": "",
|
||||||
"oi_top_api_url": "",
|
"oi_top_api_url": "",
|
||||||
@@ -360,6 +368,9 @@ cp config.json.example config.json
|
|||||||
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen |
|
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen |
|
||||||
| `initial_balance` | Начальный баланс для расчета P/L | `1000.0` | ✅ Да |
|
| `initial_balance` | Начальный баланс для расчета P/L | `1000.0` | ✅ Да |
|
||||||
| `scan_interval_minutes` | Частота решений (минуты) | `3` (рекомендуется 3-5) | ✅ Да |
|
| `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>(Опционально, авто) |
|
| `use_default_coins` | Использовать встроенный список монет<br>**✨ Умное значение по умолчанию: `true`** (v2.0.2+)<br>Автоматически включается без API | `true` или опустить | ❌ Нет<br>(Опционально, авто) |
|
||||||
| `coin_pool_api_url` | API пользовательского пула монет<br>*Требуется только при `use_default_coins: false`* | `""` (пусто) | ❌ Нет |
|
| `coin_pool_api_url` | API пользовательского пула монет<br>*Требуется только при `use_default_coins: false`* | `""` (пусто) | ❌ Нет |
|
||||||
| `oi_top_api_url` | API открытого интереса<br>*Опциональные дополнительные данные* | `""` (пусто) | ❌ Нет |
|
| `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`
|
#### ⚠️ Важно: Поле `use_default_coins`
|
||||||
|
|
||||||
**Умное поведение по умолчанию (v2.0.2+):**
|
**Умное поведение по умолчанию (v2.0.2+):**
|
||||||
|
|||||||
70
README.uk.md
70
README.uk.md
@@ -50,7 +50,11 @@
|
|||||||
- **Ліміт позиції по монеті**:
|
- **Ліміт позиції по монеті**:
|
||||||
- Альткоїни ≤ 1.5x капітал рахунку
|
- Альткоїни ≤ 1.5x капітал рахунку
|
||||||
- BTC/ETH ≤ 10x капітал рахунку
|
- BTC/ETH ≤ 10x капітал рахунку
|
||||||
- **Фіксоване плече**: Альткоїни 20x | BTC/ETH 50x
|
- **Налаштовуване плече** (v2.0.3+):
|
||||||
|
- Встановіть максимальне плече в config.json
|
||||||
|
- За замовчуванням: 5x для всіх монет (безпечно для субакаунтів)
|
||||||
|
- Основні акаунти можуть збільшити: Альткоїни до 20x, BTC/ETH до 50x
|
||||||
|
- ⚠️ Субакаунти Binance обмежені ≤5x плечем
|
||||||
- **Управління маржею**: Загальне використання ≤90%, AI приймає автономні рішення
|
- **Управління маржею**: Загальне використання ≤90%, AI приймає автономні рішення
|
||||||
- **Співвідношення ризик/дохід**: Обов'язкове ≥1:2 (стоп-лосс:тейк-профіт)
|
- **Співвідношення ризик/дохід**: Обов'язкове ≥1:2 (стоп-лосс:тейк-профіт)
|
||||||
- **Запобігання накопиченню позицій**: Заборона дублювання відкриття тієї ж монети/напрямку
|
- **Запобігання накопиченню позицій**: Заборона дублювання відкриття тієї ж монети/напрямку
|
||||||
@@ -268,6 +272,10 @@ cp config.json.example config.json
|
|||||||
"scan_interval_minutes": 3
|
"scan_interval_minutes": 3
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"leverage": {
|
||||||
|
"btc_eth_leverage": 5,
|
||||||
|
"altcoin_leverage": 5
|
||||||
|
},
|
||||||
"use_default_coins": true,
|
"use_default_coins": true,
|
||||||
"coin_pool_api_url": "",
|
"coin_pool_api_url": "",
|
||||||
"oi_top_api_url": "",
|
"oi_top_api_url": "",
|
||||||
@@ -360,6 +368,9 @@ cp config.json.example config.json
|
|||||||
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen |
|
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen |
|
||||||
| `initial_balance` | Початковий баланс для розрахунку P/L | `1000.0` | ✅ Так |
|
| `initial_balance` | Початковий баланс для розрахунку P/L | `1000.0` | ✅ Так |
|
||||||
| `scan_interval_minutes` | Частота рішень (хвилини) | `3` (рекомендується 3-5) | ✅ Так |
|
| `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>(Опціонально, авто) |
|
| `use_default_coins` | Використовувати вбудований список монет<br>**✨ Розумне значення за замовчуванням: `true`** (v2.0.2+)<br>Автоматично включається без API | `true` або опустити | ❌ Ні<br>(Опціонально, авто) |
|
||||||
| `coin_pool_api_url` | API користувацького пулу монет<br>*Потрібно лише при `use_default_coins: false`* | `""` (пусто) | ❌ Ні |
|
| `coin_pool_api_url` | API користувацького пулу монет<br>*Потрібно лише при `use_default_coins: false`* | `""` (пусто) | ❌ Ні |
|
||||||
| `oi_top_api_url` | API відкритого інтересу<br>*Опціональні додаткові дані* | `""` (пусто) | ❌ Ні |
|
| `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`
|
#### ⚠️ Важливо: Поле `use_default_coins`
|
||||||
|
|
||||||
**Розумна поведінка за замовчуванням (v2.0.2+):**
|
**Розумна поведінка за замовчуванням (v2.0.2+):**
|
||||||
|
|||||||
@@ -50,7 +50,11 @@
|
|||||||
- **单币种仓位上限**:
|
- **单币种仓位上限**:
|
||||||
- 山寨币 ≤ 1.5倍账户净值
|
- 山寨币 ≤ 1.5倍账户净值
|
||||||
- BTC/ETH ≤ 10倍账户净值
|
- BTC/ETH ≤ 10倍账户净值
|
||||||
- **固定杠杆**: 山寨币20倍 | BTC/ETH 50倍
|
- **可配置杠杆** (v2.0.3+):
|
||||||
|
- 在config.json中设置最大杠杆
|
||||||
|
- 默认:所有币种5倍(子账户安全)
|
||||||
|
- 主账户可增加:山寨币最高20倍,BTC/ETH最高50倍
|
||||||
|
- ⚠️ 币安子账户限制≤5倍杠杆
|
||||||
- **保证金管理**: 总使用率≤90%,AI自主决策使用率
|
- **保证金管理**: 总使用率≤90%,AI自主决策使用率
|
||||||
- **风险回报比**: 强制≥1:2(止损:止盈)
|
- **风险回报比**: 强制≥1:2(止损:止盈)
|
||||||
- **防止仓位叠加**: 同币种同方向不允许重复开仓
|
- **防止仓位叠加**: 同币种同方向不允许重复开仓
|
||||||
@@ -331,6 +335,10 @@ cp config.json.example config.json
|
|||||||
"scan_interval_minutes": 3
|
"scan_interval_minutes": 3
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"leverage": {
|
||||||
|
"btc_eth_leverage": 5,
|
||||||
|
"altcoin_leverage": 5
|
||||||
|
},
|
||||||
"use_default_coins": true,
|
"use_default_coins": true,
|
||||||
"coin_pool_api_url": "",
|
"coin_pool_api_url": "",
|
||||||
"oi_top_api_url": "",
|
"oi_top_api_url": "",
|
||||||
@@ -423,6 +431,9 @@ cp config.json.example config.json
|
|||||||
| `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 |
|
| `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 |
|
||||||
| `initial_balance` | 用于P/L计算的起始余额 | `1000.0` | ✅ 是 |
|
| `initial_balance` | 用于P/L计算的起始余额 | `1000.0` | ✅ 是 |
|
||||||
| `scan_interval_minutes` | 决策频率(分钟) | `3`(建议3-5) | ✅ 是 |
|
| `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>(可选,自动默认) |
|
| `use_default_coins` | 使用内置币种列表<br>**✨ 智能默认:`true`** (v2.0.2+)<br>未提供API时自动启用 | `true` 或省略 | ❌ 否<br>(可选,自动默认) |
|
||||||
| `coin_pool_api_url` | 自定义币种池API<br>*仅当`use_default_coins: false`时需要* | `""`(空) | ❌ 否 |
|
| `coin_pool_api_url` | 自定义币种池API<br>*仅当`use_default_coins: false`时需要* | `""`(空) | ❌ 否 |
|
||||||
| `oi_top_api_url` | 持仓量API<br>*可选补充数据* | `""`(空) | ❌ 否 |
|
| `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` 字段
|
#### ⚠️ 重要:`use_default_coins` 字段
|
||||||
|
|
||||||
**智能默认行为(v2.0.2+):**
|
**智能默认行为(v2.0.2+):**
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
"scan_interval_minutes": 3
|
"scan_interval_minutes": 3
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"leverage": {
|
||||||
|
"btc_eth_leverage": 5,
|
||||||
|
"altcoin_leverage": 5
|
||||||
|
},
|
||||||
"use_default_coins": true,
|
"use_default_coins": true,
|
||||||
"coin_pool_api_url": "",
|
"coin_pool_api_url": "",
|
||||||
"oi_top_api_url": "",
|
"oi_top_api_url": "",
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ type TraderConfig struct {
|
|||||||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
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 总配置
|
// Config 总配置
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Traders []TraderConfig `json:"traders"`
|
Traders []TraderConfig `json:"traders"`
|
||||||
@@ -42,6 +48,7 @@ type Config struct {
|
|||||||
MaxDailyLoss float64 `json:"max_daily_loss"`
|
MaxDailyLoss float64 `json:"max_daily_loss"`
|
||||||
MaxDrawdown float64 `json:"max_drawdown"`
|
MaxDrawdown float64 `json:"max_drawdown"`
|
||||||
StopTradingMinutes int `json:"stop_trading_minutes"`
|
StopTradingMinutes int `json:"stop_trading_minutes"`
|
||||||
|
Leverage LeverageConfig `json:"leverage"` // 杠杆配置
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig 从文件加载配置
|
// LoadConfig 从文件加载配置
|
||||||
@@ -129,6 +136,20 @@ func (c *Config) Validate() error {
|
|||||||
c.APIServerPort = 8080 // 默认8080端口
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,15 +55,17 @@ type OITopData struct {
|
|||||||
|
|
||||||
// Context 交易上下文(传递给AI的完整信息)
|
// Context 交易上下文(传递给AI的完整信息)
|
||||||
type Context struct {
|
type Context struct {
|
||||||
CurrentTime string `json:"current_time"`
|
CurrentTime string `json:"current_time"`
|
||||||
RuntimeMinutes int `json:"runtime_minutes"`
|
RuntimeMinutes int `json:"runtime_minutes"`
|
||||||
CallCount int `json:"call_count"`
|
CallCount int `json:"call_count"`
|
||||||
Account AccountInfo `json:"account"`
|
Account AccountInfo `json:"account"`
|
||||||
Positions []PositionInfo `json:"positions"`
|
Positions []PositionInfo `json:"positions"`
|
||||||
CandidateCoins []CandidateCoin `json:"candidate_coins"`
|
CandidateCoins []CandidateCoin `json:"candidate_coins"`
|
||||||
MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用
|
MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用
|
||||||
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
|
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
|
||||||
Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis)
|
Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis)
|
||||||
|
BTCETHLeverage int `json:"-"` // BTC/ETH杠杆倍数(从配置读取)
|
||||||
|
AltcoinLeverage int `json:"-"` // 山寨币杠杆倍数(从配置读取)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decision AI的交易决策
|
// Decision AI的交易决策
|
||||||
@@ -105,7 +107,7 @@ func GetFullDecision(ctx *Context) (*FullDecision, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. 解析AI响应
|
// 4. 解析AI响应
|
||||||
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity)
|
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("解析AI响应失败: %w", err)
|
return nil, fmt.Errorf("解析AI响应失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -415,7 +417,7 @@ func buildUserPrompt(ctx *Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseFullDecisionResponse 解析AI的完整决策响应
|
// parseFullDecisionResponse 解析AI的完整决策响应
|
||||||
func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullDecision, error) {
|
func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthLeverage, altcoinLeverage int) (*FullDecision, error) {
|
||||||
// 1. 提取思维链
|
// 1. 提取思维链
|
||||||
cotTrace := extractCoTTrace(aiResponse)
|
cotTrace := extractCoTTrace(aiResponse)
|
||||||
|
|
||||||
@@ -429,7 +431,7 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullD
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 验证决策
|
// 3. 验证决策
|
||||||
if err := validateDecisions(decisions, accountEquity); err != nil {
|
if err := validateDecisions(decisions, accountEquity, btcEthLeverage, altcoinLeverage); err != nil {
|
||||||
return &FullDecision{
|
return &FullDecision{
|
||||||
CoTTrace: cotTrace,
|
CoTTrace: cotTrace,
|
||||||
Decisions: decisions,
|
Decisions: decisions,
|
||||||
@@ -496,10 +498,10 @@ func fixMissingQuotes(jsonStr string) string {
|
|||||||
return jsonStr
|
return jsonStr
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateDecisions 验证所有决策(需要账户信息)
|
// validateDecisions 验证所有决策(需要账户信息和杠杆配置)
|
||||||
func validateDecisions(decisions []Decision, accountEquity float64) error {
|
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
|
||||||
for i, decision := range decisions {
|
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)
|
return fmt.Errorf("决策 #%d 验证失败: %w", i+1, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -529,7 +531,7 @@ func findMatchingBracket(s string, start int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateDecision 验证单个决策的有效性
|
// validateDecision 验证单个决策的有效性
|
||||||
func validateDecision(d *Decision, accountEquity float64) error {
|
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
|
||||||
// 验证action
|
// 验证action
|
||||||
validActions := map[string]bool{
|
validActions := map[string]bool{
|
||||||
"open_long": true,
|
"open_long": true,
|
||||||
@@ -546,16 +548,16 @@ func validateDecision(d *Decision, accountEquity float64) error {
|
|||||||
|
|
||||||
// 开仓操作必须提供完整参数
|
// 开仓操作必须提供完整参数
|
||||||
if d.Action == "open_long" || d.Action == "open_short" {
|
if d.Action == "open_long" || d.Action == "open_short" {
|
||||||
// 根据币种判断杠杆上限和仓位价值上限
|
// 根据币种使用配置的杠杆上限
|
||||||
maxLeverage := 20 // 山寨币固定20倍
|
maxLeverage := altcoinLeverage // 山寨币使用配置的杠杆
|
||||||
maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值
|
maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值
|
||||||
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
|
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
|
||||||
maxLeverage = 50 // BTC和ETH固定50倍
|
maxLeverage = btcEthLeverage // BTC和ETH使用配置的杠杆
|
||||||
maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值
|
maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.Leverage <= 0 || d.Leverage > maxLeverage {
|
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 {
|
if d.PositionSizeUSD <= 0 {
|
||||||
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
|
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# 后端服务
|
# NOFX Trading Backend
|
||||||
backend:
|
nofx:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: nofx-backend
|
container_name: nofx-trading
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
# 挂载配置文件(必须)
|
|
||||||
- ./config.json:/app/config.json:ro
|
- ./config.json:/app/config.json:ro
|
||||||
# 持久化决策日志
|
|
||||||
- ./decision_logs:/app/decision_logs
|
- ./decision_logs:/app/decision_logs
|
||||||
# 持久化币种池缓存
|
- /etc/localtime:/etc/localtime:ro # 同步主机时间
|
||||||
- ./coin_pool_cache:/app/coin_pool_cache
|
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai # 使用中国时区
|
||||||
networks:
|
networks:
|
||||||
- nofx-network
|
- nofx-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -26,45 +23,23 @@ services:
|
|||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 10s
|
start_period: 60s
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
# 前端服务
|
# Frontend (Nginx)
|
||||||
frontend:
|
nofx-frontend:
|
||||||
build:
|
image: nginx:alpine
|
||||||
context: ./web
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: nofx-frontend
|
container_name: nofx-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "3000:80"
|
- "3000:80"
|
||||||
depends_on:
|
volumes:
|
||||||
backend:
|
- ./web/dist:/usr/share/nginx/html:ro
|
||||||
condition: service_healthy
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
networks:
|
networks:
|
||||||
- nofx-network
|
- nofx-network
|
||||||
environment:
|
depends_on:
|
||||||
- TZ=Asia/Shanghai
|
- nofx
|
||||||
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"
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
nofx-network:
|
nofx-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
|
||||||
decision_logs:
|
|
||||||
coin_pool_cache:
|
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -64,6 +64,7 @@ func main() {
|
|||||||
cfg.MaxDailyLoss,
|
cfg.MaxDailyLoss,
|
||||||
cfg.MaxDrawdown,
|
cfg.MaxDrawdown,
|
||||||
cfg.StopTradingMinutes,
|
cfg.StopTradingMinutes,
|
||||||
|
cfg.Leverage, // 传递杠杆配置
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("❌ 初始化trader失败: %v", err)
|
log.Fatalf("❌ 初始化trader失败: %v", err)
|
||||||
@@ -79,7 +80,8 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("🤖 AI全权决策模式:")
|
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将自主设置止损和止盈价格")
|
fmt.Println(" • AI将自主设置止损和止盈价格")
|
||||||
fmt.Println(" • AI将基于市场数据、技术指标、账户状态做出全面分析")
|
fmt.Println(" • AI将基于市场数据、技术指标、账户状态做出全面分析")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func NewTraderManager() *TraderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddTrader 添加一个trader
|
// 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()
|
tm.mu.Lock()
|
||||||
defer tm.mu.Unlock()
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
@@ -47,6 +47,8 @@ func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string,
|
|||||||
QwenKey: cfg.QwenKey,
|
QwenKey: cfg.QwenKey,
|
||||||
ScanInterval: cfg.GetScanInterval(),
|
ScanInterval: cfg.GetScanInterval(),
|
||||||
InitialBalance: cfg.InitialBalance,
|
InitialBalance: cfg.InitialBalance,
|
||||||
|
BTCETHLeverage: leverage.BTCETHLeverage, // 使用配置的杠杆倍数
|
||||||
|
AltcoinLeverage: leverage.AltcoinLeverage, // 使用配置的杠杆倍数
|
||||||
MaxDailyLoss: maxDailyLoss,
|
MaxDailyLoss: maxDailyLoss,
|
||||||
MaxDrawdown: maxDrawdown,
|
MaxDrawdown: maxDrawdown,
|
||||||
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
||||||
|
|||||||
49
nginx.conf
Normal file
49
nginx.conf
Normal file
@@ -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 // 初始金额(用于计算盈亏,需手动设置)
|
InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置)
|
||||||
|
|
||||||
|
// 杠杆配置
|
||||||
|
BTCETHLeverage int // BTC和ETH的杠杆倍数
|
||||||
|
AltcoinLeverage int // 山寨币的杠杆倍数
|
||||||
|
|
||||||
// 风险控制(仅作为提示,AI可自主决定)
|
// 风险控制(仅作为提示,AI可自主决定)
|
||||||
MaxDailyLoss float64 // 最大日亏损百分比(提示)
|
MaxDailyLoss float64 // 最大日亏损百分比(提示)
|
||||||
MaxDrawdown float64 // 最大回撤百分比(提示)
|
MaxDrawdown float64 // 最大回撤百分比(提示)
|
||||||
@@ -489,9 +493,11 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
|||||||
|
|
||||||
// 6. 构建上下文
|
// 6. 构建上下文
|
||||||
ctx := &decision.Context{
|
ctx := &decision.Context{
|
||||||
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
|
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
|
||||||
CallCount: at.callCount,
|
CallCount: at.callCount,
|
||||||
|
BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数
|
||||||
|
AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数
|
||||||
Account: decision.AccountInfo{
|
Account: decision.AccountInfo{
|
||||||
TotalEquity: totalEquity,
|
TotalEquity: totalEquity,
|
||||||
AvailableBalance: availableBalance,
|
AvailableBalance: availableBalance,
|
||||||
|
|||||||
@@ -290,7 +290,6 @@ function TraderDetailsPage({
|
|||||||
account,
|
account,
|
||||||
positions,
|
positions,
|
||||||
decisions,
|
decisions,
|
||||||
stats,
|
|
||||||
lastUpdate,
|
lastUpdate,
|
||||||
language,
|
language,
|
||||||
}: {
|
}: {
|
||||||
@@ -358,9 +357,9 @@ function TraderDetailsPage({
|
|||||||
{account && (
|
{account && (
|
||||||
<div className="mb-4 p-3 rounded text-xs font-mono" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
<div className="mb-4 p-3 rounded text-xs font-mono" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||||
<div style={{ color: '#848E9C' }}>
|
<div style={{ color: '#848E9C' }}>
|
||||||
🔄 Last Update: {lastUpdate} | Total Equity: {account.total_equity.toFixed(2)} |
|
🔄 Last Update: {lastUpdate} | Total Equity: {account.total_equity?.toFixed(2) || '0.00'} |
|
||||||
Available: {account.available_balance.toFixed(2)} | P&L: {account.total_pnl.toFixed(2)}{' '}
|
Available: {account.available_balance?.toFixed(2) || '0.00'} | P&L: {account.total_pnl?.toFixed(2) || '0.00'}{' '}
|
||||||
({account.total_pnl_pct.toFixed(2)}%)
|
({account.total_pnl_pct?.toFixed(2) || '0.00'}%)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -369,25 +368,25 @@ function TraderDetailsPage({
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||||
<StatCard
|
<StatCard
|
||||||
title={t('totalEquity', language)}
|
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}
|
change={account?.total_pnl_pct || 0}
|
||||||
positive={account ? account.total_pnl > 0 : false}
|
positive={(account?.total_pnl ?? 0) > 0}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title={t('availableBalance', language)}
|
title={t('availableBalance', language)}
|
||||||
value={`${account?.available_balance.toFixed(2) || '0.00'} USDT`}
|
value={`${account?.available_balance?.toFixed(2) || '0.00'} USDT`}
|
||||||
subtitle={`${((account?.available_balance / account?.total_equity) * 100 || 0).toFixed(1)}% ${t('free', language)}`}
|
subtitle={`${(account?.available_balance && account?.total_equity ? ((account.available_balance / account.total_equity) * 100).toFixed(1) : '0.0')}% ${t('free', language)}`}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title={t('totalPnL', language)}
|
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}
|
change={account?.total_pnl_pct || 0}
|
||||||
positive={account ? account.total_pnl >= 0 : false}
|
positive={(account?.total_pnl ?? 0) >= 0}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title={t('positions', language)}
|
title={t('positions', language)}
|
||||||
value={`${account?.position_count || 0}`}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -559,7 +558,6 @@ function StatCard({
|
|||||||
|
|
||||||
// Decision Card Component with CoT Trace - Binance Style
|
// Decision Card Component with CoT Trace - Binance Style
|
||||||
function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) {
|
function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) {
|
||||||
const [showInput, setShowInput] = useState(false);
|
|
||||||
const [showCoT, setShowCoT] = useState(false);
|
const [showCoT, setShowCoT] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -583,25 +581,6 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* AI Chain of Thought - Collapsible */}
|
||||||
{decision.cot_trace && (
|
{decision.cot_trace && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
LineChart,
|
LineChart,
|
||||||
Line,
|
Line,
|
||||||
@@ -19,14 +19,24 @@ interface ComparisonChartProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ComparisonChart({ traders }: ComparisonChartProps) {
|
export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||||
// 获取所有trader的历史数据
|
// 获取所有trader的历史数据 - 修复: 使用固定数量的Hook调用
|
||||||
const traderHistories = traders.map((trader) => {
|
// 始终调用最多2个trader的useSWR,即使实际trader数量不同
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
const trader1 = traders[0];
|
||||||
return useSWR(`equity-history-${trader.trader_id}`, () =>
|
const trader2 = traders[1];
|
||||||
api.getEquityHistory(trader.trader_id),
|
|
||||||
{ refreshInterval: 10000 }
|
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对象作为依赖
|
// 使用useMemo自动处理数据合并,直接使用data对象作为依赖
|
||||||
const combinedData = useMemo(() => {
|
const combinedData = useMemo(() => {
|
||||||
@@ -275,7 +285,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{traders.map((trader, index) => (
|
{traders.map((trader) => (
|
||||||
<Line
|
<Line
|
||||||
key={trader.trader_id}
|
key={trader.trader_id}
|
||||||
type="monotone"
|
type="monotone"
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ export function CompetitionPage() {
|
|||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('leader', language)}</div>
|
<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-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div>
|
||||||
<div className="text-sm font-semibold" style={{ color: leader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}>
|
<div className="text-sm font-semibold" style={{ color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||||
{leader.total_pnl >= 0 ? '+' : ''}{leader.total_pnl_pct.toFixed(2)}%
|
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +139,7 @@ export function CompetitionPage() {
|
|||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('equity', language)}</div>
|
<div className="text-xs" style={{ color: '#848E9C' }}>{t('equity', language)}</div>
|
||||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||||
{trader.total_equity.toFixed(2)}
|
{trader.total_equity?.toFixed(2) || '0.00'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -148,13 +148,13 @@ export function CompetitionPage() {
|
|||||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pnl', language)}</div>
|
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pnl', language)}</div>
|
||||||
<div
|
<div
|
||||||
className="text-lg font-bold mono"
|
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 ?? 0) >= 0 ? '+' : ''}
|
||||||
{trader.total_pnl_pct.toFixed(2)}%
|
{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs mono" style={{ color: '#848E9C' }}>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -226,8 +226,8 @@ export function CompetitionPage() {
|
|||||||
>
|
>
|
||||||
{trader.trader_name}
|
{trader.trader_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold mono mb-1" style={{ color: trader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}>
|
<div className="text-2xl font-bold mono mb-1" 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>
|
||||||
{isWinning && gap > 0 && (
|
{isWinning && gap > 0 && (
|
||||||
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
|
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user