Merge pull request #17 from PorunC/main

引入了可配置的杠杆倍数设置、Docker 部署优化以及前端关键 Bug 修复
This commit is contained in:
tinkle-community
2025-10-29 21:18:14 +08:00
committed by GitHub
17 changed files with 521 additions and 154 deletions
+3 -2
View File
@@ -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
View File
@@ -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-certificatesHTTPS 请求需要)
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"]
+69 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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+):**
+4
View File
@@ -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": "",
+21
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+3 -1
View File
@@ -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将基于市场数据、技术指标、账户状态做出全面分析")
+3 -1
View File
@@ -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
View 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;
}
}
+9 -3
View File
@@ -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
View File
@@ -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">
+20 -10
View File
@@ -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"
+9 -9
View File
@@ -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' }}>