Merge branch 'tinkle-community:main' into main

This commit is contained in:
d0lwl0b
2025-10-30 12:13:00 +08:00
committed by GitHub
22 changed files with 1758 additions and 30 deletions
+23 -3
View File
@@ -40,7 +40,7 @@
| 字段 | 类型 | 必需 | 说明 |
|-----|------|------|------|
| `ai_model` | string | ✅ | 设置为 `"custom"` 启用自定义 API |
| `custom_api_url` | string | ✅ | API 的 Base URL (不含 `/chat/completions`) |
| `custom_api_url` | string | ✅ | API 的 Base URL (不含 `/chat/completions`)。特殊用法:如果以 `#` 结尾,则使用完整 URL(不自动添加路径) |
| `custom_api_key` | string | ✅ | API 密钥 |
| `custom_model_name` | string | ✅ | 模型名称 (如 `gpt-4o`, `claude-3-5-sonnet` 等) |
@@ -90,11 +90,26 @@
}
```
### 5. 使用完整自定义路径(末尾添加 #)
对于某些特殊的 API 端点,如果已经包含完整路径(包括 `/chat/completions` 或其他自定义路径),可以在 URL 末尾添加 `#` 来强制使用完整 URL
```json
{
"ai_model": "custom",
"custom_api_url": "https://api.example.com/v2/ai/chat/completions#",
"custom_api_key": "your-api-key",
"custom_model_name": "custom-model"
}
```
**注意**`#` 会被自动去除,实际请求会发送到 `https://api.example.com/v2/ai/chat/completions`
## 兼容性要求
自定义 API 必须:
1. 支持 OpenAI Chat Completions 格式
2. 接受 `POST /chat/completions` 端点
2. 接受 `POST` 请求到 `/chat/completions` 端点(或在 URL 末尾添加 `#` 以使用自定义路径)
3. 支持 `Authorization: Bearer {api_key}` 认证
4. 返回标准的 OpenAI 响应格式
@@ -103,6 +118,9 @@
1. **URL 格式**`custom_api_url` 应该是 Base URL,系统会自动添加 `/chat/completions`
- ✅ 正确:`https://api.openai.com/v1`
- ❌ 错误:`https://api.openai.com/v1/chat/completions`
- 🔧 **特殊用法**:如果需要使用完整的自定义路径(不自动添加 `/chat/completions`),可以在 URL 末尾添加 `#`
- 例如:`https://api.example.com/custom/path/chat/completions#`
- 系统会自动去掉 `#` 并直接使用该完整 URL
2. **模型名称**:确保 `custom_model_name` 与 API 提供商支持的模型名称完全一致
@@ -157,7 +175,9 @@
### 问题:API 调用失败
**可能原因**
1. URL 格式错误(检查是否包含了 `/chat/completions`
1. URL 格式错误
- 普通用法:不应包含 `/chat/completions`(系统会自动添加)
- 特殊用法:如果需要完整路径,记得在 URL 末尾添加 `#`
2. API 密钥无效
3. 模型名称错误
4. 网络连接问题
+1 -1
View File
@@ -44,7 +44,7 @@ RUN go mod download
COPY . .
# Build the application
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o nofx .
RUN CGO_ENABLED=1 GOOS=linux go build -trimpath -ldflags="-s -w" -o nofx .
# Frontend build stage
FROM node:18-alpine AS frontend-builder
+122 -4
View File
@@ -9,7 +9,7 @@
---
An automated crypto futures trading system powered by **DeepSeek/Qwen AI**, supporting **Binance and Hyperliquid exchanges**, **multi-AI model live trading competition**, featuring comprehensive market analysis, AI decision-making, **self-learning mechanism**, and professional Web monitoring interface.
An automated crypto futures trading system powered by **DeepSeek/Qwen AI**, supporting **Binance, Hyperliquid, and Aster DEX exchanges**, **multi-AI model live trading competition**, featuring comprehensive market analysis, AI decision-making, **self-learning mechanism**, and professional Web monitoring interface.
> ⚠️ **Risk Warning**: This system is experimental. AI auto-trading carries significant risks. Strongly recommended for learning/research purposes or testing with small amounts only!
@@ -23,9 +23,13 @@ Join our Telegram developer community to discuss, share ideas, and get support:
## 🆕 What's New (Latest Update)
### 🚀 Hyperliquid Exchange Support Added!
### 🚀 Multi-Exchange Support!
NOFX now supports **Hyperliquid** - a high-performance decentralized perpetual futures exchange!
NOFX now supports **three major exchanges**: Binance, Hyperliquid, and Aster DEX!
#### **Hyperliquid Exchange**
A high-performance decentralized perpetual futures exchange!
**Key Features:**
- ✅ Full trading support (long/short, leverage, stop-loss/take-profit)
@@ -48,6 +52,42 @@ NOFX now supports **Hyperliquid** - a high-performance decentralized perpetual f
See [Configuration Guide](#-alternative-using-hyperliquid-exchange) for details.
#### **Aster DEX Exchange** (NEW! v2.0.2)
A Binance-compatible decentralized perpetual futures exchange!
**Key Features:**
- ✅ Binance-style API (easy migration from Binance)
- ✅ Web3 wallet authentication (secure and decentralized)
- ✅ Full trading support with automatic precision handling
- ✅ Lower trading fees than CEX
- ✅ EVM-compatible (Ethereum, BSC, Polygon, etc.)
**Why Aster?**
- 🎯 **Binance-compatible API** - minimal code changes required
- 🔐 **API Wallet System** - separate trading wallet for security
- 💰 **Competitive fees** - lower than most centralized exchanges
- 🌐 **Multi-chain support** - trade on your preferred EVM chain
**Quick Start:**
1. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
2. Connect your main wallet and create an API wallet
3. Copy the API Signer address and Private Key
4. Set `"exchange": "aster"` in config.json
5. Add `"aster_user"`, `"aster_signer"`, and `"aster_private_key"`
---
## 📸 Screenshots
### 🏆 Competition Mode - Real-time AI Battle
![Competition Page](screenshots/competition-page.png)
*Multi-AI leaderboard with real-time performance comparison charts showing Qwen vs DeepSeek live trading battle*
### 📊 Trader Details - Complete Trading Dashboard
![Details Page](screenshots/details-page.png)
*Professional trading interface with equity curves, live positions, and AI decision logs with expandable input prompts & chain-of-thought reasoning*
---
## ✨ Core Features
@@ -442,6 +482,71 @@ cp config.json.example config.json
---
#### 🔶 Alternative: Using Aster DEX Exchange
**NOFX also supports Aster DEX** - a Binance-compatible decentralized perpetual futures exchange!
**Why Choose Aster?**
- 🎯 Binance-compatible API (easy migration)
- 🔐 API Wallet security system
- 💰 Lower trading fees
- 🌐 Multi-chain support (ETH, BSC, Polygon)
- 🌍 No KYC required
**Step 1**: Create Aster API Wallet
1. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
2. Connect your main wallet (MetaMask, WalletConnect, etc.)
3. Click "Create API Wallet"
4. **Save these 3 items immediately:**
- Main Wallet address (User)
- API Wallet address (Signer)
- API Wallet Private Key (⚠️ shown only once!)
**Step 2**: Configure `config.json` for Aster
```json
{
"traders": [
{
"id": "aster_deepseek",
"name": "Aster DeepSeek Trader",
"ai_model": "deepseek",
"exchange": "aster",
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
"deepseek_key": "sk-xxxxxxxxxxxxx",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
}
],
"use_default_coins": true,
"api_server_port": 8080,
"leverage": {
"btc_eth_leverage": 5,
"altcoin_leverage": 5
}
}
```
**Key Configuration Fields:**
- `"exchange": "aster"` - Set exchange to Aster
- `aster_user` - Your main wallet address
- `aster_signer` - API wallet address (from Step 1)
- `aster_private_key` - API wallet private key (without `0x` prefix)
**📖 For detailed setup instructions, see**: [Aster Integration Guide](ASTER_INTEGRATION.md)
**⚠️ Security Notes**:
- API wallet is separate from your main wallet (extra security layer)
- Never share your API private key
- You can revoke API wallet access anytime at [asterdex.com](https://www.asterdex.com/en/api-wallet)
---
#### ⚔️ Expert Mode: Multi-Trader Competition
For running multiple AI traders competing against each other:
@@ -1092,6 +1197,19 @@ This version fixes **critical calculation errors** in the historical trade recor
**Recommendation**: If you were running the system before this update, your historical statistics were inaccurate. After updating to v2.0.2, new trades will be calculated correctly.
### v2.0.2 (2025-10-29)
**Bug Fixes:**
- ✅ Fixed Aster exchange precision error (code -1111: "Precision is over the maximum defined for this asset")
- ✅ Improved price and quantity formatting to match exchange precision requirements
- ✅ Added detailed precision processing logs for debugging
- ✅ Enhanced all order functions (OpenLong, OpenShort, CloseLong, CloseShort, SetStopLoss, SetTakeProfit) with proper precision handling
**Technical Details:**
- Added `formatFloatWithPrecision` function to convert float64 to strings with correct precision
- Price and quantity parameters are now formatted according to exchange's `pricePrecision` and `quantityPrecision` specifications
- Trailing zeros are removed from formatted values to optimize API requests
### v2.0.1 (2025-10-29)
**Bug Fixes:**
@@ -1159,7 +1277,7 @@ Issues and Pull Requests are welcome!
---
**Last Updated**: 2025-10-29 (v2.0.2)
**Last Updated**: 2025-10-29 (v2.0.3)
**⚡ Explore the possibilities of quantitative trading with the power of AI!**
+175 -1
View File
@@ -9,7 +9,7 @@
---
Автоматизированная система торговли фьючерсами Binance на базе **DeepSeek/Qwen AI**, поддерживающая **конкуренцию нескольких AI-моделей в реальной торговле**, с полным анализом рынка, принятием решений AI, **механизмом самообучения** и профессиональным веб-интерфейсом мониторинга.
Автоматизированная система торговли криптовалютными фьючерсами на базе **DeepSeek/Qwen AI**, поддерживающая **Binance, Hyperliquid и Aster DEX биржи**, **конкуренцию нескольких AI-моделей в реальной торговле**, с полным анализом рынка, принятием решений AI, **механизмом самообучения** и профессиональным веб-интерфейсом мониторинга.
> ⚠️ **Предупреждение о рисках**: Эта система экспериментальная. Автоматическая торговля с AI несет значительные риски. Настоятельно рекомендуется использовать только для обучения/исследований или тестирования с небольшими суммами!
@@ -21,6 +21,75 @@
---
## 🆕 Последние обновления
### 🚀 Поддержка нескольких бирж!
NOFX теперь поддерживает **три основные биржи**: Binance, Hyperliquid и Aster DEX!
#### **Биржа Hyperliquid**
Высокопроизводительная децентрализованная биржа бессрочных фьючерсов!
**Ключевые особенности:**
- ✅ Полная поддержка торговли (лонг/шорт, плечо, стоп-лосс/тейк-профит)
- ✅ Автоматическая обработка точности (размер и цена ордера)
- ✅ Единый интерфейс трейдера (бесшовное переключение бирж)
- ✅ Поддержка мейннета и тестнета
- ✅ Не нужны API ключи - только приватный ключ Ethereum
**Почему Hyperliquid?**
- 🔥 Более низкие комиссии чем на централизованных биржах
- 🔒 Без хранения - вы контролируете свои средства
- ⚡ Быстрое исполнение с расчетом на цепи
- 🌍 Не нужна KYC
**Быстрый старт:**
1. Получите приватный ключ MetaMask (удалите префикс `0x`)
2. Установите `"exchange": "hyperliquid"` в config.json
3. Добавьте `"hyperliquid_private_key": "your_key"`
4. Начните торговать!
См. [Руководство по конфигурации](#-альтернатива-использование-биржи-hyperliquid).
#### **Биржа Aster DEX** (НОВОЕ! v2.0.2)
Децентрализованная биржа бессрочных фьючерсов, совместимая с Binance!
**Ключевые особенности:**
- ✅ API в стиле Binance (легкая миграция с Binance)
- ✅ Web3 аутентификация кошелька (безопасно и децентрализованно)
- ✅ Полная поддержка торговли с автоматической обработкой точности
- ✅ Более низкие комиссии за торговлю чем CEX
- ✅ Совместимость с EVM (Ethereum, BSC, Polygon и т.д.)
**Почему Aster?**
- 🎯 **API совместимый с Binance** - нужны минимальные изменения кода
- 🔐 **Система API кошелька** - отдельный торговый кошелек для безопасности
- 💰 **Конкурентные комиссии** - ниже чем большинство централизованных бирж
- 🌐 **Поддержка нескольких цепей** - торгуйте на вашей любимой EVM цепи
**Быстрый старт:**
1. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
2. Подключите основной кошелек и создайте API кошелек
3. Скопируйте адрес API Signer и приватный ключ
4. Установите `"exchange": "aster"` в config.json
5. Добавьте `"aster_user"`, `"aster_signer"` и `"aster_private_key"`
---
## 📸 Скриншоты
### 🏆 Режим конкуренции - Битва AI в реальном времени
![Страница конкуренции](screenshots/competition-page.png)
*Лидерборд с несколькими AI и графики сравнения производительности в реальном времени показывают битву Qwen против DeepSeek*
### 📊 Детали трейдера - Полная торговая панель
![Страница деталей](screenshots/details-page.png)
*Профессиональный торговый интерфейс с кривыми капитала, живыми позициями и логами решений AI с раскрываемыми входными промптами и цепочкой рассуждений*
---
## ✨ Основные возможности
### 🏆 Режим конкуренции нескольких AI
@@ -308,6 +377,111 @@ cp config.json.example config.json
---
#### 🔷 Альтернатива: Использование биржи Hyperliquid
**NOFX также поддерживает Hyperliquid** - децентрализованную биржу бессрочных фьючерсов. Чтобы использовать Hyperliquid вместо Binance:
**Шаг 1**: Получите приватный ключ Ethereum (для аутентификации Hyperliquid)
1. Откройте **MetaMask** (или любой Ethereum кошелек)
2. Экспортируйте приватный ключ
3. **Удалите префикс `0x`** из ключа
4. Пополните кошелек на [Hyperliquid](https://hyperliquid.xyz)
**Шаг 2**: Настройте `config.json` для Hyperliquid
```json
{
"traders": [
{
"id": "hyperliquid_trader",
"name": "My Hyperliquid Trader",
"ai_model": "deepseek",
"exchange": "hyperliquid",
"hyperliquid_private_key": "your_private_key_without_0x",
"hyperliquid_testnet": false,
"deepseek_key": "sk-xxxxxxxxxxxxx",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
}
],
"use_default_coins": true,
"api_server_port": 8080
}
```
**Ключевые отличия от конфигурации Binance:**
- Замените `binance_api_key` + `binance_secret_key` на `hyperliquid_private_key`
- Добавьте поле `"exchange": "hyperliquid"`
- Установите `hyperliquid_testnet: false` для мейннета (или `true` для тестнета)
**⚠️ Предупреждение безопасности**: Никогда не делитесь приватным ключом! Используйте отдельный кошелек для торговли, а не основной.
---
#### 🔶 Альтернатива: Использование биржи Aster DEX
**NOFX также поддерживает Aster DEX** - децентрализованную биржу бессрочных фьючерсов, совместимую с Binance!
**Почему выбрать Aster?**
- 🎯 API совместимый с Binance (легкая миграция)
- 🔐 Система безопасности API кошелька
- 💰 Более низкие комиссии за торговлю
- 🌐 Поддержка нескольких цепей (ETH, BSC, Polygon)
- 🌍 Не нужна KYC
**Шаг 1**: Создайте Aster API кошелек
1. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
2. Подключите основной кошелек (MetaMask, WalletConnect и т.д.)
3. Нажмите "Создать API кошелек"
4. **Сохраните эти 3 элемента немедленно:**
- Адрес основного кошелька (User)
- Адрес API кошелька (Signer)
- Приватный ключ API кошелька (⚠️ показывается только один раз!)
**Шаг 2**: Настройте `config.json` для Aster
```json
{
"traders": [
{
"id": "aster_deepseek",
"name": "Aster DeepSeek Trader",
"ai_model": "deepseek",
"exchange": "aster",
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
"deepseek_key": "sk-xxxxxxxxxxxxx",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
}
],
"use_default_coins": true,
"api_server_port": 8080,
"leverage": {
"btc_eth_leverage": 5,
"altcoin_leverage": 5
}
}
```
**Ключевые поля конфигурации:**
- `"exchange": "aster"` - Установите биржу на Aster
- `aster_user` - Адрес вашего основного кошелька
- `aster_signer` - Адрес API кошелька (из Шага 1)
- `aster_private_key` - Приватный ключ API кошелька (без префикса `0x`)
**⚠️ Примечания безопасности**:
- API кошелек отдельный от основного (дополнительный уровень безопасности)
- Никогда не делитесь приватным ключом API
- Вы можете отозвать доступ API кошелька в любое время на [asterdex.com](https://www.asterdex.com/en/api-wallet)
---
#### ⚔️ Экспертный режим: Конкуренция нескольких трейдеров
Для запуска нескольких AI трейдеров, конкурирующих друг с другом:
+175 -1
View File
@@ -9,7 +9,7 @@
---
Автоматизована система торгівлі ф'ючерсами Binance на базі **DeepSeek/Qwen AI**, що підтримує **змагання кількох AI-моделей у реальній торгівлі**, з повним аналізом ринку, прийняттям рішень AI, **механізмом самонавчання** та професійним веб-інтерфейсом моніторингу.
Автоматизована система торгівлі криптовалютними ф'ючерсами на базі **DeepSeek/Qwen AI**, що підтримує **Binance, Hyperliquid та Aster DEX біржі**, **змагання кількох AI-моделей у реальній торгівлі**, з повним аналізом ринку, прийняттям рішень AI, **механізмом самонавчання** та професійним веб-інтерфейсом моніторингу.
> ⚠️ **Попередження про ризики**: Ця система експериментальна. Автоматична торгівля з AI несе значні ризики. Наполегливо рекомендується використовувати лише для навчання/досліджень або тестування з невеликими сумами!
@@ -21,6 +21,75 @@
---
## 🆕 Останні оновлення
### 🚀 Підтримка кількох бірж!
NOFX тепер підтримує **три основні біржі**: Binance, Hyperliquid та Aster DEX!
#### **Біржа Hyperliquid**
Високопродуктивна децентралізована біржа безстрокових ф'ючерсів!
**Ключові особливості:**
- ✅ Повна підтримка торгівлі (лонг/шорт, плече, стоп-лосс/тейк-профіт)
- ✅ Автоматична обробка точності (розмір та ціна ордера)
- ✅ Єдиний інтерфейс трейдера (безшовне перемикання бірж)
- ✅ Підтримка мейннету та тестнету
- ✅ Не потрібні API ключі - тільки приватний ключ Ethereum
**Чому Hyperliquid?**
- 🔥 Нижчі комісії ніж на централізованих біржах
- 🔒 Без зберігання - ви контролюєте свої кошти
- ⚡ Швидке виконання з розрахунком на ланцюзі
- 🌍 Не потрібна KYC
**Швидкий старт:**
1. Отримайте приватний ключ MetaMask (видаліть префікс `0x`)
2. Встановіть `"exchange": "hyperliquid"` в config.json
3. Додайте `"hyperliquid_private_key": "your_key"`
4. Почніть торгувати!
Див. [Посібник з конфігурації](#-альтернатива-використання-біржі-hyperliquid).
#### **Біржа Aster DEX** (НОВЕ! v2.0.2)
Децентралізована біржа безстрокових ф'ючерсів, сумісна з Binance!
**Ключові особливості:**
- ✅ API в стилі Binance (легка міграція з Binance)
- ✅ Web3 автентифікація гаманця (безпечно та децентралізовано)
- ✅ Повна підтримка торгівлі з автоматичною обробкою точності
- ✅ Нижчі комісії за торгівлю ніж CEX
- ✅ Сумісність з EVM (Ethereum, BSC, Polygon тощо)
**Чому Aster?**
- 🎯 **API сумісний з Binance** - потрібні мінімальні зміни коду
- 🔐 **Система API гаманця** - окремий торговий гаманець для безпеки
- 💰 **Конкурентні комісії** - нижче ніж більшість централізованих бірж
- 🌐 **Підтримка кількох ланцюгів** - торгуйте на вашому улюбленому EVM ланцюзі
**Швидкий старт:**
1. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
2. Підключіть основний гаманець і створіть API гаманець
3. Скопіюйте адресу API Signer та приватний ключ
4. Встановіть `"exchange": "aster"` в config.json
5. Додайте `"aster_user"`, `"aster_signer"` та `"aster_private_key"`
---
## 📸 Скриншоти
### 🏆 Режим змагання - Битва AI в реальному часі
![Сторінка змагання](screenshots/competition-page.png)
*Лідерборд з кількома AI та графіки порівняння продуктивності в реальному часі показують битву Qwen проти DeepSeek*
### 📊 Деталі трейдера - Повна торгова панель
![Сторінка деталей](screenshots/details-page.png)
*Професійний торговий інтерфейс з кривими капіталу, живими позиціями та логами рішень AI з розкриваємими вхідними промптами та ланцюгом міркувань*
---
## ✨ Основні можливості
### 🏆 Режим змагання кількох AI
@@ -308,6 +377,111 @@ cp config.json.example config.json
---
#### 🔷 Альтернатива: Використання біржі Hyperliquid
**NOFX також підтримує Hyperliquid** - децентралізовану біржу безстрокових ф'ючерсів. Щоб використовувати Hyperliquid замість Binance:
**Крок 1**: Отримайте приватний ключ Ethereum (для автентифікації Hyperliquid)
1. Відкрийте **MetaMask** (або будь-який Ethereum гаманець)
2. Експортуйте приватний ключ
3. **Видаліть префікс `0x`** з ключа
4. Поповніть гаманець на [Hyperliquid](https://hyperliquid.xyz)
**Крок 2**: Налаштуйте `config.json` для Hyperliquid
```json
{
"traders": [
{
"id": "hyperliquid_trader",
"name": "My Hyperliquid Trader",
"ai_model": "deepseek",
"exchange": "hyperliquid",
"hyperliquid_private_key": "your_private_key_without_0x",
"hyperliquid_testnet": false,
"deepseek_key": "sk-xxxxxxxxxxxxx",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
}
],
"use_default_coins": true,
"api_server_port": 8080
}
```
**Ключові відмінності від конфігурації Binance:**
- Замініть `binance_api_key` + `binance_secret_key` на `hyperliquid_private_key`
- Додайте поле `"exchange": "hyperliquid"`
- Встановіть `hyperliquid_testnet: false` для мейннету (або `true` для тестнету)
**⚠️ Попередження безпеки**: Ніколи не діліться приватним ключем! Використовуйте окремий гаманець для торгівлі, а не основний.
---
#### 🔶 Альтернатива: Використання біржі Aster DEX
**NOFX також підтримує Aster DEX** - децентралізовану біржу безстрокових ф'ючерсів, сумісну з Binance!
**Чому обрати Aster?**
- 🎯 API сумісний з Binance (легка міграція)
- 🔐 Система безпеки API гаманця
- 💰 Нижчі комісії за торгівлю
- 🌐 Підтримка кількох ланцюгів (ETH, BSC, Polygon)
- 🌍 Не потрібна KYC
**Крок 1**: Створіть Aster API гаманець
1. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
2. Підключіть основний гаманець (MetaMask, WalletConnect тощо)
3. Натисніть "Створити API гаманець"
4. **Збережіть ці 3 елементи негайно:**
- Адреса основного гаманця (User)
- Адреса API гаманця (Signer)
- Приватний ключ API гаманця (⚠️ показується лише один раз!)
**Крок 2**: Налаштуйте `config.json` для Aster
```json
{
"traders": [
{
"id": "aster_deepseek",
"name": "Aster DeepSeek Trader",
"ai_model": "deepseek",
"exchange": "aster",
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
"deepseek_key": "sk-xxxxxxxxxxxxx",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
}
],
"use_default_coins": true,
"api_server_port": 8080,
"leverage": {
"btc_eth_leverage": 5,
"altcoin_leverage": 5
}
}
```
**Ключові поля конфігурації:**
- `"exchange": "aster"` - Встановіть біржу на Aster
- `aster_user` - Адреса вашого основного гаманця
- `aster_signer` - Адреса API гаманця (з Кроку 1)
- `aster_private_key` - Приватний ключ API гаманця (без префікса `0x`)
**⚠️ Примітки безпеки**:
- API гаманець окремий від основного (додатковий рівень безпеки)
- Ніколи не діліться приватним ключем API
- Ви можете відкликати доступ API гаманця в будь-який час на [asterdex.com](https://www.asterdex.com/en/api-wallet)
---
#### ⚔️ Експертний режим: Змагання кількох трейдерів
Для запуску кількох AI трейдерів, що змагаються один з одним:
+175 -1
View File
@@ -9,7 +9,7 @@
---
一个基于 **DeepSeek/Qwen AI**币安合约自动交易系统,支持**多AI模型实盘竞赛**,具备完整的市场分析、AI决策、**自我学习机制**和专业的Web监控界面。
一个基于 **DeepSeek/Qwen AI**加密货币期货自动交易系统,支持 **Binance、Hyperliquid和Aster DEX交易所****多AI模型实盘竞赛**,具备完整的市场分析、AI决策、**自我学习机制**和专业的Web监控界面。
> ⚠️ **风险提示**:本系统为实验性项目,AI自动交易存在重大风险,强烈建议仅用于学习研究或小额资金测试!
@@ -21,6 +21,75 @@
---
## 🆕 最新更新
### 🚀 多交易所支持!
NOFX现已支持**三大交易所**Binance、Hyperliquid和Aster DEX
#### **Hyperliquid交易所**
高性能的去中心化永续期货交易所!
**核心特性:**
- ✅ 完整交易支持(做多/做空、杠杆、止损/止盈)
- ✅ 自动精度处理(订单数量和价格)
- ✅ 统一trader接口(无缝切换交易所)
- ✅ 支持主网和测试网
- ✅ 无需API密钥 - 只需以太坊私钥
**为什么选择Hyperliquid**
- 🔥 比中心化交易所手续费更低
- 🔒 非托管 - 你掌控自己的资金
- ⚡ 快速执行与链上结算
- 🌍 无需KYC
**快速开始:**
1. 获取你的MetaMask私钥(去掉`0x`前缀)
2. 在config.json中设置`"exchange": "hyperliquid"`
3. 添加`"hyperliquid_private_key": "your_key"`
4. 开始交易!
详见[配置指南](#-备选使用hyperliquid交易所)。
#### **Aster DEX交易所**(新!v2.0.2
兼容Binance的去中心化永续期货交易所!
**核心特性:**
- ✅ Binance风格API(从Binance轻松迁移)
- ✅ Web3钱包认证(安全且去中心化)
- ✅ 完整交易支持,自动精度处理
- ✅ 比中心化交易所手续费更低
- ✅ 兼容EVM(以太坊、BSC、Polygon等)
**为什么选择Aster**
- 🎯 **兼容Binance API** - 需要最少的代码修改
- 🔐 **API钱包系统** - 独立交易钱包提升安全性
- 💰 **有竞争力的手续费** - 比大多数中心化交易所更低
- 🌐 **多链支持** - 在你喜欢的EVM链上交易
**快速开始:**
1. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet)
2. 连接你的主钱包并创建API钱包
3. 复制API Signer地址和私钥
4. 在config.json中设置`"exchange": "aster"`
5. 添加`"aster_user"``"aster_signer"``"aster_private_key"`
---
## 📸 系统截图
### 🏆 竞赛模式 - AI实时对战
![竞赛页面](screenshots/competition-page.png)
*多AI排行榜和实时性能对比图表,展示Qwen vs DeepSeek实时交易对战*
### 📊 交易详情 - 完整交易仪表盘
![详情页面](screenshots/details-page.png)
*专业交易界面,包含权益曲线、实时持仓、AI决策日志,支持展开查看输入提示词和AI思维链推理过程*
---
## ✨ 核心特性
### 🏆 多AI竞赛模式
@@ -371,6 +440,111 @@ cp config.json.example config.json
---
#### 🔷 备选:使用Hyperliquid交易所
**NOFX也支持Hyperliquid** - 去中心化永续期货交易所。使用Hyperliquid而非Binance
**步骤1**:获取以太坊私钥(用于Hyperliquid身份验证)
1. 打开**MetaMask**(或任何以太坊钱包)
2. 导出你的私钥
3. **去掉`0x`前缀**
4. 在[Hyperliquid](https://hyperliquid.xyz)上为钱包充值
**步骤2**:为Hyperliquid配置`config.json`
```json
{
"traders": [
{
"id": "hyperliquid_trader",
"name": "My Hyperliquid Trader",
"ai_model": "deepseek",
"exchange": "hyperliquid",
"hyperliquid_private_key": "your_private_key_without_0x",
"hyperliquid_testnet": false,
"deepseek_key": "sk-xxxxxxxxxxxxx",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
}
],
"use_default_coins": true,
"api_server_port": 8080
}
```
**与Binance配置的关键区别:**
-`hyperliquid_private_key`替换`binance_api_key` + `binance_secret_key`
- 添加`"exchange": "hyperliquid"`字段
- 设置`hyperliquid_testnet: false`用于主网(或`true`用于测试网)
**⚠️ 安全警告**:切勿分享你的私钥!使用专门的钱包进行交易,而非主钱包。
---
#### 🔶 备选:使用Aster DEX交易所
**NOFX也支持Aster DEX** - 兼容Binance的去中心化永续期货交易所!
**为什么选择Aster**
- 🎯 兼容Binance API(轻松迁移)
- 🔐 API钱包安全系统
- 💰 更低的交易手续费
- 🌐 多链支持(ETH、BSC、Polygon
- 🌍 无需KYC
**步骤1**:创建Aster API钱包
1. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet)
2. 连接你的主钱包(MetaMask、WalletConnect等)
3. 点击"创建API钱包"
4. **立即保存这3项:**
- 主钱包地址(User
- API钱包地址(Signer
- API钱包私钥(⚠️ 仅显示一次!)
**步骤2**:为Aster配置`config.json`
```json
{
"traders": [
{
"id": "aster_deepseek",
"name": "Aster DeepSeek Trader",
"ai_model": "deepseek",
"exchange": "aster",
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
"deepseek_key": "sk-xxxxxxxxxxxxx",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
}
],
"use_default_coins": true,
"api_server_port": 8080,
"leverage": {
"btc_eth_leverage": 5,
"altcoin_leverage": 5
}
}
```
**关键配置字段:**
- `"exchange": "aster"` - 设置交易所为Aster
- `aster_user` - 你的主钱包地址
- `aster_signer` - API钱包地址(来自步骤1
- `aster_private_key` - API钱包私钥(去掉`0x`前缀)
**⚠️ 安全提示**
- API钱包与主钱包分离(额外的安全层)
- 切勿分享API私钥
- 你可以随时在[asterdex.com](https://www.asterdex.com/en/api-wallet)撤销API钱包访问
---
#### ⚔️ 专家模式:多Trader竞赛
用于运行多个AI trader相互竞争:
+29
View File
@@ -34,6 +34,25 @@
"custom_model_name": "gpt-4o",
"initial_balance": 1000,
"scan_interval_minutes": 3
}
{
"id": "aster_deepseek",
"name": "Aster DeepSeek Trader",
"ai_model": "deepseek",
"exchange": "aster",
// 注意请仔细阅读这三个提示 请进入https://www.asterdex.com/en/api-wallet网站 -> 选择专业api -> 创建新api获取以下信息
// user: 主钱包地址 (登录地址/连接到aster的钱包地址)
// signer: API钱包地址 (点击生成地址后生成的地址)
// privateKey: API钱包私钥 (生成地址对应的私钥)
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
"aster_private_key": "your_aster_api_wallet_private_key_without_0x_prefix",
"deepseek_key": "your_deepseek_api_key",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
}
],
"leverage": {
@@ -41,6 +60,16 @@
"altcoin_leverage": 5
},
"use_default_coins": true,
"default_coins": [
"BTCUSDT",
"ETHUSDT",
"SOLUSDT",
"BNBUSDT",
"XRPUSDT",
"DOGEUSDT",
"ADAUSDT",
"HYPEUSDT",
],
"coin_pool_api_url": "",
"oi_top_api_url": "",
"api_server_port": 8080,
+26 -2
View File
@@ -24,6 +24,11 @@ type TraderConfig struct {
HyperliquidPrivateKey string `json:"hyperliquid_private_key,omitempty"`
HyperliquidTestnet bool `json:"hyperliquid_testnet,omitempty"`
// Aster配置
AsterUser string `json:"aster_user,omitempty"` // Aster主钱包地址
AsterSigner string `json:"aster_signer,omitempty"` // Aster API钱包地址
AsterPrivateKey string `json:"aster_private_key,omitempty"` // Aster API钱包私钥
// AI配置
QwenKey string `json:"qwen_key,omitempty"`
DeepSeekKey string `json:"deepseek_key,omitempty"`
@@ -47,6 +52,7 @@ type LeverageConfig struct {
type Config struct {
Traders []TraderConfig `json:"traders"`
UseDefaultCoins bool `json:"use_default_coins"` // 是否使用默认主流币种列表
DefaultCoins []string `json:"default_coins"` // 默认主流币种池
CoinPoolAPIURL string `json:"coin_pool_api_url"`
OITopAPIURL string `json:"oi_top_api_url"`
APIServerPort int `json:"api_server_port"`
@@ -73,6 +79,20 @@ func LoadConfig(filename string) (*Config, error) {
config.UseDefaultCoins = true
}
// 设置默认币种池
if len(config.DefaultCoins) == 0 {
config.DefaultCoins = []string{
"BTCUSDT",
"ETHUSDT",
"SOLUSDT",
"BNBUSDT",
"XRPUSDT",
"DOGEUSDT",
"ADAUSDT",
"HYPEUSDT",
}
}
// 验证配置
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("配置验证失败: %w", err)
@@ -108,8 +128,8 @@ func (c *Config) Validate() error {
if trader.Exchange == "" {
trader.Exchange = "binance" // 默认使用币安
}
if trader.Exchange != "binance" && trader.Exchange != "hyperliquid" {
return fmt.Errorf("trader[%d]: exchange必须是 'binance' 'hyperliquid'", i)
if trader.Exchange != "binance" && trader.Exchange != "hyperliquid" && trader.Exchange != "aster" {
return fmt.Errorf("trader[%d]: exchange必须是 'binance', 'hyperliquid' 或 'aster'", i)
}
// 根据平台验证对应的密钥
@@ -121,6 +141,10 @@ func (c *Config) Validate() error {
if trader.HyperliquidPrivateKey == "" {
return fmt.Errorf("trader[%d]: 使用Hyperliquid时必须配置hyperliquid_private_key", i)
}
} else if trader.Exchange == "aster" {
if trader.AsterUser == "" || trader.AsterSigner == "" || trader.AsterPrivateKey == "" {
return fmt.Errorf("trader[%d]: 使用Aster时必须配置aster_user, aster_signer和aster_private_key", i)
}
}
if trader.AIModel == "qwen" && trader.QwenKey == "" {
+5 -5
View File
@@ -97,7 +97,7 @@ func GetFullDecision(ctx *Context) (*FullDecision, error) {
}
// 2. 构建 System Prompt(固定规则)和 User Prompt(动态数据)
systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity)
systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
userPrompt := buildUserPrompt(ctx)
// 3. 调用AI API(使用 system + user prompt
@@ -200,7 +200,7 @@ func calculateMaxCandidates(ctx *Context) int {
}
// buildSystemPrompt 构建 System Prompt(固定规则,可缓存)
func buildSystemPrompt(accountEquity float64) string {
func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage int) string {
var sb strings.Builder
// === 核心使命 ===
@@ -222,8 +222,8 @@ func buildSystemPrompt(accountEquity float64) string {
sb.WriteString("# ⚖️ 硬约束(风险控制)\n\n")
sb.WriteString("1. **风险回报比**: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n")
sb.WriteString("2. **最多持仓**: 3个币种(质量>数量)\n")
sb.WriteString(fmt.Sprintf("3. **单币仓位**: 山寨%.0f-%.0f U(20x杠杆) | BTC/ETH %.0f-%.0f U(50x杠杆)\n",
accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10))
sb.WriteString(fmt.Sprintf("3. **单币仓位**: 山寨%.0f-%.0f U(%dx杠杆) | BTC/ETH %.0f-%.0f U(%dx杠杆)\n",
accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage))
sb.WriteString("4. **保证金**: 总使用率 ≤ 90%\n\n")
// === 做空激励 ===
@@ -296,7 +296,7 @@ func buildSystemPrompt(accountEquity float64) string {
sb.WriteString("简洁分析你的思考过程\n\n")
sb.WriteString("**第二步: JSON决策数组**\n\n")
sb.WriteString("```json\n[\n")
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": 50, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300, \"reasoning\": \"下跌趋势+MACD死叉\"},\n", accountEquity*5))
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300, \"reasoning\": \"下跌趋势+MACD死叉\"},\n", btcEthLeverage, accountEquity*5))
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\", \"reasoning\": \"止盈离场\"}\n")
sb.WriteString("]\n```\n\n")
sb.WriteString("**字段说明**:\n")
+1 -1
View File
@@ -20,7 +20,7 @@ services:
networks:
- nofx-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
+4 -1
View File
@@ -34,10 +34,13 @@ func main() {
log.Printf("✓ 配置加载成功,共%d个trader参赛", len(cfg.Traders))
fmt.Println()
// 设置默认主流币种列表
pool.SetDefaultCoins(cfg.DefaultCoins)
// 设置是否使用默认主流币种
pool.SetUseDefaultCoins(cfg.UseDefaultCoins)
if cfg.UseDefaultCoins {
log.Printf("✓ 已启用默认主流币种列表(BTC、ETH、SOL、BNB、XRP、DOGE、ADA、HYPE")
log.Printf("✓ 已启用默认主流币种列表(共%d个币种): %v", len(cfg.DefaultCoins), cfg.DefaultCoins)
}
// 设置币种池API URL
+3
View File
@@ -41,6 +41,9 @@ func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string,
BinanceSecretKey: cfg.BinanceSecretKey,
HyperliquidPrivateKey: cfg.HyperliquidPrivateKey,
HyperliquidTestnet: cfg.HyperliquidTestnet,
AsterUser: cfg.AsterUser,
AsterSigner: cfg.AsterSigner,
AsterPrivateKey: cfg.AsterPrivateKey,
CoinPoolAPIURL: coinPoolURL,
UseQwen: cfg.AIModel == "qwen",
DeepSeekKey: cfg.DeepSeekKey,
+19 -2
View File
@@ -27,6 +27,7 @@ type Config struct {
BaseURL string
Model string
Timeout time.Duration
UseFullURL bool // 是否使用完整URL(不添加/chat/completions
}
// 默认配置
@@ -58,7 +59,16 @@ func SetQwenAPIKey(apiKey, secretKey string) {
func SetCustomAPI(apiURL, apiKey, modelName string) {
defaultConfig.Provider = ProviderCustom
defaultConfig.APIKey = apiKey
defaultConfig.BaseURL = apiURL
// 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions
if strings.HasSuffix(apiURL, "#") {
defaultConfig.BaseURL = strings.TrimSuffix(apiURL, "#")
defaultConfig.UseFullURL = true
} else {
defaultConfig.BaseURL = apiURL
defaultConfig.UseFullURL = false
}
defaultConfig.Model = modelName
defaultConfig.Timeout = 120 * time.Second
}
@@ -147,7 +157,14 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
}
// 创建HTTP请求
url := fmt.Sprintf("%s/chat/completions", defaultConfig.BaseURL)
var url string
if defaultConfig.UseFullURL {
// 使用完整URL,不添加/chat/completions
url = defaultConfig.BaseURL
} else {
// 默认行为:添加/chat/completions
url = fmt.Sprintf("%s/chat/completions", defaultConfig.BaseURL)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("创建请求失败: %w", err)
+9 -1
View File
@@ -12,7 +12,7 @@ import (
"time"
)
// defaultMainstreamCoins 默认主流币种池(当AI500和OI Top都失败时使用
// defaultMainstreamCoins 默认主流币种池(从配置文件读取
var defaultMainstreamCoins = []string{
"BTCUSDT",
"ETHUSDT",
@@ -83,6 +83,14 @@ func SetUseDefaultCoins(useDefault bool) {
coinPoolConfig.UseDefaultCoins = useDefault
}
// SetDefaultCoins 设置默认主流币种列表
func SetDefaultCoins(coins []string) {
if len(coins) > 0 {
defaultMainstreamCoins = coins
log.Printf("✓ 已设置默认币种池(共%d个币种): %v", len(coins), coins)
}
}
// GetCoinPool 获取币种池列表(带重试和缓存机制)
func GetCoinPool() ([]CoinInfo, error) {
// 优先检查是否启用默认币种列表
Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

+937
View File
@@ -0,0 +1,937 @@
package trader
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math"
"math/big"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
// AsterTrader Aster交易平台实现
type AsterTrader struct {
ctx context.Context
user string // 主钱包地址 (ERC20)
signer string // API钱包地址
privateKey *ecdsa.PrivateKey // API钱包私钥
client *http.Client
baseURL string
// 缓存交易对精度信息
symbolPrecision map[string]SymbolPrecision
mu sync.RWMutex
}
// SymbolPrecision 交易对精度信息
type SymbolPrecision struct {
PricePrecision int
QuantityPrecision int
TickSize float64 // 价格步进值
StepSize float64 // 数量步进值
}
// NewAsterTrader 创建Aster交易器
// user: 主钱包地址 (登录地址)
// signer: API钱包地址 (从 https://www.asterdex.com/en/api-wallet 获取)
// privateKey: API钱包私钥 (从 https://www.asterdex.com/en/api-wallet 获取)
func NewAsterTrader(user, signer, privateKeyHex string) (*AsterTrader, error) {
// 解析私钥
privKey, err := crypto.HexToECDSA(strings.TrimPrefix(privateKeyHex, "0x"))
if err != nil {
return nil, fmt.Errorf("解析私钥失败: %w", err)
}
return &AsterTrader{
ctx: context.Background(),
user: user,
signer: signer,
privateKey: privKey,
symbolPrecision: make(map[string]SymbolPrecision),
client: &http.Client{
Timeout: 30 * time.Second, // 增加到30秒
Transport: &http.Transport{
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
IdleConnTimeout: 90 * time.Second,
},
},
baseURL: "https://fapi.asterdex.com",
}, nil
}
// genNonce 生成微秒时间戳
func (t *AsterTrader) genNonce() uint64 {
return uint64(time.Now().UnixMicro())
}
// getPrecision 获取交易对精度信息
func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) {
t.mu.RLock()
if prec, ok := t.symbolPrecision[symbol]; ok {
t.mu.RUnlock()
return prec, nil
}
t.mu.RUnlock()
// 获取交易所信息
resp, err := t.client.Get(t.baseURL + "/fapi/v3/exchangeInfo")
if err != nil {
return SymbolPrecision{}, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var info struct {
Symbols []struct {
Symbol string `json:"symbol"`
PricePrecision int `json:"pricePrecision"`
QuantityPrecision int `json:"quantityPrecision"`
Filters []map[string]interface{} `json:"filters"`
} `json:"symbols"`
}
if err := json.Unmarshal(body, &info); err != nil {
return SymbolPrecision{}, err
}
// 缓存所有交易对的精度
t.mu.Lock()
for _, s := range info.Symbols {
prec := SymbolPrecision{
PricePrecision: s.PricePrecision,
QuantityPrecision: s.QuantityPrecision,
}
// 解析filters获取tickSize和stepSize
for _, filter := range s.Filters {
filterType, _ := filter["filterType"].(string)
switch filterType {
case "PRICE_FILTER":
if tickSizeStr, ok := filter["tickSize"].(string); ok {
prec.TickSize, _ = strconv.ParseFloat(tickSizeStr, 64)
}
case "LOT_SIZE":
if stepSizeStr, ok := filter["stepSize"].(string); ok {
prec.StepSize, _ = strconv.ParseFloat(stepSizeStr, 64)
}
}
}
t.symbolPrecision[s.Symbol] = prec
}
t.mu.Unlock()
if prec, ok := t.symbolPrecision[symbol]; ok {
return prec, nil
}
return SymbolPrecision{}, fmt.Errorf("未找到交易对 %s 的精度信息", symbol)
}
// roundToTickSize 将价格/数量四舍五入到tick size/step size的整数倍
func roundToTickSize(value float64, tickSize float64) float64 {
if tickSize <= 0 {
return value
}
// 计算有多少个tick size
steps := value / tickSize
// 四舍五入到最近的整数
roundedSteps := math.Round(steps)
// 乘回tick size
return roundedSteps * tickSize
}
// formatPrice 格式化价格到正确精度和tick size
func (t *AsterTrader) formatPrice(symbol string, price float64) (float64, error) {
prec, err := t.getPrecision(symbol)
if err != nil {
return 0, err
}
// 优先使用tick size,确保价格是tick size的整数倍
if prec.TickSize > 0 {
return roundToTickSize(price, prec.TickSize), nil
}
// 如果没有tick size,则按精度四舍五入
multiplier := math.Pow10(prec.PricePrecision)
return math.Round(price*multiplier) / multiplier, nil
}
// formatQuantity 格式化数量到正确精度和step size
func (t *AsterTrader) formatQuantity(symbol string, quantity float64) (float64, error) {
prec, err := t.getPrecision(symbol)
if err != nil {
return 0, err
}
// 优先使用step size,确保数量是step size的整数倍
if prec.StepSize > 0 {
return roundToTickSize(quantity, prec.StepSize), nil
}
// 如果没有step size,则按精度四舍五入
multiplier := math.Pow10(prec.QuantityPrecision)
return math.Round(quantity*multiplier) / multiplier, nil
}
// formatFloatWithPrecision 将浮点数格式化为指定精度的字符串(去除末尾的0)
func (t *AsterTrader) formatFloatWithPrecision(value float64, precision int) string {
// 使用指定精度格式化
formatted := strconv.FormatFloat(value, 'f', precision, 64)
// 去除末尾的0和小数点(如果有)
formatted = strings.TrimRight(formatted, "0")
formatted = strings.TrimRight(formatted, ".")
return formatted
}
// normalizeAndStringify 对参数进行规范化并序列化为JSON字符串(按key排序)
func (t *AsterTrader) normalizeAndStringify(params map[string]interface{}) (string, error) {
normalized, err := t.normalize(params)
if err != nil {
return "", err
}
bs, err := json.Marshal(normalized)
if err != nil {
return "", err
}
return string(bs), nil
}
// normalize 递归规范化参数(按key排序,所有值转为字符串)
func (t *AsterTrader) normalize(v interface{}) (interface{}, error) {
switch val := v.(type) {
case map[string]interface{}:
keys := make([]string, 0, len(val))
for k := range val {
keys = append(keys, k)
}
sort.Strings(keys)
newMap := make(map[string]interface{}, len(keys))
for _, k := range keys {
nv, err := t.normalize(val[k])
if err != nil {
return nil, err
}
newMap[k] = nv
}
return newMap, nil
case []interface{}:
out := make([]interface{}, 0, len(val))
for _, it := range val {
nv, err := t.normalize(it)
if err != nil {
return nil, err
}
out = append(out, nv)
}
return out, nil
case string:
return val, nil
case int:
return fmt.Sprintf("%d", val), nil
case int64:
return fmt.Sprintf("%d", val), nil
case float64:
return fmt.Sprintf("%v", val), nil
case bool:
return fmt.Sprintf("%v", val), nil
default:
// 其他类型转为字符串
return fmt.Sprintf("%v", val), nil
}
}
// sign 对请求参数进行签名
func (t *AsterTrader) sign(params map[string]interface{}, nonce uint64) error {
// 添加时间戳和接收窗口
params["recvWindow"] = "50000"
params["timestamp"] = strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
// 规范化参数为JSON字符串
jsonStr, err := t.normalizeAndStringify(params)
if err != nil {
return err
}
// ABI编码: (string, address, address, uint256)
addrUser := common.HexToAddress(t.user)
addrSigner := common.HexToAddress(t.signer)
nonceBig := new(big.Int).SetUint64(nonce)
tString, _ := abi.NewType("string", "", nil)
tAddress, _ := abi.NewType("address", "", nil)
tUint256, _ := abi.NewType("uint256", "", nil)
arguments := abi.Arguments{
{Type: tString},
{Type: tAddress},
{Type: tAddress},
{Type: tUint256},
}
packed, err := arguments.Pack(jsonStr, addrUser, addrSigner, nonceBig)
if err != nil {
return fmt.Errorf("ABI编码失败: %w", err)
}
// Keccak256哈希
hash := crypto.Keccak256(packed)
// 以太坊签名消息前缀
prefixedMsg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(hash), hash)
msgHash := crypto.Keccak256Hash([]byte(prefixedMsg))
// ECDSA签名
sig, err := crypto.Sign(msgHash.Bytes(), t.privateKey)
if err != nil {
return fmt.Errorf("签名失败: %w", err)
}
// 将v从0/1转换为27/28
if len(sig) != 65 {
return fmt.Errorf("签名长度异常: %d", len(sig))
}
sig[64] += 27
// 添加签名参数
params["user"] = t.user
params["signer"] = t.signer
params["signature"] = "0x" + hex.EncodeToString(sig)
params["nonce"] = nonce
return nil
}
// request 发送HTTP请求(带重试机制)
func (t *AsterTrader) request(method, endpoint string, params map[string]interface{}) ([]byte, error) {
const maxRetries = 3
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
// 每次重试都生成新的nonce和签名
nonce := t.genNonce()
paramsCopy := make(map[string]interface{})
for k, v := range params {
paramsCopy[k] = v
}
// 签名
if err := t.sign(paramsCopy, nonce); err != nil {
return nil, err
}
body, err := t.doRequest(method, endpoint, paramsCopy)
if err == nil {
return body, nil
}
lastErr = err
// 如果是网络超时或临时错误,重试
if strings.Contains(err.Error(), "timeout") ||
strings.Contains(err.Error(), "connection reset") ||
strings.Contains(err.Error(), "EOF") {
if attempt < maxRetries {
waitTime := time.Duration(attempt) * time.Second
time.Sleep(waitTime)
continue
}
}
// 其他错误(如400/401等)不重试
return nil, err
}
return nil, fmt.Errorf("请求失败(已重试%d次): %w", maxRetries, lastErr)
}
// doRequest 执行实际的HTTP请求
func (t *AsterTrader) doRequest(method, endpoint string, params map[string]interface{}) ([]byte, error) {
fullURL := t.baseURL + endpoint
method = strings.ToUpper(method)
switch method {
case "POST":
// POST请求:参数放在表单body中
form := url.Values{}
for k, v := range params {
form.Set(k, fmt.Sprintf("%v", v))
}
req, err := http.NewRequest("POST", fullURL, strings.NewReader(form.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
return body, nil
case "GET", "DELETE":
// GET/DELETE请求:参数放在querystring中
q := url.Values{}
for k, v := range params {
q.Set(k, fmt.Sprintf("%v", v))
}
u, _ := url.Parse(fullURL)
u.RawQuery = q.Encode()
req, err := http.NewRequest(method, u.String(), nil)
if err != nil {
return nil, err
}
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
return body, nil
default:
return nil, fmt.Errorf("不支持的HTTP方法: %s", method)
}
}
// GetBalance 获取账户余额
func (t *AsterTrader) GetBalance() (map[string]interface{}, error) {
params := make(map[string]interface{})
body, err := t.request("GET", "/fapi/v3/balance", params)
if err != nil {
return nil, err
}
var balances []map[string]interface{}
if err := json.Unmarshal(body, &balances); err != nil {
return nil, err
}
// 查找USDT余额
totalBalance := 0.0
availableBalance := 0.0
crossUnPnl := 0.0
for _, bal := range balances {
if asset, ok := bal["asset"].(string); ok && asset == "USDT" {
if wb, ok := bal["balance"].(string); ok {
totalBalance, _ = strconv.ParseFloat(wb, 64)
}
if avail, ok := bal["availableBalance"].(string); ok {
availableBalance, _ = strconv.ParseFloat(avail, 64)
}
if unpnl, ok := bal["crossUnPnl"].(string); ok {
crossUnPnl, _ = strconv.ParseFloat(unpnl, 64)
}
break
}
}
// 返回与Binance相同的字段名,确保AutoTrader能正确解析
return map[string]interface{}{
"totalWalletBalance": totalBalance,
"availableBalance": availableBalance,
"totalUnrealizedProfit": crossUnPnl,
}, nil
}
// GetPositions 获取持仓信息
func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) {
params := make(map[string]interface{})
body, err := t.request("GET", "/fapi/v3/positionRisk", params)
if err != nil {
return nil, err
}
var positions []map[string]interface{}
if err := json.Unmarshal(body, &positions); err != nil {
return nil, err
}
result := []map[string]interface{}{}
for _, pos := range positions {
posAmtStr, ok := pos["positionAmt"].(string)
if !ok {
continue
}
posAmt, _ := strconv.ParseFloat(posAmtStr, 64)
if posAmt == 0 {
continue // 跳过空仓位
}
entryPrice, _ := strconv.ParseFloat(pos["entryPrice"].(string), 64)
markPrice, _ := strconv.ParseFloat(pos["markPrice"].(string), 64)
unRealizedProfit, _ := strconv.ParseFloat(pos["unRealizedProfit"].(string), 64)
leverageVal, _ := strconv.ParseFloat(pos["leverage"].(string), 64)
liquidationPrice, _ := strconv.ParseFloat(pos["liquidationPrice"].(string), 64)
// 判断方向(与Binance一致)
side := "long"
if posAmt < 0 {
side = "short"
posAmt = -posAmt
}
// 返回与Binance相同的字段名
result = append(result, map[string]interface{}{
"symbol": pos["symbol"],
"side": side,
"positionAmt": posAmt,
"entryPrice": entryPrice,
"markPrice": markPrice,
"unRealizedProfit": unRealizedProfit,
"leverage": leverageVal,
"liquidationPrice": liquidationPrice,
})
}
return result, nil
}
// OpenLong 开多单
func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
// 先设置杠杆
if err := t.SetLeverage(symbol, leverage); err != nil {
return nil, fmt.Errorf("设置杠杆失败: %w", err)
}
// 获取当前价格
price, err := t.GetMarketPrice(symbol)
if err != nil {
return nil, err
}
// 使用限价单模拟市价单(价格设置得稍高一些以确保成交)
limitPrice := price * 1.01
// 格式化价格和数量到正确精度
formattedPrice, err := t.formatPrice(symbol, limitPrice)
if err != nil {
return nil, err
}
formattedQty, err := t.formatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 获取精度信息
prec, err := t.getPrecision(symbol)
if err != nil {
return nil, err
}
// 转换为字符串,使用正确的精度格式
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision)
params := map[string]interface{}{
"symbol": symbol,
"positionSide": "BOTH",
"type": "LIMIT",
"side": "BUY",
"timeInForce": "GTC",
"quantity": qtyStr,
"price": priceStr,
}
body, err := t.request("POST", "/fapi/v3/order", params)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return result, nil
}
// OpenShort 开空单
func (t *AsterTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
// 先设置杠杆
if err := t.SetLeverage(symbol, leverage); err != nil {
return nil, fmt.Errorf("设置杠杆失败: %w", err)
}
// 获取当前价格
price, err := t.GetMarketPrice(symbol)
if err != nil {
return nil, err
}
// 使用限价单模拟市价单(价格设置得稍低一些以确保成交)
limitPrice := price * 0.99
// 格式化价格和数量到正确精度
formattedPrice, err := t.formatPrice(symbol, limitPrice)
if err != nil {
return nil, err
}
formattedQty, err := t.formatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 获取精度信息
prec, err := t.getPrecision(symbol)
if err != nil {
return nil, err
}
// 转换为字符串,使用正确的精度格式
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision)
params := map[string]interface{}{
"symbol": symbol,
"positionSide": "BOTH",
"type": "LIMIT",
"side": "SELL",
"timeInForce": "GTC",
"quantity": qtyStr,
"price": priceStr,
}
body, err := t.request("POST", "/fapi/v3/order", params)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return result, nil
}
// CloseLong 平多单
func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) {
// 如果数量为0,获取当前持仓数量
if quantity == 0 {
positions, err := t.GetPositions()
if err != nil {
return nil, err
}
for _, pos := range positions {
if pos["symbol"] == symbol && pos["side"] == "long" {
quantity = pos["positionAmt"].(float64)
break
}
}
if quantity == 0 {
return nil, fmt.Errorf("没有找到 %s 的多仓", symbol)
}
log.Printf(" 📊 获取到多仓数量: %.8f", quantity)
}
price, err := t.GetMarketPrice(symbol)
if err != nil {
return nil, err
}
limitPrice := price * 0.99
// 格式化价格和数量到正确精度
formattedPrice, err := t.formatPrice(symbol, limitPrice)
if err != nil {
return nil, err
}
formattedQty, err := t.formatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 获取精度信息
prec, err := t.getPrecision(symbol)
if err != nil {
return nil, err
}
// 转换为字符串,使用正确的精度格式
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision)
params := map[string]interface{}{
"symbol": symbol,
"positionSide": "BOTH",
"type": "LIMIT",
"side": "SELL",
"timeInForce": "GTC",
"quantity": qtyStr,
"price": priceStr,
}
body, err := t.request("POST", "/fapi/v3/order", params)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
log.Printf("✓ 平多仓成功: %s 数量: %s", symbol, qtyStr)
return result, nil
}
// CloseShort 平空单
func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) {
// 如果数量为0,获取当前持仓数量
if quantity == 0 {
positions, err := t.GetPositions()
if err != nil {
return nil, err
}
for _, pos := range positions {
if pos["symbol"] == symbol && pos["side"] == "short" {
// Aster的GetPositions已经将空仓数量转换为正数,直接使用
quantity = pos["positionAmt"].(float64)
break
}
}
if quantity == 0 {
return nil, fmt.Errorf("没有找到 %s 的空仓", symbol)
}
log.Printf(" 📊 获取到空仓数量: %.8f", quantity)
}
price, err := t.GetMarketPrice(symbol)
if err != nil {
return nil, err
}
limitPrice := price * 1.01
// 格式化价格和数量到正确精度
formattedPrice, err := t.formatPrice(symbol, limitPrice)
if err != nil {
return nil, err
}
formattedQty, err := t.formatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 获取精度信息
prec, err := t.getPrecision(symbol)
if err != nil {
return nil, err
}
// 转换为字符串,使用正确的精度格式
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision)
params := map[string]interface{}{
"symbol": symbol,
"positionSide": "BOTH",
"type": "LIMIT",
"side": "BUY",
"timeInForce": "GTC",
"quantity": qtyStr,
"price": priceStr,
}
body, err := t.request("POST", "/fapi/v3/order", params)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
log.Printf("✓ 平空仓成功: %s 数量: %s", symbol, qtyStr)
return result, nil
}
// SetLeverage 设置杠杆倍数
func (t *AsterTrader) SetLeverage(symbol string, leverage int) error {
params := map[string]interface{}{
"symbol": symbol,
"leverage": leverage,
}
_, err := t.request("POST", "/fapi/v3/leverage", params)
return err
}
// GetMarketPrice 获取市场价格
func (t *AsterTrader) GetMarketPrice(symbol string) (float64, error) {
// 使用ticker接口获取当前价格
resp, err := t.client.Get(fmt.Sprintf("%s/fapi/v3/ticker/price?symbol=%s", t.baseURL, symbol))
if err != nil {
return 0, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return 0, err
}
priceStr, ok := result["price"].(string)
if !ok {
return 0, errors.New("无法获取价格")
}
return strconv.ParseFloat(priceStr, 64)
}
// SetStopLoss 设置止损
func (t *AsterTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
side := "SELL"
if positionSide == "SHORT" {
side = "BUY"
}
// 格式化价格和数量到正确精度
formattedPrice, err := t.formatPrice(symbol, stopPrice)
if err != nil {
return err
}
formattedQty, err := t.formatQuantity(symbol, quantity)
if err != nil {
return err
}
// 获取精度信息
prec, err := t.getPrecision(symbol)
if err != nil {
return err
}
// 转换为字符串,使用正确的精度格式
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
params := map[string]interface{}{
"symbol": symbol,
"positionSide": "BOTH",
"type": "STOP_MARKET",
"side": side,
"stopPrice": priceStr,
"quantity": qtyStr,
"timeInForce": "GTC",
}
_, err = t.request("POST", "/fapi/v3/order", params)
return err
}
// SetTakeProfit 设置止盈
func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
side := "SELL"
if positionSide == "SHORT" {
side = "BUY"
}
// 格式化价格和数量到正确精度
formattedPrice, err := t.formatPrice(symbol, takeProfitPrice)
if err != nil {
return err
}
formattedQty, err := t.formatQuantity(symbol, quantity)
if err != nil {
return err
}
// 获取精度信息
prec, err := t.getPrecision(symbol)
if err != nil {
return err
}
// 转换为字符串,使用正确的精度格式
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
params := map[string]interface{}{
"symbol": symbol,
"positionSide": "BOTH",
"type": "TAKE_PROFIT_MARKET",
"side": side,
"stopPrice": priceStr,
"quantity": qtyStr,
"timeInForce": "GTC",
}
_, err = t.request("POST", "/fapi/v3/order", params)
return err
}
// CancelAllOrders 取消所有订单
func (t *AsterTrader) CancelAllOrders(symbol string) error {
params := map[string]interface{}{
"symbol": symbol,
}
_, err := t.request("DELETE", "/fapi/v3/allOpenOrders", params)
return err
}
// FormatQuantity 格式化数量(实现Trader接口)
func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
formatted, err := t.formatQuantity(symbol, quantity)
if err != nil {
return "", err
}
return fmt.Sprintf("%v", formatted), nil
}
+12 -1
View File
@@ -21,7 +21,7 @@ type AutoTraderConfig struct {
AIModel string // AI模型: "qwen" 或 "deepseek"
// 交易平台选择
Exchange string // "binance" "hyperliquid"
Exchange string // "binance", "hyperliquid" 或 "aster"
// 币安API配置
BinanceAPIKey string
@@ -31,6 +31,11 @@ type AutoTraderConfig struct {
HyperliquidPrivateKey string
HyperliquidTestnet bool
// Aster配置
AsterUser string // Aster主钱包地址
AsterSigner string // Aster API钱包地址
AsterPrivateKey string // Aster API钱包私钥
CoinPoolAPIURL string
// AI配置
@@ -134,6 +139,12 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
if err != nil {
return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err)
}
case "aster":
log.Printf("🏦 [%s] 使用Aster交易", config.Name)
trader, err = NewAsterTrader(config.AsterUser, config.AsterSigner, config.AsterPrivateKey)
if err != nil {
return nil, fmt.Errorf("初始化Aster交易器失败: %w", err)
}
default:
return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange)
}
+3 -4
View File
@@ -1,6 +1,7 @@
import useSWR from 'swr';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
import { api } from '../lib/api';
interface TradeOutcome {
symbol: string;
@@ -44,13 +45,11 @@ interface AILearningProps {
traderId: string;
}
const fetcher = (url: string) => fetch(url).then(res => res.json());
export default function AILearning({ traderId }: AILearningProps) {
const { language } = useLanguage();
const { data: performance, error } = useSWR<PerformanceAnalysis>(
`http://localhost:8080/api/performance?trader_id=${traderId}`,
fetcher,
traderId ? `performance-${traderId}` : 'performance',
() => api.getPerformance(traderId),
{ refreshInterval: 10000 }
);
+4 -2
View File
@@ -80,8 +80,10 @@ export function EquityChart({ traderId }: EquityChartProps) {
? history.slice(-MAX_DISPLAY_POINTS)
: history;
// 计算初始余额(使用第一个数据点)
const initialBalance = history[0]?.total_equity || 1000;
// 计算初始余额(使用第一个数据点,如果无数据则从account获取,最后才用默认值
const initialBalance = history[0]?.total_equity
|| account?.total_equity
|| 100; // 默认值改为100,与常见配置一致
// 转换数据格式
const chartData = displayHistory.map((point) => {
+10
View File
@@ -100,4 +100,14 @@ export const api = {
if (!res.ok) throw new Error('获取历史数据失败');
return res.json();
},
// 获取AI学习表现分析(支持trader_id
async getPerformance(traderId?: string): Promise<any> {
const url = traderId
? `${API_BASE}/performance?trader_id=${traderId}`
: `${API_BASE}/performance`;
const res = await fetch(url);
if (!res.ok) throw new Error('获取AI学习数据失败');
return res.json();
},
};
+25
View File
@@ -0,0 +1,25 @@
# 常见问题
## 币安持仓模式错误 (code=-4061)
**错误信息**`Order's position side does not match user's setting`
**原因**:系统需要使用双向持仓模式,但您的币安账户设置为单向持仓。
### 解决方法
1. 登录 [币安合约交易平台](https://www.binance.com/zh-CN/futures/BTCUSDT)
2. 点击右上角的 **⚙️ 偏好设置**
3. 选择 **持仓模式**
4. 切换为 **双向持仓** (Hedge Mode)
5. 确认切换
**注意**:切换前必须先平掉所有持仓。
---
更多问题请查看 [GitHub Issues](https://github.com/tinkle-community/nofx/issues)