diff --git a/CUSTOM_API.md b/CUSTOM_API.md new file mode 100644 index 00000000..979a89c6 --- /dev/null +++ b/CUSTOM_API.md @@ -0,0 +1,185 @@ +# 自定义 AI API 使用指南 + +## 功能说明 + +现在 NOFX 支持使用任何 OpenAI 格式兼容的 API,包括: +- OpenAI 官方 API (gpt-4o, gpt-4-turbo 等) +- OpenRouter (可访问多种模型) +- 本地部署的模型 (Ollama, LM Studio 等) +- 其他兼容 OpenAI 格式的 API 服务 + +## 配置方式 + +在 `config.json` 中添加使用自定义 API 的 trader: + +```json +{ + "traders": [ + { + "id": "trader_custom", + "name": "My Custom AI Trader", + "ai_model": "custom", + "exchange": "binance", + + "binance_api_key": "your_binance_api_key", + "binance_secret_key": "your_binance_secret_key", + + "custom_api_url": "https://api.openai.com/v1", + "custom_api_key": "sk-your-openai-api-key", + "custom_model_name": "gpt-4o", + + "initial_balance": 1000, + "scan_interval_minutes": 3 + } + ] +} +``` + +## 配置字段说明 + +| 字段 | 类型 | 必需 | 说明 | +|-----|------|------|------| +| `ai_model` | string | ✅ | 设置为 `"custom"` 启用自定义 API | +| `custom_api_url` | string | ✅ | API 的 Base URL (不含 `/chat/completions`) | +| `custom_api_key` | string | ✅ | API 密钥 | +| `custom_model_name` | string | ✅ | 模型名称 (如 `gpt-4o`, `claude-3-5-sonnet` 等) | + +## 使用示例 + +### 1. OpenAI 官方 API + +```json +{ + "ai_model": "custom", + "custom_api_url": "https://api.openai.com/v1", + "custom_api_key": "sk-proj-xxxxx", + "custom_model_name": "gpt-4o" +} +``` + +### 2. OpenRouter + +```json +{ + "ai_model": "custom", + "custom_api_url": "https://openrouter.ai/api/v1", + "custom_api_key": "sk-or-xxxxx", + "custom_model_name": "anthropic/claude-3.5-sonnet" +} +``` + +### 3. 本地 Ollama + +```json +{ + "ai_model": "custom", + "custom_api_url": "http://localhost:11434/v1", + "custom_api_key": "ollama", + "custom_model_name": "llama3.1:70b" +} +``` + +### 4. Azure OpenAI + +```json +{ + "ai_model": "custom", + "custom_api_url": "https://your-resource.openai.azure.com/openai/deployments/your-deployment", + "custom_api_key": "your-azure-api-key", + "custom_model_name": "gpt-4" +} +``` + +## 兼容性要求 + +自定义 API 必须: +1. 支持 OpenAI Chat Completions 格式 +2. 接受 `POST /chat/completions` 端点 +3. 支持 `Authorization: Bearer {api_key}` 认证 +4. 返回标准的 OpenAI 响应格式 + +## 注意事项 + +1. **URL 格式**:`custom_api_url` 应该是 Base URL,系统会自动添加 `/chat/completions` + - ✅ 正确:`https://api.openai.com/v1` + - ❌ 错误:`https://api.openai.com/v1/chat/completions` + +2. **模型名称**:确保 `custom_model_name` 与 API 提供商支持的模型名称完全一致 + +3. **API 密钥**:某些本地部署的模型可能不需要真实的 API 密钥,可以填写任意字符串 + +4. **超时设置**:默认超时时间为 120 秒,如果模型响应较慢可能需要调整 + +## 多 AI 对比交易 + +你可以同时配置多个不同 AI 的 trader 进行对比: + +```json +{ + "traders": [ + { + "id": "deepseek_trader", + "ai_model": "deepseek", + "deepseek_key": "sk-xxxxx", + ... + }, + { + "id": "gpt4_trader", + "ai_model": "custom", + "custom_api_url": "https://api.openai.com/v1", + "custom_api_key": "sk-xxxxx", + "custom_model_name": "gpt-4o", + ... + }, + { + "id": "claude_trader", + "ai_model": "custom", + "custom_api_url": "https://openrouter.ai/api/v1", + "custom_api_key": "sk-or-xxxxx", + "custom_model_name": "anthropic/claude-3.5-sonnet", + ... + } + ] +} +``` + +## 故障排除 + +### 问题:配置验证失败 + +**错误信息**:`使用自定义API时必须配置custom_api_url` + +**解决方案**:确保设置了 `ai_model: "custom"` 后,同时配置了: +- `custom_api_url` +- `custom_api_key` +- `custom_model_name` + +### 问题:API 调用失败 + +**可能原因**: +1. URL 格式错误(检查是否包含了 `/chat/completions`) +2. API 密钥无效 +3. 模型名称错误 +4. 网络连接问题 + +**调试方法**:查看日志中的错误信息,通常会包含 HTTP 状态码和错误详情 + +## 向后兼容性 + +现有的 `deepseek` 和 `qwen` 配置完全不受影响,可以继续使用: + +```json +{ + "ai_model": "deepseek", + "deepseek_key": "sk-xxxxx" +} +``` + +或 + +```json +{ + "ai_model": "qwen", + "qwen_key": "sk-xxxxx" +} +``` diff --git a/Dockerfile b/Dockerfile index 67c9f22d..4a8d8fb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,19 +2,29 @@ FROM golang:1.25-alpine AS backend-builder # Install build dependencies including TA-Lib -RUN apk add --no-cache \ +RUN apk update && \ + apk add --no-cache \ git \ make \ gcc \ g++ \ musl-dev \ wget \ - tar + tar \ + autoconf \ + automake # Install TA-Lib RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz && \ tar -xzf ta-lib-0.4.0-src.tar.gz && \ cd ta-lib && \ + if [ "$(uname -m)" = "aarch64" ]; then \ + CONFIG_GUESS=$(find /usr/share -name config.guess | head -1) && \ + CONFIG_SUB=$(find /usr/share -name config.sub | head -1) && \ + cp "$CONFIG_GUESS" config.guess && \ + cp "$CONFIG_SUB" config.sub && \ + chmod +x config.guess config.sub; \ + fi && \ ./configure --prefix=/usr && \ make && \ make install && \ @@ -56,8 +66,9 @@ RUN npm run build # Final stage FROM alpine:latest -# Install runtime dependencies -RUN apk add --no-cache \ +# Update package index and install runtime dependencies +RUN apk update && \ + apk add --no-cache \ ca-certificates \ tzdata \ wget \ @@ -65,12 +76,21 @@ RUN apk add --no-cache \ make \ gcc \ g++ \ - musl-dev + musl-dev \ + autoconf \ + automake # Install TA-Lib runtime RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz && \ tar -xzf ta-lib-0.4.0-src.tar.gz && \ cd ta-lib && \ + if [ "$(uname -m)" = "aarch64" ]; then \ + CONFIG_GUESS=$(find /usr/share -name config.guess | head -1) && \ + CONFIG_SUB=$(find /usr/share -name config.sub | head -1) && \ + cp "$CONFIG_GUESS" config.guess && \ + cp "$CONFIG_SUB" config.sub && \ + chmod +x config.guess config.sub; \ + fi && \ ./configure --prefix=/usr && \ make && \ make install && \ 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 + +*Multi-AI leaderboard with real-time performance comparison charts showing Qwen vs DeepSeek live trading battle* + +### 📊 Trader Details - Complete Trading Dashboard + +*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 в реальном времени + +*Лидерборд с несколькими AI и графики сравнения производительности в реальном времени показывают битву Qwen против DeepSeek* + +### 📊 Детали трейдера - Полная торговая панель + +*Профессиональный торговый интерфейс с кривыми капитала, живыми позициями и логами решений 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 в реальному часі + +*Лідерборд з кількома AI та графіки порівняння продуктивності в реальному часі показують битву Qwen проти DeepSeek* + +### 📊 Деталі трейдера - Повна торгова панель + +*Професійний торговий інтерфейс з кривими капіталу, живими позиціями та логами рішень 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实时对战 + +*多AI排行榜和实时性能对比图表,展示Qwen vs DeepSeek实时交易对战* + +### 📊 交易详情 - 完整交易仪表盘 + +*专业交易界面,包含权益曲线、实时持仓、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 60a494ea..f897a555 100644 --- a/config.json.example +++ b/config.json.example @@ -21,6 +21,38 @@ "qwen_key": "your_qwen_api_key", "initial_balance": 1000, "scan_interval_minutes": 3 + }, + { + "id": "binance_custom", + "name": "Binance Custom API Trader", + "ai_model": "custom", + "exchange": "binance", + "binance_api_key": "your_binance_api_key", + "binance_secret_key": "your_binance_secret_key", + "custom_api_url": "https://api.openai.com/v1", + "custom_api_key": "sk-your-api-key", + "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": { diff --git a/config/config.go b/config/config.go index 2e26c925..d89a17fa 100644 --- a/config/config.go +++ b/config/config.go @@ -24,10 +24,20 @@ 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"` + // 自定义AI API配置(支持任何OpenAI格式的API) + CustomAPIURL string `json:"custom_api_url,omitempty"` + CustomAPIKey string `json:"custom_api_key,omitempty"` + CustomModelName string `json:"custom_model_name,omitempty"` + InitialBalance float64 `json:"initial_balance"` ScanIntervalMinutes int `json:"scan_interval_minutes"` } @@ -95,16 +105,16 @@ func (c *Config) Validate() error { if trader.Name == "" { return fmt.Errorf("trader[%d]: Name不能为空", i) } - if trader.AIModel != "qwen" && trader.AIModel != "deepseek" { - return fmt.Errorf("trader[%d]: ai_model必须是 'qwen' 或 'deepseek'", i) + if trader.AIModel != "qwen" && trader.AIModel != "deepseek" && trader.AIModel != "custom" { + return fmt.Errorf("trader[%d]: ai_model必须是 'qwen', 'deepseek' 或 'custom'", i) } // 验证交易平台配置 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) } // 根据平台验证对应的密钥 @@ -116,6 +126,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 == "" { @@ -124,6 +138,17 @@ func (c *Config) Validate() error { if trader.AIModel == "deepseek" && trader.DeepSeekKey == "" { return fmt.Errorf("trader[%d]: 使用DeepSeek时必须配置deepseek_key", i) } + if trader.AIModel == "custom" { + if trader.CustomAPIURL == "" { + return fmt.Errorf("trader[%d]: 使用自定义API时必须配置custom_api_url", i) + } + if trader.CustomAPIKey == "" { + return fmt.Errorf("trader[%d]: 使用自定义API时必须配置custom_api_key", i) + } + if trader.CustomModelName == "" { + return fmt.Errorf("trader[%d]: 使用自定义API时必须配置custom_model_name", i) + } + } if trader.InitialBalance <= 0 { return fmt.Errorf("trader[%d]: initial_balance必须大于0", i) } diff --git a/decision/engine.go b/decision/engine.go index 4892c155..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(2x杠杆) | BTC/ETH %.0f-%.0f U(5x杠杆)\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") // === 做空激励 === @@ -253,7 +253,7 @@ func buildSystemPrompt(accountEquity float64) string { sb.WriteString("- 💰 **资金序列**:成交量序列、持仓量(OI)序列、资金费率\n") sb.WriteString("- 🎯 **筛选标记**:AI500评分 / OI_Top排名(如果有标注)\n\n") sb.WriteString("**分析方法**(完全由你自主决定):\n") - sb.WriteString("- 自由运用序列数据,你可以做趋势分析、形态识别、支撑阻力计算\n") + sb.WriteString("- 自由运用序列数据,你可以做但不限于趋势分析、形态识别、支撑阻力、技术阻力位、斐波那契、波动带计算\n") sb.WriteString("- 多维度交叉验证(价格+量+OI+指标+序列形态)\n") sb.WriteString("- 用你认为最有效的方法发现高确定性机会\n") sb.WriteString("- 综合信心度 ≥ 75 才开仓\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 61ea53e9..96260ca7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: - ./config.json:/app/config.json:ro - ./decision_logs:/app/decision_logs - /etc/localtime:/etc/localtime:ro # 同步主机时间 + - frontend-dist:/app/web/dist-shared:rw # 共享前端文件 environment: - TZ=Asia/Shanghai # 使用中国时区 networks: @@ -24,6 +25,7 @@ services: timeout: 10s retries: 3 start_period: 60s + command: sh -c "cp -r /app/web/dist/* /app/web/dist-shared/ 2>/dev/null || true && exec ./nofx" # Frontend (Nginx) nofx-frontend: @@ -33,13 +35,16 @@ services: ports: - "3000:80" volumes: - - ./web/dist:/usr/share/nginx/html:ro + - frontend-dist:/usr/share/nginx/html:ro - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro networks: - nofx-network depends_on: - nofx +volumes: + frontend-dist: + networks: nofx-network: driver: bridge diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 18373a13..cb01508e 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -41,10 +41,16 @@ 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, QwenKey: cfg.QwenKey, + CustomAPIURL: cfg.CustomAPIURL, + CustomAPIKey: cfg.CustomAPIKey, + CustomModelName: cfg.CustomModelName, ScanInterval: cfg.GetScanInterval(), InitialBalance: cfg.InitialBalance, BTCETHLeverage: leverage.BTCETHLeverage, // 使用配置的杠杆倍数 diff --git a/mcp/client.go b/mcp/client.go index 55f1fb81..ead27384 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -16,6 +16,7 @@ type Provider string const ( ProviderDeepSeek Provider = "deepseek" ProviderQwen Provider = "qwen" + ProviderCustom Provider = "custom" ) // Config AI API配置 @@ -53,6 +54,15 @@ func SetQwenAPIKey(apiKey, secretKey string) { defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max } +// SetCustomAPI 设置自定义OpenAI兼容API +func SetCustomAPI(apiURL, apiKey, modelName string) { + defaultConfig.Provider = ProviderCustom + defaultConfig.APIKey = apiKey + defaultConfig.BaseURL = apiURL + defaultConfig.Model = modelName + defaultConfig.Timeout = 120 * time.Second +} + // SetConfig 设置完整的AI配置(高级用户) func SetConfig(config Config) { if config.Timeout == 0 { 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 c90ceef1..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配置 @@ -38,6 +43,11 @@ type AutoTraderConfig struct { DeepSeekKey string QwenKey string + // 自定义AI API配置 + CustomAPIURL string + CustomAPIKey string + CustomModelName string + // 扫描配置 ScanInterval time.Duration // 扫描间隔(建议3分钟) @@ -91,10 +101,16 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { } // 初始化AI - if config.UseQwen { + if config.AIModel == "custom" { + // 使用自定义API + mcp.SetCustomAPI(config.CustomAPIURL, config.CustomAPIKey, config.CustomModelName) + log.Printf("🤖 [%s] 使用自定义AI API: %s (模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName) + } else if config.UseQwen || config.AIModel == "qwen" { + // 使用Qwen mcp.SetQwenAPIKey(config.QwenKey, "") log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name) } else { + // 默认使用DeepSeek mcp.SetDeepSeekAPIKey(config.DeepSeekKey) log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name) } @@ -123,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/App.tsx b/web/src/App.tsx index 70761155..8e4d412b 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -558,6 +558,7 @@ function StatCard({ // Decision Card Component with CoT Trace - Binance Style function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) { + const [showInputPrompt, setShowInputPrompt] = useState(false); const [showCoT, setShowCoT] = useState(false); return ( @@ -581,6 +582,25 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua + {/* Input Prompt - Collapsible */} + {decision.input_prompt && ( +