diff --git a/CUSTOM_API.md b/CUSTOM_API.md index 979a89c6..e08b25e8 100644 --- a/CUSTOM_API.md +++ b/CUSTOM_API.md @@ -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. 网络连接问题 diff --git a/Dockerfile b/Dockerfile index 95467a0c..4a8d8fb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index af6da581..1e6c15f7 100644 --- a/README.md +++ b/README.md @@ -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!** diff --git a/README.ru.md b/README.ru.md index 9febe179..874c478a 100644 --- a/README.ru.md +++ b/README.ru.md @@ -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 трейдеров, конкурирующих друг с другом: diff --git a/README.uk.md b/README.uk.md index b5afe3fb..8c955c59 100644 --- a/README.uk.md +++ b/README.uk.md @@ -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 трейдерів, що змагаються один з одним: diff --git a/README.zh-CN.md b/README.zh-CN.md index 4fbabdde..91447973 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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相互竞争: diff --git a/config.json.example b/config.json.example index d6865ec5..4df4314e 100644 --- a/config.json.example +++ b/config.json.example @@ -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, diff --git a/config/config.go b/config/config.go index bed19a3b..7c3626ae 100644 --- a/config/config.go +++ b/config/config.go @@ -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 == "" { diff --git a/decision/engine.go b/decision/engine.go index f6676196..a25f3644 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -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") diff --git a/docker-compose.yml b/docker-compose.yml index 71c76f50..e273f511 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/main.go b/main.go index d1347535..66cbfca4 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/manager/trader_manager.go b/manager/trader_manager.go index cf41570f..cb01508e 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -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, diff --git a/mcp/client.go b/mcp/client.go index ead27384..7c8643eb 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -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) diff --git a/pool/coin_pool.go b/pool/coin_pool.go index 6675013c..72ce5c72 100644 --- a/pool/coin_pool.go +++ b/pool/coin_pool.go @@ -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) { // 优先检查是否启用默认币种列表 diff --git a/screenshots/competition-page.png b/screenshots/competition-page.png new file mode 100644 index 00000000..dad13d4e Binary files /dev/null and b/screenshots/competition-page.png differ diff --git a/screenshots/details-page.png b/screenshots/details-page.png new file mode 100644 index 00000000..e7c9328d Binary files /dev/null and b/screenshots/details-page.png differ diff --git a/trader/aster_trader.go b/trader/aster_trader.go new file mode 100644 index 00000000..9aaf078c --- /dev/null +++ b/trader/aster_trader.go @@ -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 +} diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 1e6b84a3..cc39341b 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -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) } diff --git a/web/src/components/AILearning.tsx b/web/src/components/AILearning.tsx index e3806b36..b7757e32 100644 --- a/web/src/components/AILearning.tsx +++ b/web/src/components/AILearning.tsx @@ -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( - `http://localhost:8080/api/performance?trader_id=${traderId}`, - fetcher, + traderId ? `performance-${traderId}` : 'performance', + () => api.getPerformance(traderId), { refreshInterval: 10000 } ); diff --git a/web/src/components/EquityChart.tsx b/web/src/components/EquityChart.tsx index 505069b9..36f58342 100644 --- a/web/src/components/EquityChart.tsx +++ b/web/src/components/EquityChart.tsx @@ -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) => { diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 22d1b82d..d00ae2ab 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -100,4 +100,14 @@ export const api = { if (!res.ok) throw new Error('获取历史数据失败'); return res.json(); }, + + // 获取AI学习表现分析(支持trader_id) + async getPerformance(traderId?: string): Promise { + 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(); + }, }; diff --git a/常见问题.md b/常见问题.md new file mode 100644 index 00000000..5d823205 --- /dev/null +++ b/常见问题.md @@ -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)