mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-04 11:30:58 +08:00
feat: Add Aster DEX exchange support + fix precision issues
## Features - Add full Aster DEX integration with Binance-compatible API - Support Web3 authentication with API wallet system - Add comprehensive Aster integration guide (ASTER_INTEGRATION.md) - Add example Aster configuration (config.aster.example.json) ## Bug Fixes - Fix precision error (code -1111) for all order types - Implement proper float-to-string conversion with exchange precision - Add automatic precision fetching from /exchangeInfo endpoint - Remove trailing zeros from formatted values ## Documentation - Update README.md with Aster quick start guide - Add detailed setup instructions for creating API wallet - Include troubleshooting FAQ and security best practices - Update core features to mention three supported exchanges ## Technical Details - Added formatFloatWithPrecision() helper function - Updated all order functions to use proper precision formatting - Added precision logging for debugging - Fully backward compatible with existing configurations Closes #[issue number if applicable]
This commit is contained in:
28
COMMIT_MESSAGE.txt
Normal file
28
COMMIT_MESSAGE.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
feat: Add Aster DEX exchange support + fix precision issues
|
||||
|
||||
## Features
|
||||
- Add full Aster DEX integration with Binance-compatible API
|
||||
- Support Web3 authentication with API wallet system
|
||||
- Add comprehensive Aster integration guide (ASTER_INTEGRATION.md)
|
||||
- Add example Aster configuration (config.aster.example.json)
|
||||
|
||||
## Bug Fixes
|
||||
- Fix precision error (code -1111) for all order types
|
||||
- Implement proper float-to-string conversion with exchange precision
|
||||
- Add automatic precision fetching from /exchangeInfo endpoint
|
||||
- Remove trailing zeros from formatted values
|
||||
|
||||
## Documentation
|
||||
- Update README.md with Aster quick start guide
|
||||
- Add detailed setup instructions for creating API wallet
|
||||
- Include troubleshooting FAQ and security best practices
|
||||
- Update core features to mention three supported exchanges
|
||||
|
||||
## Technical Details
|
||||
- Added formatFloatWithPrecision() helper function
|
||||
- Updated all order functions to use proper precision formatting
|
||||
- Added precision logging for debugging
|
||||
- Fully backward compatible with existing configurations
|
||||
|
||||
Closes #[issue number if applicable]
|
||||
|
||||
172
HOW_TO_SUBMIT_PR.md
Normal file
172
HOW_TO_SUBMIT_PR.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 如何提交这个PR (How to Submit This PR)
|
||||
|
||||
## 📋 修改摘要 (Summary of Changes)
|
||||
|
||||
### 新增文件 (New Files)
|
||||
1. **trader/aster_trader.go** - Aster交易所完整实现 (889行)
|
||||
2. **ASTER_INTEGRATION.md** - Aster集成完整指南 (英文)
|
||||
3. **config.aster.example.json** - Aster配置示例
|
||||
4. **COMMIT_MESSAGE.txt** - 提交信息模板 (提交后可删除)
|
||||
5. **HOW_TO_SUBMIT_PR.md** - 本文件 (提交后可删除)
|
||||
|
||||
### 修改文件 (Modified Files)
|
||||
1. **README.md** - 添加Aster介绍和配置说明
|
||||
2. **trader/aster_trader.go** - 修复精度问题
|
||||
3. **config/config.go** - 添加Aster配置字段 (如有修改)
|
||||
4. **manager/trader_manager.go** - 添加Aster初始化 (如有修改)
|
||||
5. **trader/auto_trader.go** - 相关更新 (如有修改)
|
||||
|
||||
## 🚀 提交步骤 (Submission Steps)
|
||||
|
||||
### 1. 检查修改 (Check Changes)
|
||||
```bash
|
||||
# 查看所有修改
|
||||
git status
|
||||
|
||||
# 查看具体更改
|
||||
git diff README.md
|
||||
git diff trader/aster_trader.go
|
||||
```
|
||||
|
||||
### 2. 暂存文件 (Stage Files)
|
||||
```bash
|
||||
# 添加新文件
|
||||
git add trader/aster_trader.go
|
||||
git add ASTER_INTEGRATION.md
|
||||
git add config.aster.example.json
|
||||
|
||||
# 添加修改的文件
|
||||
git add README.md
|
||||
git add config/config.go
|
||||
git add manager/trader_manager.go
|
||||
git add trader/auto_trader.go
|
||||
|
||||
# 查看暂存状态
|
||||
git status
|
||||
```
|
||||
|
||||
### 3. 提交更改 (Commit Changes)
|
||||
```bash
|
||||
# 使用提供的提交信息
|
||||
git commit -F COMMIT_MESSAGE.txt
|
||||
|
||||
# 或者手动编写提交信息
|
||||
git commit -m "feat: Add Aster DEX exchange support + fix precision issues"
|
||||
```
|
||||
|
||||
### 4. 推送到您的分支 (Push to Your Branch)
|
||||
```bash
|
||||
# 如果还没有创建分支,先创建
|
||||
git checkout -b feat/aster-dex-support
|
||||
|
||||
# 推送到远程仓库
|
||||
git push origin feat/aster-dex-support
|
||||
```
|
||||
|
||||
### 5. 创建Pull Request (Create Pull Request)
|
||||
|
||||
1. 访问您的GitHub仓库
|
||||
2. 点击 "Compare & pull request" 按钮
|
||||
3. 填写PR信息:
|
||||
|
||||
**标题 (Title):**
|
||||
```
|
||||
feat: Add Aster DEX exchange support + fix precision issues
|
||||
```
|
||||
|
||||
**描述 (Description):**
|
||||
```markdown
|
||||
## 🎯 Summary
|
||||
This PR adds full support for Aster DEX - a Binance-compatible decentralized perpetual futures exchange - and fixes critical precision handling issues.
|
||||
|
||||
## ✨ Features Added
|
||||
- ✅ Full Aster DEX trading support (long/short, leverage, stop-loss/take-profit)
|
||||
- ✅ Web3 authentication with API wallet security model
|
||||
- ✅ Binance-compatible API (easy migration)
|
||||
- ✅ Comprehensive integration guide with step-by-step instructions
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
- ✅ Fixed precision error (code -1111) for all order types
|
||||
- ✅ Automatic precision handling from exchange specifications
|
||||
- ✅ Proper float-to-string conversion with trailing zero removal
|
||||
|
||||
## 📚 Documentation
|
||||
- ✅ Complete ASTER_INTEGRATION.md guide (setup, API, troubleshooting)
|
||||
- ✅ Updated README.md with Aster quick start
|
||||
- ✅ Added config.aster.example.json
|
||||
|
||||
## 🔧 Technical Details
|
||||
- Added `formatFloatWithPrecision()` helper function
|
||||
- Updated all order functions (OpenLong, OpenShort, CloseLong, CloseShort, SetStopLoss, SetTakeProfit)
|
||||
- Added precision logging for debugging
|
||||
- Fully backward compatible
|
||||
|
||||
## 🎓 How to Use
|
||||
See [ASTER_INTEGRATION.md](ASTER_INTEGRATION.md) for detailed setup instructions.
|
||||
|
||||
Quick start:
|
||||
1. Visit https://www.asterdex.com/en/api-wallet
|
||||
2. Create API wallet and save credentials
|
||||
3. Configure config.json with Aster settings
|
||||
4. Run `./nofx`
|
||||
|
||||
## 🧪 Testing
|
||||
- ✅ Compiled successfully
|
||||
- ✅ Orders placed successfully on Aster
|
||||
- ✅ Precision handling verified with multiple trading pairs
|
||||
- ✅ No breaking changes to existing Binance/Hyperliquid configs
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
Thanks to Aster DEX for the excellent API documentation and Binance-compatible design!
|
||||
```
|
||||
|
||||
### 6. 清理临时文件 (Clean Up)
|
||||
```bash
|
||||
# PR创建后,可以删除这些临时文件
|
||||
rm COMMIT_MESSAGE.txt
|
||||
rm HOW_TO_SUBMIT_PR.md
|
||||
```
|
||||
|
||||
## ✅ 提交前检查清单 (Pre-Submit Checklist)
|
||||
|
||||
- [ ] 所有新文件都已添加
|
||||
- [ ] 所有修改都已暂存
|
||||
- [ ] 代码可以正常编译 (`go build`)
|
||||
- [ ] 没有语法错误
|
||||
- [ ] 文档格式正确(Markdown)
|
||||
- [ ] 敏感信息已移除(API密钥、私钥等)
|
||||
- [ ] ASTER_INTEGRATION.md 文档完整
|
||||
- [ ] README.md 更新完整
|
||||
- [ ] config.aster.example.json 使用示例数据
|
||||
|
||||
## 📝 PR描述要点 (Key Points for PR Description)
|
||||
|
||||
### 核心价值 (Core Value)
|
||||
1. **Aster DEX集成** - 第三个支持的交易所
|
||||
2. **Binance兼容API** - 降低迁移成本
|
||||
3. **修复精度BUG** - 解决实际交易问题
|
||||
4. **完整文档** - 详细的设置指南
|
||||
|
||||
### 技术亮点 (Technical Highlights)
|
||||
1. Web3认证 - API钱包安全系统
|
||||
2. 自动精度处理 - 从交易所获取精度要求
|
||||
3. 向后兼容 - 不影响现有配置
|
||||
|
||||
### 用户价值 (User Benefits)
|
||||
1. 更多交易所选择
|
||||
2. 去中心化选项
|
||||
3. 更低手续费
|
||||
4. 无需KYC
|
||||
|
||||
## 🔗 相关链接 (Related Links)
|
||||
|
||||
- Aster DEX官网: https://www.asterdex.com/
|
||||
- Aster API文档: https://github.com/asterdex/api-docs
|
||||
- API钱包管理: https://www.asterdex.com/en/api-wallet
|
||||
|
||||
---
|
||||
|
||||
**需要帮助?** 加入Telegram开发者社区: https://t.me/nofx_dev_community
|
||||
|
||||
**祝您PR顺利! Good luck with your PR! 🚀**
|
||||
|
||||
114
README.md
114
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,30 @@ 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"`
|
||||
|
||||
---
|
||||
|
||||
## ✨ Core Features
|
||||
@@ -442,6 +470,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 +1185,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 +1265,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!**
|
||||
|
||||
|
||||
@@ -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"`
|
||||
@@ -108,8 +113,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 +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 == "" {
|
||||
|
||||
@@ -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,
|
||||
|
||||
888
trader/aster_trader.go
Normal file
888
trader/aster_trader.go
Normal file
@@ -0,0 +1,888 @@
|
||||
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) {
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// CloseShort 平空单
|
||||
func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) {
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user