diff --git a/COMMIT_MESSAGE.txt b/COMMIT_MESSAGE.txt new file mode 100644 index 00000000..85d0fd23 --- /dev/null +++ b/COMMIT_MESSAGE.txt @@ -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] + diff --git a/HOW_TO_SUBMIT_PR.md b/HOW_TO_SUBMIT_PR.md new file mode 100644 index 00000000..2d0a4a83 --- /dev/null +++ b/HOW_TO_SUBMIT_PR.md @@ -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! 🚀** + diff --git a/README.md b/README.md index af6da581..f0f48175 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,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!** diff --git a/config/config.go b/config/config.go index bed19a3b..d89a17fa 100644 --- a/config/config.go +++ b/config/config.go @@ -24,6 +24,11 @@ type TraderConfig struct { HyperliquidPrivateKey string `json:"hyperliquid_private_key,omitempty"` HyperliquidTestnet bool `json:"hyperliquid_testnet,omitempty"` + // Aster配置 + AsterUser string `json:"aster_user,omitempty"` // Aster主钱包地址 + AsterSigner string `json:"aster_signer,omitempty"` // Aster API钱包地址 + AsterPrivateKey string `json:"aster_private_key,omitempty"` // Aster API钱包私钥 + // AI配置 QwenKey string `json:"qwen_key,omitempty"` DeepSeekKey string `json:"deepseek_key,omitempty"` @@ -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 == "" { diff --git a/manager/trader_manager.go b/manager/trader_manager.go index cf41570f..cb01508e 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -41,6 +41,9 @@ func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string, BinanceSecretKey: cfg.BinanceSecretKey, HyperliquidPrivateKey: cfg.HyperliquidPrivateKey, HyperliquidTestnet: cfg.HyperliquidTestnet, + AsterUser: cfg.AsterUser, + AsterSigner: cfg.AsterSigner, + AsterPrivateKey: cfg.AsterPrivateKey, CoinPoolAPIURL: coinPoolURL, UseQwen: cfg.AIModel == "qwen", DeepSeekKey: cfg.DeepSeekKey, diff --git a/trader/aster_trader.go b/trader/aster_trader.go new file mode 100644 index 00000000..937262ef --- /dev/null +++ b/trader/aster_trader.go @@ -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 +} diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 1e6b84a3..cc39341b 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -21,7 +21,7 @@ type AutoTraderConfig struct { AIModel string // AI模型: "qwen" 或 "deepseek" // 交易平台选择 - Exchange string // "binance" 或 "hyperliquid" + Exchange string // "binance", "hyperliquid" 或 "aster" // 币安API配置 BinanceAPIKey string @@ -31,6 +31,11 @@ type AutoTraderConfig struct { HyperliquidPrivateKey string HyperliquidTestnet bool + // Aster配置 + AsterUser string // Aster主钱包地址 + AsterSigner string // Aster API钱包地址 + AsterPrivateKey string // Aster API钱包私钥 + CoinPoolAPIURL string // AI配置 @@ -134,6 +139,12 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { if err != nil { return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err) } + case "aster": + log.Printf("🏦 [%s] 使用Aster交易", config.Name) + trader, err = NewAsterTrader(config.AsterUser, config.AsterSigner, config.AsterPrivateKey) + if err != nil { + return nil, fmt.Errorf("初始化Aster交易器失败: %w", err) + } default: return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange) }