mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 17:12:25 +08:00
Refactor/trading actions (#1169)
* refactor: 简化交易动作,移除 update_stop_loss/update_take_profit/partial_close - 移除 Decision 结构体中的 NewStopLoss, NewTakeProfit, ClosePercentage 字段 - 删除 executeUpdateStopLossWithRecord, executeUpdateTakeProfitWithRecord, executePartialCloseWithRecord 函数 - 简化 logger 中的 partial_close 聚合逻辑 - 更新 AI prompt 和验证逻辑,只保留 6 个核心动作 - 清理相关测试代码 保留的交易动作: open_long, open_short, close_long, close_short, hold, wait * refactor: 移除 AI学习与反思 模块 - 删除前端 AILearning.tsx 组件和相关引用 - 删除后端 /performance API 接口 - 删除 logger 中 AnalyzePerformance、calculateSharpeRatio 等函数 - 删除 PerformanceAnalysis、TradeOutcome、SymbolPerformance 等结构体 - 删除 Context 中的 Performance 字段 - 移除 AI prompt 中夏普比率自我进化相关内容 - 清理 i18n 翻译文件中的相关条目 该模块基于磁盘存储计算,经常出错,做减法移除 * refactor: 将数据库操作统一迁移到 store 包 - 新增 store/ 包,统一管理所有数据库操作 - store.go: 主 Store 结构,懒加载各子模块 - user.go, ai_model.go, exchange.go, trader.go 等子模块 - 支持加密/解密函数注入 (SetCryptoFuncs) - 更新 main.go 使用 store.New() 替代 config.NewDatabase() - 更新 api/server.go 使用 *store.Store 替代 *config.Database - 更新 manager/trader_manager.go: - 新增 LoadTradersFromStore, LoadUserTradersFromStore 方法 - 删除旧版 LoadUserTraders, LoadTraderByID, loadSingleTrader 等方法 - 移除 nofx/config 依赖 - 删除 config/database.go 和 config/database_test.go - 更新 api/server_test.go 使用 store.Trader 类型 - 清理 logger/ 包中未使用的 telegram 相关代码 * refactor: unify encryption key management via .env - Remove redundant EncryptionManager and SecureStorage - Simplify CryptoService to load keys from environment variables only - RSA_PRIVATE_KEY: RSA private key for client-server encryption - DATA_ENCRYPTION_KEY: AES-256 key for database encryption - JWT_SECRET: JWT signing key for authentication - Update start.sh to auto-generate missing keys on first run - Remove secrets/ directory and file-based key storage - Delete obsolete encryption setup scripts - Update .env.example with all required keys * refactor: unify logger usage across mcp package - Add MCPLogger adapter in logger package to implement mcp.Logger interface - Update mcp/config.go to use global logger by default - Remove redundant defaultLogger from mcp/logger.go - Keep noopLogger for testing purposes * chore: remove leftover test RSA key file * chore: remove unused bootstrap package * refactor: unify logging to use logger package instead of fmt/log - Replace all fmt.Print/log.Print calls with logger package - Add auto-initialization in logger package init() for test compatibility - Update main.go to initialize logger at startup - Migrate all packages: api, backtest, config, decision, manager, market, store, trader * refactor: rename database file from config.db to data.db - Update main.go, start.sh, docker-compose.yml - Update migration script and documentation - Update .gitignore and translations * fix: add RSA_PRIVATE_KEY to docker-compose environment * fix: add registration_enabled to /api/config response * fix: Fix navigation between login and register pages Use window.location.href instead of react-router's navigate() to fix the issue where URL changes but the page doesn't reload due to App.tsx using custom route state management. * fix: Switch SQLite from WAL to DELETE mode for Docker compatibility WAL mode causes data sync issues with Docker bind mounts on macOS due to incompatible file locking mechanisms between the container and host. DELETE mode (traditional journaling) ensures data is written directly to the main database file. * refactor: Remove default user from database initialization The default user was a legacy placeholder that is no longer needed now that proper user registration is in place. * feat: Add order tracking system with centralized status sync - Add trader_orders table for tracking all order lifecycle - Implement GetOrderStatus interface for all exchanges (Binance, Bybit, Hyperliquid, Aster, Lighter) - Create OrderSyncManager for centralized order status polling - Add trading statistics (Sharpe ratio, win rate, profit factor) to AI context - Include recent completed orders in AI decision input - Remove per-order goroutine polling in favor of global sync manager * feat: Add TradingView K-line chart to dashboard - Create TradingViewChart component with exchange/symbol selectors - Support Binance, Bybit, OKX, Coinbase, Kraken, KuCoin exchanges - Add popular symbols quick selection - Support multiple timeframes (1m to 1W) - Add fullscreen mode - Integrate with Dashboard page below equity chart - Add i18n translations for zh/en * refactor: Replace separate charts with tabbed ChartTabs component - Create ChartTabs component with tab switching between equity curve and K-line - Add embedded mode support for EquityChart and TradingViewChart - User can now switch between account equity and market chart in same area * fix: Use ChartTabs in App.tsx and fix embedded mode in EquityChart - Replace EquityChart with ChartTabs in App.tsx (the actual dashboard renderer) - Fix EquityChart embedded mode for error and empty data states - Rename interval state to timeInterval to avoid shadowing window.setInterval - Add debug logging to ChartTabs component * feat: Add position tracking system for accurate trade history - Add trader_positions table to track complete open/close trades - Add PositionSyncManager to detect manual closes via polling - Record position on open, update on close with PnL calculation - Use positions table for trading stats and recent trades (replacing orders table) - Fix TradingView chart symbol format (add .P suffix for futures) - Fix DecisionCard wait/hold action color (gray instead of red) - Auto-append USDT suffix for custom symbol input * update ---------
This commit is contained in:
@@ -8,7 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/http"
|
||||
@@ -469,13 +469,13 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
if !foundUSDT {
|
||||
log.Printf("⚠️ 未找到USDT资产记录!")
|
||||
logger.Infof("⚠️ 未找到USDT资产记录!")
|
||||
}
|
||||
|
||||
// 获取持仓计算保证金占用和真实未实现盈亏
|
||||
positions, err := t.GetPositions()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 获取持仓信息失败: %v", err)
|
||||
logger.Infof("⚠️ 获取持仓信息失败: %v", err)
|
||||
// fallback: 无法获取持仓时使用简单计算
|
||||
return map[string]interface{}{
|
||||
"totalWalletBalance": crossWalletBalance,
|
||||
@@ -577,7 +577,7 @@ func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 开仓前先取消所有挂单,防止残留挂单导致仓位叠加
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败(继续开仓): %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败(继续开仓): %v", err)
|
||||
}
|
||||
|
||||
// 先设置杠杆
|
||||
@@ -614,7 +614,7 @@ func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (m
|
||||
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
|
||||
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
|
||||
|
||||
log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
|
||||
logger.Infof(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
|
||||
limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision)
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -644,7 +644,7 @@ func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (m
|
||||
func (t *AsterTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 开仓前先取消所有挂单,防止残留挂单导致仓位叠加
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败(继续开仓): %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败(继续开仓): %v", err)
|
||||
}
|
||||
|
||||
// 先设置杠杆
|
||||
@@ -681,7 +681,7 @@ func (t *AsterTrader) OpenShort(symbol string, quantity float64, leverage int) (
|
||||
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
|
||||
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
|
||||
|
||||
log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
|
||||
logger.Infof(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
|
||||
limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision)
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -726,7 +726,7 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int
|
||||
if quantity == 0 {
|
||||
return nil, fmt.Errorf("没有找到 %s 的多仓", symbol)
|
||||
}
|
||||
log.Printf(" 📊 获取到多仓数量: %.8f", quantity)
|
||||
logger.Infof(" 📊 获取到多仓数量: %.8f", quantity)
|
||||
}
|
||||
|
||||
price, err := t.GetMarketPrice(symbol)
|
||||
@@ -756,7 +756,7 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int
|
||||
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
|
||||
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
|
||||
|
||||
log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
|
||||
logger.Infof(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
|
||||
limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision)
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -779,11 +779,11 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("✓ 平多仓成功: %s 数量: %s", symbol, qtyStr)
|
||||
logger.Infof("✓ 平多仓成功: %s 数量: %s", symbol, qtyStr)
|
||||
|
||||
// 平仓后取消该币种的所有挂单(止损止盈单)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -809,7 +809,7 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in
|
||||
if quantity == 0 {
|
||||
return nil, fmt.Errorf("没有找到 %s 的空仓", symbol)
|
||||
}
|
||||
log.Printf(" 📊 获取到空仓数量: %.8f", quantity)
|
||||
logger.Infof(" 📊 获取到空仓数量: %.8f", quantity)
|
||||
}
|
||||
|
||||
price, err := t.GetMarketPrice(symbol)
|
||||
@@ -839,7 +839,7 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in
|
||||
priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision)
|
||||
qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision)
|
||||
|
||||
log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
|
||||
logger.Infof(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)",
|
||||
limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision)
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -862,11 +862,11 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("✓ 平空仓成功: %s 数量: %s", symbol, qtyStr)
|
||||
logger.Infof("✓ 平空仓成功: %s 数量: %s", symbol, qtyStr)
|
||||
|
||||
// 平仓后取消该币种的所有挂单(止损止盈单)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -892,30 +892,30 @@ func (t *AsterTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
|
||||
// 如果错误表示无需更改,忽略错误
|
||||
if strings.Contains(err.Error(), "No need to change") ||
|
||||
strings.Contains(err.Error(), "Margin type cannot be changed") {
|
||||
log.Printf(" ✓ %s 仓位模式已是 %s 或有持仓无法更改", symbol, marginType)
|
||||
logger.Infof(" ✓ %s 仓位模式已是 %s 或有持仓无法更改", symbol, marginType)
|
||||
return nil
|
||||
}
|
||||
// 检测多资产模式(错误码 -4168)
|
||||
if strings.Contains(err.Error(), "Multi-Assets mode") ||
|
||||
strings.Contains(err.Error(), "-4168") ||
|
||||
strings.Contains(err.Error(), "4168") {
|
||||
log.Printf(" ⚠️ %s 检测到多资产模式,强制使用全仓模式", symbol)
|
||||
log.Printf(" 💡 提示:如需使用逐仓模式,请在交易所关闭多资产模式")
|
||||
logger.Infof(" ⚠️ %s 检测到多资产模式,强制使用全仓模式", symbol)
|
||||
logger.Infof(" 💡 提示:如需使用逐仓模式,请在交易所关闭多资产模式")
|
||||
return nil
|
||||
}
|
||||
// 检测统一账户 API
|
||||
if strings.Contains(err.Error(), "unified") ||
|
||||
strings.Contains(err.Error(), "portfolio") ||
|
||||
strings.Contains(err.Error(), "Portfolio") {
|
||||
log.Printf(" ❌ %s 检测到统一账户 API,无法进行合约交易", symbol)
|
||||
logger.Infof(" ❌ %s 检测到统一账户 API,无法进行合约交易", symbol)
|
||||
return fmt.Errorf("请使用「现货与合约交易」API 权限,不要使用「统一账户 API」")
|
||||
}
|
||||
log.Printf(" ⚠️ 设置仓位模式失败: %v", err)
|
||||
logger.Infof(" ⚠️ 设置仓位模式失败: %v", err)
|
||||
// 不返回错误,让交易继续
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf(" ✓ %s 仓位模式已设置为 %s", symbol, marginType)
|
||||
logger.Infof(" ✓ %s 仓位模式已设置为 %s", symbol, marginType)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1075,19 +1075,19 @@ func (t *AsterTrader) CancelStopLossOrders(symbol string) error {
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("订单ID %d: %v", int64(orderID), err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
log.Printf(" ⚠ 取消止损单失败: %s", errMsg)
|
||||
logger.Infof(" ⚠ 取消止损单失败: %s", errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消止损单 (订单ID: %d, 类型: %s, 方向: %s)", int64(orderID), orderType, positionSide)
|
||||
logger.Infof(" ✓ 已取消止损单 (订单ID: %d, 类型: %s, 方向: %s)", int64(orderID), orderType, positionSide)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 && len(cancelErrors) == 0 {
|
||||
log.Printf(" ℹ %s 没有止损单需要取消", symbol)
|
||||
logger.Infof(" ℹ %s 没有止损单需要取消", symbol)
|
||||
} else if canceledCount > 0 {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount)
|
||||
logger.Infof(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
// 如果所有取消都失败了,返回错误
|
||||
@@ -1134,19 +1134,19 @@ func (t *AsterTrader) CancelTakeProfitOrders(symbol string) error {
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("订单ID %d: %v", int64(orderID), err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
log.Printf(" ⚠ 取消止盈单失败: %s", errMsg)
|
||||
logger.Infof(" ⚠ 取消止盈单失败: %s", errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s, 方向: %s)", int64(orderID), orderType, positionSide)
|
||||
logger.Infof(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s, 方向: %s)", int64(orderID), orderType, positionSide)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 && len(cancelErrors) == 0 {
|
||||
log.Printf(" ℹ %s 没有止盈单需要取消", symbol)
|
||||
logger.Infof(" ℹ %s 没有止盈单需要取消", symbol)
|
||||
} else if canceledCount > 0 {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount)
|
||||
logger.Infof(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
// 如果所有取消都失败了,返回错误
|
||||
@@ -1203,20 +1203,20 @@ func (t *AsterTrader) CancelStopOrders(symbol string) error {
|
||||
|
||||
_, err := t.request("DELETE", "/fapi/v3/order", cancelParams)
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ 取消订单 %d 失败: %v", int64(orderID), err)
|
||||
logger.Infof(" ⚠ 取消订单 %d 失败: %v", int64(orderID), err)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)",
|
||||
logger.Infof(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)",
|
||||
symbol, int64(orderID), orderType)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol)
|
||||
logger.Infof(" ℹ %s 没有止盈/止损单需要取消", symbol)
|
||||
} else {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount)
|
||||
logger.Infof(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1230,3 +1230,52 @@ func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, e
|
||||
}
|
||||
return fmt.Sprintf("%v", formatted), nil
|
||||
}
|
||||
|
||||
// GetOrderStatus 获取订单状态
|
||||
func (t *AsterTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) {
|
||||
params := map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
"orderId": orderID,
|
||||
}
|
||||
|
||||
body, err := t.request("GET", "/fapi/v3/order", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取订单状态失败: %w", err)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析订单响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 标准化返回字段
|
||||
response := map[string]interface{}{
|
||||
"orderId": result["orderId"],
|
||||
"symbol": result["symbol"],
|
||||
"status": result["status"],
|
||||
"side": result["side"],
|
||||
"type": result["type"],
|
||||
"time": result["time"],
|
||||
"updateTime": result["updateTime"],
|
||||
"commission": 0.0, // Aster 可能需要单独查询
|
||||
}
|
||||
|
||||
// 解析数值字段
|
||||
if avgPrice, ok := result["avgPrice"].(string); ok {
|
||||
if v, err := strconv.ParseFloat(avgPrice, 64); err == nil {
|
||||
response["avgPrice"] = v
|
||||
}
|
||||
} else if avgPrice, ok := result["avgPrice"].(float64); ok {
|
||||
response["avgPrice"] = avgPrice
|
||||
}
|
||||
|
||||
if executedQty, ok := result["executedQty"].(string); ok {
|
||||
if v, err := strconv.ParseFloat(executedQty, 64); err == nil {
|
||||
response["executedQty"] = v
|
||||
}
|
||||
} else if executedQty, ok := result["executedQty"].(float64); ok {
|
||||
response["executedQty"] = executedQty
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
"nofx/decision"
|
||||
"nofx/logger"
|
||||
"nofx/market"
|
||||
"nofx/pool"
|
||||
"nofx/store"
|
||||
|
||||
"github.com/agiledragon/gomonkey/v2"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@@ -30,8 +30,7 @@ type AutoTraderTestSuite struct {
|
||||
|
||||
// Mock 依赖
|
||||
mockTrader *MockTrader
|
||||
mockDB *MockDatabase
|
||||
mockLogger logger.IDecisionLogger
|
||||
mockStore *store.Store
|
||||
|
||||
// gomonkey patches
|
||||
patches *gomonkey.Patches
|
||||
@@ -65,10 +64,9 @@ func (s *AutoTraderTestSuite) SetupTest() {
|
||||
positions: []map[string]interface{}{},
|
||||
}
|
||||
|
||||
s.mockDB = &MockDatabase{}
|
||||
|
||||
// 创建临时决策日志记录器
|
||||
s.mockLogger = logger.NewDecisionLogger("/tmp/test_decision_logs")
|
||||
// 创建临时store(使用nil表示测试中不需要实际的store)
|
||||
s.mockStore = nil
|
||||
|
||||
// 设置默认配置
|
||||
s.config = AutoTraderConfig{
|
||||
@@ -93,7 +91,7 @@ func (s *AutoTraderTestSuite) SetupTest() {
|
||||
config: s.config,
|
||||
trader: s.mockTrader,
|
||||
mcpClient: nil, // 测试中不需要实际的 MCP Client
|
||||
decisionLogger: s.mockLogger,
|
||||
store: s.mockStore,
|
||||
initialBalance: s.config.InitialBalance,
|
||||
systemPromptTemplate: s.config.SystemPromptTemplate,
|
||||
defaultCoins: []string{"BTC", "ETH"},
|
||||
@@ -106,7 +104,6 @@ func (s *AutoTraderTestSuite) SetupTest() {
|
||||
stopMonitorCh: make(chan struct{}),
|
||||
peakPnLCache: make(map[string]float64),
|
||||
lastBalanceSyncTime: time.Now(),
|
||||
database: s.mockDB,
|
||||
userID: "test_user",
|
||||
}
|
||||
}
|
||||
@@ -134,9 +131,8 @@ func (s *AutoTraderTestSuite) TestSortDecisionsByPriority() {
|
||||
{Action: "open_long", Symbol: "BTCUSDT"},
|
||||
{Action: "close_short", Symbol: "ETHUSDT"},
|
||||
{Action: "hold", Symbol: "BNBUSDT"},
|
||||
{Action: "update_stop_loss", Symbol: "SOLUSDT"},
|
||||
{Action: "open_short", Symbol: "ADAUSDT"},
|
||||
{Action: "partial_close", Symbol: "DOGEUSDT"},
|
||||
{Action: "close_long", Symbol: "DOGEUSDT"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -150,14 +146,12 @@ func (s *AutoTraderTestSuite) TestSortDecisionsByPriority() {
|
||||
// 验证优先级是否递增
|
||||
getActionPriority := func(action string) int {
|
||||
switch action {
|
||||
case "close_long", "close_short", "partial_close":
|
||||
case "close_long", "close_short":
|
||||
return 1
|
||||
case "update_stop_loss", "update_take_profit":
|
||||
return 2
|
||||
case "open_long", "open_short":
|
||||
return 3
|
||||
return 2
|
||||
case "hold", "wait":
|
||||
return 4
|
||||
return 3
|
||||
default:
|
||||
return 999
|
||||
}
|
||||
@@ -413,14 +407,14 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() {
|
||||
existingSide string
|
||||
availBalance float64
|
||||
expectedErr string
|
||||
executeFn func(*decision.Decision, *logger.DecisionAction) error
|
||||
executeFn func(*decision.Decision, *store.DecisionAction) error
|
||||
}{
|
||||
{
|
||||
name: "成功开多仓",
|
||||
action: "open_long",
|
||||
expectedOrder: 123456,
|
||||
availBalance: 8000.0,
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
executeFn: func(d *decision.Decision, a *store.DecisionAction) error {
|
||||
return s.autoTrader.executeOpenLongWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
@@ -429,7 +423,7 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() {
|
||||
action: "open_short",
|
||||
expectedOrder: 123457,
|
||||
availBalance: 8000.0,
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
executeFn: func(d *decision.Decision, a *store.DecisionAction) error {
|
||||
return s.autoTrader.executeOpenShortWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
@@ -438,7 +432,7 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() {
|
||||
action: "open_long",
|
||||
availBalance: 0.0,
|
||||
expectedErr: "保证金不足",
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
executeFn: func(d *decision.Decision, a *store.DecisionAction) error {
|
||||
return s.autoTrader.executeOpenLongWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
@@ -447,7 +441,7 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() {
|
||||
action: "open_short",
|
||||
availBalance: 0.0,
|
||||
expectedErr: "保证金不足",
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
executeFn: func(d *decision.Decision, a *store.DecisionAction) error {
|
||||
return s.autoTrader.executeOpenShortWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
@@ -457,7 +451,7 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() {
|
||||
existingSide: "long",
|
||||
availBalance: 8000.0,
|
||||
expectedErr: "已有多仓",
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
executeFn: func(d *decision.Decision, a *store.DecisionAction) error {
|
||||
return s.autoTrader.executeOpenLongWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
@@ -467,7 +461,7 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() {
|
||||
existingSide: "short",
|
||||
availBalance: 8000.0,
|
||||
expectedErr: "已有空仓",
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
executeFn: func(d *decision.Decision, a *store.DecisionAction) error {
|
||||
return s.autoTrader.executeOpenShortWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
@@ -488,7 +482,7 @@ func (s *AutoTraderTestSuite) TestExecuteOpenPosition() {
|
||||
}
|
||||
|
||||
decision := &decision.Decision{Action: tt.action, Symbol: "BTCUSDT", PositionSizeUSD: 1000.0, Leverage: 10}
|
||||
actionRecord := &logger.DecisionAction{Action: tt.action, Symbol: "BTCUSDT"}
|
||||
actionRecord := &store.DecisionAction{Action: tt.action, Symbol: "BTCUSDT"}
|
||||
|
||||
err := tt.executeFn(decision, actionRecord)
|
||||
|
||||
@@ -516,14 +510,14 @@ func (s *AutoTraderTestSuite) TestExecuteClosePosition() {
|
||||
action string
|
||||
currentPrice float64
|
||||
expectedOrder int64
|
||||
executeFn func(*decision.Decision, *logger.DecisionAction) error
|
||||
executeFn func(*decision.Decision, *store.DecisionAction) error
|
||||
}{
|
||||
{
|
||||
name: "成功平多仓",
|
||||
action: "close_long",
|
||||
currentPrice: 51000.0,
|
||||
expectedOrder: 123458,
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
executeFn: func(d *decision.Decision, a *store.DecisionAction) error {
|
||||
return s.autoTrader.executeCloseLongWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
@@ -532,7 +526,7 @@ func (s *AutoTraderTestSuite) TestExecuteClosePosition() {
|
||||
action: "close_short",
|
||||
currentPrice: 49000.0,
|
||||
expectedOrder: 123459,
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
executeFn: func(d *decision.Decision, a *store.DecisionAction) error {
|
||||
return s.autoTrader.executeCloseShortWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
@@ -546,7 +540,7 @@ func (s *AutoTraderTestSuite) TestExecuteClosePosition() {
|
||||
})
|
||||
|
||||
decision := &decision.Decision{Action: tt.action, Symbol: "BTCUSDT"}
|
||||
actionRecord := &logger.DecisionAction{Action: tt.action, Symbol: "BTCUSDT"}
|
||||
actionRecord := &store.DecisionAction{Action: tt.action, Symbol: "BTCUSDT"}
|
||||
|
||||
err := tt.executeFn(decision, actionRecord)
|
||||
|
||||
@@ -557,221 +551,6 @@ func (s *AutoTraderTestSuite) TestExecuteClosePosition() {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteUpdateStopOrTakeProfit 测试更新止损/止盈(多空通用)
|
||||
func (s *AutoTraderTestSuite) TestExecuteUpdateStopOrTakeProfit() {
|
||||
// 使用指针变量来控制 market.Get 的返回值
|
||||
var testPrice *float64
|
||||
s.patches.ApplyFunc(market.Get, func(symbol string) (*market.Data, error) {
|
||||
price := 50000.0
|
||||
if testPrice != nil {
|
||||
price = *testPrice
|
||||
}
|
||||
return &market.Data{Symbol: symbol, CurrentPrice: price}, nil
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
action string
|
||||
symbol string
|
||||
side string
|
||||
currentPrice float64
|
||||
newPrice float64
|
||||
hasPosition bool
|
||||
expectedErr string
|
||||
executeFn func(*decision.Decision, *logger.DecisionAction) error
|
||||
}{
|
||||
{
|
||||
name: "成功更新多头止损",
|
||||
action: "update_stop_loss",
|
||||
symbol: "BTCUSDT",
|
||||
side: "long",
|
||||
currentPrice: 52000.0,
|
||||
newPrice: 51000.0,
|
||||
hasPosition: true,
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
return s.autoTrader.executeUpdateStopLossWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "成功更新空头止损",
|
||||
action: "update_stop_loss",
|
||||
symbol: "ETHUSDT",
|
||||
side: "short",
|
||||
currentPrice: 2900.0,
|
||||
newPrice: 2950.0,
|
||||
hasPosition: true,
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
return s.autoTrader.executeUpdateStopLossWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "成功更新多头止盈",
|
||||
action: "update_take_profit",
|
||||
symbol: "BTCUSDT",
|
||||
side: "long",
|
||||
currentPrice: 52000.0,
|
||||
newPrice: 55000.0,
|
||||
hasPosition: true,
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
return s.autoTrader.executeUpdateTakeProfitWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "成功更新空头止盈",
|
||||
action: "update_take_profit",
|
||||
symbol: "ETHUSDT",
|
||||
side: "short",
|
||||
currentPrice: 2900.0,
|
||||
newPrice: 2800.0,
|
||||
hasPosition: true,
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
return s.autoTrader.executeUpdateTakeProfitWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "多头止损价格不合理",
|
||||
action: "update_stop_loss",
|
||||
symbol: "BTCUSDT",
|
||||
side: "long",
|
||||
currentPrice: 50000.0,
|
||||
newPrice: 51000.0,
|
||||
hasPosition: true,
|
||||
expectedErr: "多单止损必须低于当前价格",
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
return s.autoTrader.executeUpdateStopLossWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "多头止盈价格不合理",
|
||||
action: "update_take_profit",
|
||||
symbol: "BTCUSDT",
|
||||
side: "long",
|
||||
currentPrice: 50000.0,
|
||||
newPrice: 49000.0,
|
||||
hasPosition: true,
|
||||
expectedErr: "多单止盈必须高于当前价格",
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
return s.autoTrader.executeUpdateTakeProfitWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "止损_持仓不存在",
|
||||
action: "update_stop_loss",
|
||||
symbol: "BTCUSDT",
|
||||
currentPrice: 50000.0,
|
||||
newPrice: 49000.0,
|
||||
hasPosition: false,
|
||||
expectedErr: "持仓不存在",
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
return s.autoTrader.executeUpdateStopLossWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "止盈_持仓不存在",
|
||||
action: "update_take_profit",
|
||||
symbol: "BTCUSDT",
|
||||
currentPrice: 50000.0,
|
||||
newPrice: 55000.0,
|
||||
hasPosition: false,
|
||||
expectedErr: "持仓不存在",
|
||||
executeFn: func(d *decision.Decision, a *logger.DecisionAction) error {
|
||||
return s.autoTrader.executeUpdateTakeProfitWithRecord(d, a)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
time.Sleep(time.Millisecond)
|
||||
s.Run(tt.name, func() {
|
||||
// 设置当前测试用例的价格
|
||||
testPrice = &tt.currentPrice
|
||||
|
||||
if tt.hasPosition {
|
||||
s.mockTrader.positions = []map[string]interface{}{
|
||||
{"symbol": tt.symbol, "side": tt.side, "positionAmt": 0.1},
|
||||
}
|
||||
} else {
|
||||
s.mockTrader.positions = []map[string]interface{}{}
|
||||
}
|
||||
|
||||
decision := &decision.Decision{Action: tt.action, Symbol: tt.symbol}
|
||||
if tt.action == "update_stop_loss" {
|
||||
decision.NewStopLoss = tt.newPrice
|
||||
} else {
|
||||
decision.NewTakeProfit = tt.newPrice
|
||||
}
|
||||
actionRecord := &logger.DecisionAction{Action: tt.action, Symbol: tt.symbol}
|
||||
|
||||
err := tt.executeFn(decision, actionRecord)
|
||||
|
||||
if tt.expectedErr != "" {
|
||||
s.Error(err)
|
||||
s.Contains(err.Error(), tt.expectedErr)
|
||||
} else {
|
||||
s.NoError(err)
|
||||
s.Equal(tt.currentPrice, actionRecord.Price)
|
||||
}
|
||||
|
||||
// 恢复默认状态
|
||||
s.mockTrader.positions = []map[string]interface{}{}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AutoTraderTestSuite) TestExecutePartialCloseWithRecord() {
|
||||
s.Run("成功部分平仓", func() {
|
||||
// 设置持仓
|
||||
s.mockTrader.positions = []map[string]interface{}{
|
||||
{
|
||||
"symbol": "BTCUSDT",
|
||||
"side": "long",
|
||||
"positionAmt": 0.1,
|
||||
"entryPrice": 50000.0,
|
||||
"markPrice": 52000.0,
|
||||
},
|
||||
}
|
||||
|
||||
// Mock market.Get
|
||||
s.patches.ApplyFunc(market.Get, func(symbol string) (*market.Data, error) {
|
||||
return &market.Data{
|
||||
Symbol: symbol,
|
||||
CurrentPrice: 52000.0,
|
||||
}, nil
|
||||
})
|
||||
|
||||
decision := &decision.Decision{
|
||||
Action: "partial_close",
|
||||
Symbol: "BTCUSDT",
|
||||
ClosePercentage: 50.0,
|
||||
}
|
||||
|
||||
actionRecord := &logger.DecisionAction{
|
||||
Action: "partial_close",
|
||||
Symbol: "BTCUSDT",
|
||||
}
|
||||
|
||||
err := s.autoTrader.executePartialCloseWithRecord(decision, actionRecord)
|
||||
|
||||
s.NoError(err)
|
||||
s.Equal(0.05, actionRecord.Quantity) // 50% of 0.1
|
||||
})
|
||||
|
||||
s.Run("无效的平仓百分比", func() {
|
||||
decision := &decision.Decision{
|
||||
Action: "partial_close",
|
||||
Symbol: "BTCUSDT",
|
||||
ClosePercentage: 150.0, // 无效
|
||||
}
|
||||
|
||||
actionRecord := &logger.DecisionAction{}
|
||||
|
||||
err := s.autoTrader.executePartialCloseWithRecord(decision, actionRecord)
|
||||
|
||||
s.Error(err)
|
||||
s.Contains(err.Error(), "平仓百分比必须在 0-100 之间")
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 层次 10: executeDecisionWithRecord 路由测试
|
||||
// ============================================================
|
||||
@@ -792,7 +571,7 @@ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() {
|
||||
PositionSizeUSD: 1000.0,
|
||||
Leverage: 10,
|
||||
}
|
||||
actionRecord := &logger.DecisionAction{}
|
||||
actionRecord := &store.DecisionAction{}
|
||||
|
||||
err := s.autoTrader.executeDecisionWithRecord(decision, actionRecord)
|
||||
s.NoError(err)
|
||||
@@ -803,7 +582,7 @@ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() {
|
||||
Action: "close_long",
|
||||
Symbol: "BTCUSDT",
|
||||
}
|
||||
actionRecord := &logger.DecisionAction{}
|
||||
actionRecord := &store.DecisionAction{}
|
||||
|
||||
err := s.autoTrader.executeDecisionWithRecord(decision, actionRecord)
|
||||
s.NoError(err)
|
||||
@@ -814,7 +593,7 @@ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() {
|
||||
Action: "hold",
|
||||
Symbol: "BTCUSDT",
|
||||
}
|
||||
actionRecord := &logger.DecisionAction{}
|
||||
actionRecord := &store.DecisionAction{}
|
||||
|
||||
err := s.autoTrader.executeDecisionWithRecord(decision, actionRecord)
|
||||
s.NoError(err)
|
||||
@@ -825,7 +604,7 @@ func (s *AutoTraderTestSuite) TestExecuteDecisionWithRecord() {
|
||||
Action: "unknown_action",
|
||||
Symbol: "BTCUSDT",
|
||||
}
|
||||
actionRecord := &logger.DecisionAction{}
|
||||
actionRecord := &store.DecisionAction{}
|
||||
|
||||
err := s.autoTrader.executeDecisionWithRecord(decision, actionRecord)
|
||||
s.Error(err)
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/hook"
|
||||
"nofx/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -80,7 +80,7 @@ func NewFuturesTrader(apiKey, secretKey string, userId string) *FuturesTrader {
|
||||
// 设置双向持仓模式(Hedge Mode)
|
||||
// 这是必需的,因为代码中使用了 PositionSide (LONG/SHORT)
|
||||
if err := trader.setDualSidePosition(); err != nil {
|
||||
log.Printf("⚠️ 设置双向持仓模式失败: %v (如果已是双向模式则忽略此警告)", err)
|
||||
logger.Infof("⚠️ 设置双向持仓模式失败: %v (如果已是双向模式则忽略此警告)", err)
|
||||
}
|
||||
|
||||
return trader
|
||||
@@ -96,15 +96,15 @@ func (t *FuturesTrader) setDualSidePosition() error {
|
||||
if err != nil {
|
||||
// 如果错误信息包含"No need to change",说明已经是双向持仓模式
|
||||
if strings.Contains(err.Error(), "No need to change position side") {
|
||||
log.Printf(" ✓ 账户已是双向持仓模式(Hedge Mode)")
|
||||
logger.Infof(" ✓ 账户已是双向持仓模式(Hedge Mode)")
|
||||
return nil
|
||||
}
|
||||
// 其他错误则返回(但在调用方不会中断初始化)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf(" ✓ 账户已切换为双向持仓模式(Hedge Mode)")
|
||||
log.Printf(" ℹ️ 双向持仓模式允许同时持有多单和空单")
|
||||
logger.Infof(" ✓ 账户已切换为双向持仓模式(Hedge Mode)")
|
||||
logger.Infof(" ℹ️ 双向持仓模式允许同时持有多单和空单")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -112,14 +112,14 @@ func (t *FuturesTrader) setDualSidePosition() error {
|
||||
func syncBinanceServerTime(client *futures.Client) {
|
||||
serverTime, err := client.NewServerTimeService().Do(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 同步币安服务器时间失败: %v", err)
|
||||
logger.Infof("⚠️ 同步币安服务器时间失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
offset := now - serverTime
|
||||
client.TimeOffset = offset
|
||||
log.Printf("⏱ 已同步币安服务器时间,偏移 %dms", offset)
|
||||
logger.Infof("⏱ 已同步币安服务器时间,偏移 %dms", offset)
|
||||
}
|
||||
|
||||
// GetBalance 获取账户余额(带缓存)
|
||||
@@ -129,16 +129,16 @@ func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) {
|
||||
if t.cachedBalance != nil && time.Since(t.balanceCacheTime) < t.cacheDuration {
|
||||
cacheAge := time.Since(t.balanceCacheTime)
|
||||
t.balanceCacheMutex.RUnlock()
|
||||
log.Printf("✓ 使用缓存的账户余额(缓存时间: %.1f秒前)", cacheAge.Seconds())
|
||||
logger.Infof("✓ 使用缓存的账户余额(缓存时间: %.1f秒前)", cacheAge.Seconds())
|
||||
return t.cachedBalance, nil
|
||||
}
|
||||
t.balanceCacheMutex.RUnlock()
|
||||
|
||||
// 缓存过期或不存在,调用API
|
||||
log.Printf("🔄 缓存过期,正在调用币安API获取账户余额...")
|
||||
logger.Infof("🔄 缓存过期,正在调用币安API获取账户余额...")
|
||||
account, err := t.client.NewGetAccountService().Do(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("❌ 币安API调用失败: %v", err)
|
||||
logger.Infof("❌ 币安API调用失败: %v", err)
|
||||
return nil, fmt.Errorf("获取账户信息失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) {
|
||||
result["availableBalance"], _ = strconv.ParseFloat(account.AvailableBalance, 64)
|
||||
result["totalUnrealizedProfit"], _ = strconv.ParseFloat(account.TotalUnrealizedProfit, 64)
|
||||
|
||||
log.Printf("✓ 币安API返回: 总余额=%s, 可用=%s, 未实现盈亏=%s",
|
||||
logger.Infof("✓ 币安API返回: 总余额=%s, 可用=%s, 未实现盈亏=%s",
|
||||
account.TotalWalletBalance,
|
||||
account.AvailableBalance,
|
||||
account.TotalUnrealizedProfit)
|
||||
@@ -168,13 +168,13 @@ func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration {
|
||||
cacheAge := time.Since(t.positionsCacheTime)
|
||||
t.positionsCacheMutex.RUnlock()
|
||||
log.Printf("✓ 使用缓存的持仓信息(缓存时间: %.1f秒前)", cacheAge.Seconds())
|
||||
logger.Infof("✓ 使用缓存的持仓信息(缓存时间: %.1f秒前)", cacheAge.Seconds())
|
||||
return t.cachedPositions, nil
|
||||
}
|
||||
t.positionsCacheMutex.RUnlock()
|
||||
|
||||
// 缓存过期或不存在,调用API
|
||||
log.Printf("🔄 缓存过期,正在调用币安API获取持仓信息...")
|
||||
logger.Infof("🔄 缓存过期,正在调用币安API获取持仓信息...")
|
||||
positions, err := t.client.NewGetPositionRiskService().Do(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取持仓失败: %w", err)
|
||||
@@ -238,31 +238,31 @@ func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
|
||||
if err != nil {
|
||||
// 如果错误信息包含"No need to change",说明仓位模式已经是目标值
|
||||
if contains(err.Error(), "No need to change margin type") {
|
||||
log.Printf(" ✓ %s 仓位模式已是 %s", symbol, marginModeStr)
|
||||
logger.Infof(" ✓ %s 仓位模式已是 %s", symbol, marginModeStr)
|
||||
return nil
|
||||
}
|
||||
// 如果有持仓,无法更改仓位模式,但不影响交易
|
||||
if contains(err.Error(), "Margin type cannot be changed if there exists position") {
|
||||
log.Printf(" ⚠️ %s 有持仓,无法更改仓位模式,继续使用当前模式", symbol)
|
||||
logger.Infof(" ⚠️ %s 有持仓,无法更改仓位模式,继续使用当前模式", symbol)
|
||||
return nil
|
||||
}
|
||||
// 检测多资产模式(错误码 -4168)
|
||||
if contains(err.Error(), "Multi-Assets mode") || contains(err.Error(), "-4168") || contains(err.Error(), "4168") {
|
||||
log.Printf(" ⚠️ %s 检测到多资产模式,强制使用全仓模式", symbol)
|
||||
log.Printf(" 💡 提示:如需使用逐仓模式,请在币安关闭多资产模式")
|
||||
logger.Infof(" ⚠️ %s 检测到多资产模式,强制使用全仓模式", symbol)
|
||||
logger.Infof(" 💡 提示:如需使用逐仓模式,请在币安关闭多资产模式")
|
||||
return nil
|
||||
}
|
||||
// 检测统一账户 API(Portfolio Margin)
|
||||
if contains(err.Error(), "unified") || contains(err.Error(), "portfolio") || contains(err.Error(), "Portfolio") {
|
||||
log.Printf(" ❌ %s 检测到统一账户 API,无法进行合约交易", symbol)
|
||||
logger.Infof(" ❌ %s 检测到统一账户 API,无法进行合约交易", symbol)
|
||||
return fmt.Errorf("请使用「现货与合约交易」API 权限,不要使用「统一账户 API」")
|
||||
}
|
||||
log.Printf(" ⚠️ 设置仓位模式失败: %v", err)
|
||||
logger.Infof(" ⚠️ 设置仓位模式失败: %v", err)
|
||||
// 不返回错误,让交易继续
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf(" ✓ %s 仓位模式已设置为 %s", symbol, marginModeStr)
|
||||
logger.Infof(" ✓ %s 仓位模式已设置为 %s", symbol, marginModeStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error {
|
||||
|
||||
// 如果当前杠杆已经是目标杠杆,跳过
|
||||
if currentLeverage == leverage && currentLeverage > 0 {
|
||||
log.Printf(" ✓ %s 杠杆已是 %dx,无需切换", symbol, leverage)
|
||||
logger.Infof(" ✓ %s 杠杆已是 %dx,无需切换", symbol, leverage)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -297,16 +297,16 @@ func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error {
|
||||
if err != nil {
|
||||
// 如果错误信息包含"No need to change",说明杠杆已经是目标值
|
||||
if contains(err.Error(), "No need to change") {
|
||||
log.Printf(" ✓ %s 杠杆已是 %dx", symbol, leverage)
|
||||
logger.Infof(" ✓ %s 杠杆已是 %dx", symbol, leverage)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("设置杠杆失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ %s 杠杆已切换为 %dx", symbol, leverage)
|
||||
logger.Infof(" ✓ %s 杠杆已切换为 %dx", symbol, leverage)
|
||||
|
||||
// 切换杠杆后等待5秒(避免冷却期错误)
|
||||
log.Printf(" ⏱ 等待5秒冷却期...")
|
||||
logger.Infof(" ⏱ 等待5秒冷却期...")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
return nil
|
||||
@@ -316,7 +316,7 @@ func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error {
|
||||
func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 先取消该币种的所有委托单(清理旧的止损止盈单)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err)
|
||||
logger.Infof(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err)
|
||||
}
|
||||
|
||||
// 设置杠杆
|
||||
@@ -357,8 +357,8 @@ func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int)
|
||||
return nil, fmt.Errorf("开多仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 开多仓成功: %s 数量: %s", symbol, quantityStr)
|
||||
log.Printf(" 订单ID: %d", order.OrderID)
|
||||
logger.Infof("✓ 开多仓成功: %s 数量: %s", symbol, quantityStr)
|
||||
logger.Infof(" 订单ID: %d", order.OrderID)
|
||||
|
||||
result := make(map[string]interface{})
|
||||
result["orderId"] = order.OrderID
|
||||
@@ -371,7 +371,7 @@ func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int)
|
||||
func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 先取消该币种的所有委托单(清理旧的止损止盈单)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err)
|
||||
logger.Infof(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err)
|
||||
}
|
||||
|
||||
// 设置杠杆
|
||||
@@ -412,8 +412,8 @@ func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int)
|
||||
return nil, fmt.Errorf("开空仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 开空仓成功: %s 数量: %s", symbol, quantityStr)
|
||||
log.Printf(" 订单ID: %d", order.OrderID)
|
||||
logger.Infof("✓ 开空仓成功: %s 数量: %s", symbol, quantityStr)
|
||||
logger.Infof(" 订单ID: %d", order.OrderID)
|
||||
|
||||
result := make(map[string]interface{})
|
||||
result["orderId"] = order.OrderID
|
||||
@@ -463,11 +463,11 @@ func (t *FuturesTrader) CloseLong(symbol string, quantity float64) (map[string]i
|
||||
return nil, fmt.Errorf("平多仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 平多仓成功: %s 数量: %s", symbol, quantityStr)
|
||||
logger.Infof("✓ 平多仓成功: %s 数量: %s", symbol, quantityStr)
|
||||
|
||||
// 平仓后取消该币种的所有挂单(止损止盈单)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
@@ -518,11 +518,11 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string]
|
||||
return nil, fmt.Errorf("平空仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 平空仓成功: %s 数量: %s", symbol, quantityStr)
|
||||
logger.Infof("✓ 平空仓成功: %s 数量: %s", symbol, quantityStr)
|
||||
|
||||
// 平仓后取消该币种的所有挂单(止损止盈单)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
@@ -559,19 +559,19 @@ func (t *FuturesTrader) CancelStopLossOrders(symbol string) error {
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("订单ID %d: %v", order.OrderID, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
log.Printf(" ⚠ 取消止损单失败: %s", errMsg)
|
||||
logger.Infof(" ⚠ 取消止损单失败: %s", errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消止损单 (订单ID: %d, 类型: %s, 方向: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
logger.Infof(" ✓ 已取消止损单 (订单ID: %d, 类型: %s, 方向: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 && len(cancelErrors) == 0 {
|
||||
log.Printf(" ℹ %s 没有止损单需要取消", symbol)
|
||||
logger.Infof(" ℹ %s 没有止损单需要取消", symbol)
|
||||
} else if canceledCount > 0 {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount)
|
||||
logger.Infof(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
// 如果所有取消都失败了,返回错误
|
||||
@@ -609,19 +609,19 @@ func (t *FuturesTrader) CancelTakeProfitOrders(symbol string) error {
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("订单ID %d: %v", order.OrderID, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
log.Printf(" ⚠ 取消止盈单失败: %s", errMsg)
|
||||
logger.Infof(" ⚠ 取消止盈单失败: %s", errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s, 方向: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
logger.Infof(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s, 方向: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 && len(cancelErrors) == 0 {
|
||||
log.Printf(" ℹ %s 没有止盈单需要取消", symbol)
|
||||
logger.Infof(" ℹ %s 没有止盈单需要取消", symbol)
|
||||
} else if canceledCount > 0 {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount)
|
||||
logger.Infof(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
// 如果所有取消都失败了,返回错误
|
||||
@@ -642,7 +642,7 @@ func (t *FuturesTrader) CancelAllOrders(symbol string) error {
|
||||
return fmt.Errorf("取消挂单失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ 已取消 %s 的所有挂单", symbol)
|
||||
logger.Infof(" ✓ 已取消 %s 的所有挂单", symbol)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -674,20 +674,20 @@ func (t *FuturesTrader) CancelStopOrders(symbol string) error {
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ 取消订单 %d 失败: %v", order.OrderID, err)
|
||||
logger.Infof(" ⚠ 取消订单 %d 失败: %v", order.OrderID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)",
|
||||
logger.Infof(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)",
|
||||
symbol, order.OrderID, orderType)
|
||||
}
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol)
|
||||
logger.Infof(" ℹ %s 没有止盈/止损单需要取消", symbol)
|
||||
} else {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount)
|
||||
logger.Infof(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -748,13 +748,14 @@ func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity
|
||||
Quantity(quantityStr).
|
||||
WorkingType(futures.WorkingTypeContractPrice).
|
||||
ClosePosition(true).
|
||||
NewClientOrderID(getBrOrderID()).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置止损失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" 止损价设置: %.4f", stopPrice)
|
||||
logger.Infof(" 止损价设置: %.4f", stopPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -786,13 +787,14 @@ func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quanti
|
||||
Quantity(quantityStr).
|
||||
WorkingType(futures.WorkingTypeContractPrice).
|
||||
ClosePosition(true).
|
||||
NewClientOrderID(getBrOrderID()).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置止盈失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" 止盈价设置: %.4f", takeProfitPrice)
|
||||
logger.Infof(" 止盈价设置: %.4f", takeProfitPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -836,14 +838,14 @@ func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) {
|
||||
if filter["filterType"] == "LOT_SIZE" {
|
||||
stepSize := filter["stepSize"].(string)
|
||||
precision := calculatePrecision(stepSize)
|
||||
log.Printf(" %s 数量精度: %d (stepSize: %s)", symbol, precision, stepSize)
|
||||
logger.Infof(" %s 数量精度: %d (stepSize: %s)", symbol, precision, stepSize)
|
||||
return precision, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf(" ⚠ %s 未找到精度信息,使用默认精度3", symbol)
|
||||
logger.Infof(" ⚠ %s 未找到精度信息,使用默认精度3", symbol)
|
||||
return 3, nil // 默认精度为3
|
||||
}
|
||||
|
||||
@@ -915,3 +917,42 @@ func stringContains(s, substr string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetOrderStatus 获取订单状态
|
||||
func (t *FuturesTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) {
|
||||
// 将 orderID 转换为 int64
|
||||
orderIDInt, err := strconv.ParseInt(orderID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的订单ID: %s", orderID)
|
||||
}
|
||||
|
||||
order, err := t.client.NewGetOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(orderIDInt).
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取订单状态失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析成交价格
|
||||
avgPrice, _ := strconv.ParseFloat(order.AvgPrice, 64)
|
||||
executedQty, _ := strconv.ParseFloat(order.ExecutedQuantity, 64)
|
||||
|
||||
result := map[string]interface{}{
|
||||
"orderId": order.OrderID,
|
||||
"symbol": order.Symbol,
|
||||
"status": string(order.Status),
|
||||
"avgPrice": avgPrice,
|
||||
"executedQty": executedQty,
|
||||
"side": string(order.Side),
|
||||
"type": string(order.Type),
|
||||
"time": order.Time,
|
||||
"updateTime": order.UpdateTime,
|
||||
}
|
||||
|
||||
// 币安合约的手续费需要通过 GetUserTrades 获取,这里暂时不获取
|
||||
// 后续可以通过 WebSocket 或单独查询获取
|
||||
result["commission"] = 0.0
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package trader
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -55,7 +55,7 @@ func NewBybitTrader(apiKey, secretKey string) *BybitTrader {
|
||||
cacheDuration: 15 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("🔵 [Bybit] 交易器已初始化")
|
||||
logger.Infof("🔵 [Bybit] 交易器已初始化")
|
||||
|
||||
return trader
|
||||
}
|
||||
@@ -224,7 +224,7 @@ func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
func (t *BybitTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 先设置杠杆
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
log.Printf("⚠️ [Bybit] 设置杠杆失败: %v", err)
|
||||
logger.Infof("⚠️ [Bybit] 设置杠杆失败: %v", err)
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -251,7 +251,7 @@ func (t *BybitTrader) OpenLong(symbol string, quantity float64, leverage int) (m
|
||||
func (t *BybitTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 先设置杠杆
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
log.Printf("⚠️ [Bybit] 设置杠杆失败: %v", err)
|
||||
logger.Infof("⚠️ [Bybit] 设置杠杆失败: %v", err)
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -485,7 +485,7 @@ func (t *BybitTrader) SetStopLoss(symbol string, positionSide string, quantity,
|
||||
return fmt.Errorf("设置止损失败: %s", result.RetMsg)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ [Bybit] 止损单已设置: %s @ %.2f", symbol, stopPrice)
|
||||
logger.Infof(" ✓ [Bybit] 止损单已设置: %s @ %.2f", symbol, stopPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -528,7 +528,7 @@ func (t *BybitTrader) SetTakeProfit(symbol string, positionSide string, quantity
|
||||
return fmt.Errorf("设置止盈失败: %s", result.RetMsg)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ [Bybit] 止盈单已设置: %s @ %.2f", symbol, takeProfitPrice)
|
||||
logger.Infof(" ✓ [Bybit] 止盈单已设置: %s @ %.2f", symbol, takeProfitPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -560,10 +560,10 @@ func (t *BybitTrader) CancelAllOrders(symbol string) error {
|
||||
// CancelStopOrders 取消所有止盈止损单
|
||||
func (t *BybitTrader) CancelStopOrders(symbol string) error {
|
||||
if err := t.CancelStopLossOrders(symbol); err != nil {
|
||||
log.Printf("⚠️ [Bybit] 取消止损单失败: %v", err)
|
||||
logger.Infof("⚠️ [Bybit] 取消止损单失败: %v", err)
|
||||
}
|
||||
if err := t.CancelTakeProfitOrders(symbol); err != nil {
|
||||
log.Printf("⚠️ [Bybit] 取消止盈单失败: %v", err)
|
||||
logger.Infof("⚠️ [Bybit] 取消止盈单失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -604,6 +604,67 @@ func (t *BybitTrader) parseOrderResult(result *bybit.ServerResponse) (map[string
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetOrderStatus 获取订单状态
|
||||
func (t *BybitTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) {
|
||||
params := map[string]interface{}{
|
||||
"category": "linear",
|
||||
"symbol": symbol,
|
||||
"orderId": orderID,
|
||||
}
|
||||
|
||||
result, err := t.client.NewUtaBybitServiceWithParams(params).GetOrderHistory(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取订单状态失败: %w", err)
|
||||
}
|
||||
|
||||
if result.RetCode != 0 {
|
||||
return nil, fmt.Errorf("API 错误: %s", result.RetMsg)
|
||||
}
|
||||
|
||||
resultData, ok := result.Result.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("返回格式错误")
|
||||
}
|
||||
|
||||
list, _ := resultData["list"].([]interface{})
|
||||
if len(list) == 0 {
|
||||
return nil, fmt.Errorf("未找到订单 %s", orderID)
|
||||
}
|
||||
|
||||
order, _ := list[0].(map[string]interface{})
|
||||
|
||||
// 解析订单数据
|
||||
status, _ := order["orderStatus"].(string)
|
||||
avgPriceStr, _ := order["avgPrice"].(string)
|
||||
cumExecQtyStr, _ := order["cumExecQty"].(string)
|
||||
cumExecFeeStr, _ := order["cumExecFee"].(string)
|
||||
|
||||
avgPrice, _ := strconv.ParseFloat(avgPriceStr, 64)
|
||||
executedQty, _ := strconv.ParseFloat(cumExecQtyStr, 64)
|
||||
commission, _ := strconv.ParseFloat(cumExecFeeStr, 64)
|
||||
|
||||
// 转换状态为统一格式
|
||||
unifiedStatus := status
|
||||
switch status {
|
||||
case "Filled":
|
||||
unifiedStatus = "FILLED"
|
||||
case "New", "Created":
|
||||
unifiedStatus = "NEW"
|
||||
case "Cancelled", "Rejected":
|
||||
unifiedStatus = "CANCELED"
|
||||
case "PartiallyFilled":
|
||||
unifiedStatus = "PARTIALLY_FILLED"
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"orderId": orderID,
|
||||
"status": unifiedStatus,
|
||||
"avgPrice": avgPrice,
|
||||
"executedQty": executedQty,
|
||||
"commission": commission,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *BybitTrader) cancelConditionalOrders(symbol string, orderType string) error {
|
||||
// 先获取所有条件单
|
||||
params := map[string]interface{}{
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -56,14 +56,14 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
||||
|
||||
// Check if user accidentally uses main wallet private key (security risk)
|
||||
if strings.EqualFold(walletAddr, agentAddr) {
|
||||
log.Printf("⚠️⚠️⚠️ WARNING: Main wallet address (%s) matches Agent wallet address!", walletAddr)
|
||||
log.Printf(" This indicates you may be using your main wallet private key, which poses extremely high security risks!")
|
||||
log.Printf(" Recommendation: Immediately create a separate Agent Wallet on Hyperliquid official website")
|
||||
log.Printf(" Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets")
|
||||
logger.Infof("⚠️⚠️⚠️ WARNING: Main wallet address (%s) matches Agent wallet address!", walletAddr)
|
||||
logger.Infof(" This indicates you may be using your main wallet private key, which poses extremely high security risks!")
|
||||
logger.Infof(" Recommendation: Immediately create a separate Agent Wallet on Hyperliquid official website")
|
||||
logger.Infof(" Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets")
|
||||
} else {
|
||||
log.Printf("✓ Using Agent Wallet mode (secure)")
|
||||
log.Printf(" └─ Agent wallet address: %s (for signing)", agentAddr)
|
||||
log.Printf(" └─ Main wallet address: %s (holds funds)", walletAddr)
|
||||
logger.Infof("✓ Using Agent Wallet mode (secure)")
|
||||
logger.Infof(" └─ Agent wallet address: %s (for signing)", agentAddr)
|
||||
logger.Infof(" └─ Main wallet address: %s (holds funds)", walletAddr)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -79,7 +79,7 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
||||
nil, // SpotMeta will be fetched automatically
|
||||
)
|
||||
|
||||
log.Printf("✓ Hyperliquid交易器初始化成功 (testnet=%v, wallet=%s)", testnet, walletAddr)
|
||||
logger.Infof("✓ Hyperliquid交易器初始化成功 (testnet=%v, wallet=%s)", testnet, walletAddr)
|
||||
|
||||
// 获取meta信息(包含精度等配置)
|
||||
meta, err := exchange.Info().Meta(ctx)
|
||||
@@ -97,26 +97,26 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
||||
|
||||
if agentBalance > 100 {
|
||||
// Critical: Agent wallet holds too much funds
|
||||
log.Printf("🚨🚨🚨 CRITICAL SECURITY WARNING 🚨🚨🚨")
|
||||
log.Printf(" Agent wallet balance: %.2f USDC (exceeds safe threshold of 100 USDC)", agentBalance)
|
||||
log.Printf(" Agent wallet address: %s", agentAddr)
|
||||
log.Printf(" ⚠️ Agent wallets should only be used for signing and hold minimal/zero balance")
|
||||
log.Printf(" ⚠️ High balance in Agent wallet poses security risks")
|
||||
log.Printf(" 📖 Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets")
|
||||
log.Printf(" 💡 Recommendation: Transfer funds to main wallet and keep Agent wallet balance near 0")
|
||||
logger.Infof("🚨🚨🚨 CRITICAL SECURITY WARNING 🚨🚨🚨")
|
||||
logger.Infof(" Agent wallet balance: %.2f USDC (exceeds safe threshold of 100 USDC)", agentBalance)
|
||||
logger.Infof(" Agent wallet address: %s", agentAddr)
|
||||
logger.Infof(" ⚠️ Agent wallets should only be used for signing and hold minimal/zero balance")
|
||||
logger.Infof(" ⚠️ High balance in Agent wallet poses security risks")
|
||||
logger.Infof(" 📖 Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets")
|
||||
logger.Infof(" 💡 Recommendation: Transfer funds to main wallet and keep Agent wallet balance near 0")
|
||||
return nil, fmt.Errorf("security check failed: Agent wallet balance too high (%.2f USDC), exceeds 100 USDC threshold", agentBalance)
|
||||
} else if agentBalance > 10 {
|
||||
// Warning: Agent wallet has some balance (acceptable but not ideal)
|
||||
log.Printf("⚠️ Notice: Agent wallet address (%s) has some balance: %.2f USDC", agentAddr, agentBalance)
|
||||
log.Printf(" While not critical, it's recommended to keep Agent wallet balance near 0 for security")
|
||||
logger.Infof("⚠️ Notice: Agent wallet address (%s) has some balance: %.2f USDC", agentAddr, agentBalance)
|
||||
logger.Infof(" While not critical, it's recommended to keep Agent wallet balance near 0 for security")
|
||||
} else {
|
||||
// OK: Agent wallet balance is safe
|
||||
log.Printf("✓ Agent wallet balance is safe: %.2f USDC (near zero as recommended)", agentBalance)
|
||||
logger.Infof("✓ Agent wallet balance is safe: %.2f USDC (near zero as recommended)", agentBalance)
|
||||
}
|
||||
} else if err != nil {
|
||||
// Failed to query agent balance - log warning but don't block initialization
|
||||
log.Printf("⚠️ Could not verify Agent wallet balance (query failed): %v", err)
|
||||
log.Printf(" Proceeding with initialization, but please manually verify Agent wallet balance is near 0")
|
||||
logger.Infof("⚠️ Could not verify Agent wallet balance (query failed): %v", err)
|
||||
logger.Infof(" Proceeding with initialization, but please manually verify Agent wallet balance is near 0")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,18 +131,18 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
||||
|
||||
// GetBalance 获取账户余额
|
||||
func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
log.Printf("🔄 正在调用Hyperliquid API获取账户余额...")
|
||||
logger.Infof("🔄 正在调用Hyperliquid API获取账户余额...")
|
||||
|
||||
// ✅ Step 1: 查询 Spot 现货账户余额
|
||||
spotState, err := t.exchange.Info().SpotUserState(t.ctx, t.walletAddr)
|
||||
var spotUSDCBalance float64 = 0.0
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 查询 Spot 余额失败(可能无现货资产): %v", err)
|
||||
logger.Infof("⚠️ 查询 Spot 余额失败(可能无现货资产): %v", err)
|
||||
} else if spotState != nil && len(spotState.Balances) > 0 {
|
||||
for _, balance := range spotState.Balances {
|
||||
if balance.Coin == "USDC" {
|
||||
spotUSDCBalance, _ = strconv.ParseFloat(balance.Total, 64)
|
||||
log.Printf("✓ 发现 Spot 现货余额: %.2f USDC", spotUSDCBalance)
|
||||
logger.Infof("✓ 发现 Spot 现货余额: %.2f USDC", spotUSDCBalance)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
// ✅ Step 2: 查询 Perpetuals 合约账户状态
|
||||
accountState, err := t.exchange.Info().UserState(t.ctx, t.walletAddr)
|
||||
if err != nil {
|
||||
log.Printf("❌ Hyperliquid Perpetuals API调用失败: %v", err)
|
||||
logger.Infof("❌ Hyperliquid Perpetuals API调用失败: %v", err)
|
||||
return nil, fmt.Errorf("获取账户信息失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -179,8 +179,8 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
|
||||
// 🔍 调试:打印API返回的完整摘要结构
|
||||
summaryJSON, _ := json.MarshalIndent(summary, " ", " ")
|
||||
log.Printf("🔍 [DEBUG] Hyperliquid API %s 完整数据:", summaryType)
|
||||
log.Printf("%s", string(summaryJSON))
|
||||
logger.Infof("🔍 [DEBUG] Hyperliquid API %s 完整数据:", summaryType)
|
||||
logger.Infof("%s", string(summaryJSON))
|
||||
|
||||
// ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏
|
||||
totalUnrealizedPnl := 0.0
|
||||
@@ -204,7 +204,7 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
withdrawable, err := strconv.ParseFloat(accountState.Withdrawable, 64)
|
||||
if err == nil && withdrawable > 0 {
|
||||
availableBalance = withdrawable
|
||||
log.Printf("✓ 使用 Withdrawable 作为可用余额: %.2f", availableBalance)
|
||||
logger.Infof("✓ 使用 Withdrawable 作为可用余额: %.2f", availableBalance)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
if availableBalance == 0 && accountState.Withdrawable == "" {
|
||||
availableBalance = accountValue - totalMarginUsed
|
||||
if availableBalance < 0 {
|
||||
log.Printf("⚠️ 计算出的可用余额为负数 (%.2f),重置为 0", availableBalance)
|
||||
logger.Infof("⚠️ 计算出的可用余额为负数 (%.2f),重置为 0", availableBalance)
|
||||
availableBalance = 0
|
||||
}
|
||||
}
|
||||
@@ -227,16 +227,16 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏(仅来自 Perpetuals)
|
||||
result["spotBalance"] = spotUSDCBalance // Spot 现货余额(单独返回)
|
||||
|
||||
log.Printf("✓ Hyperliquid 完整账户:")
|
||||
log.Printf(" • Spot 现货余额: %.2f USDC (需手动转账到 Perpetuals 才能开仓)", spotUSDCBalance)
|
||||
log.Printf(" • Perpetuals 合约净值: %.2f USDC (钱包%.2f + 未实现%.2f)",
|
||||
logger.Infof("✓ Hyperliquid 完整账户:")
|
||||
logger.Infof(" • Spot 现货余额: %.2f USDC (需手动转账到 Perpetuals 才能开仓)", spotUSDCBalance)
|
||||
logger.Infof(" • Perpetuals 合约净值: %.2f USDC (钱包%.2f + 未实现%.2f)",
|
||||
accountValue,
|
||||
walletBalanceWithoutUnrealized,
|
||||
totalUnrealizedPnl)
|
||||
log.Printf(" • Perpetuals 可用余额: %.2f USDC (可直接用于开仓)", availableBalance)
|
||||
log.Printf(" • 保证金占用: %.2f USDC", totalMarginUsed)
|
||||
log.Printf(" • 总资产 (Perp+Spot): %.2f USDC", totalWalletBalance)
|
||||
log.Printf(" ⭐ 总资产: %.2f USDC | Perp 可用: %.2f USDC | Spot 余额: %.2f USDC",
|
||||
logger.Infof(" • Perpetuals 可用余额: %.2f USDC (可直接用于开仓)", availableBalance)
|
||||
logger.Infof(" • 保证金占用: %.2f USDC", totalMarginUsed)
|
||||
logger.Infof(" • 总资产 (Perp+Spot): %.2f USDC", totalWalletBalance)
|
||||
logger.Infof(" ⭐ 总资产: %.2f USDC | Perp 可用: %.2f USDC | Spot 余额: %.2f USDC",
|
||||
totalWalletBalance, availableBalance, spotUSDCBalance)
|
||||
|
||||
return result, nil
|
||||
@@ -316,7 +316,7 @@ func (t *HyperliquidTrader) SetMarginMode(symbol string, isCrossMargin bool) err
|
||||
if !isCrossMargin {
|
||||
marginModeStr = "逐仓"
|
||||
}
|
||||
log.Printf(" ✓ %s 将使用 %s 模式", symbol, marginModeStr)
|
||||
logger.Infof(" ✓ %s 将使用 %s 模式", symbol, marginModeStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ func (t *HyperliquidTrader) SetLeverage(symbol string, leverage int) error {
|
||||
return fmt.Errorf("设置杠杆失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" ✓ %s 杠杆已切换为 %dx", symbol, leverage)
|
||||
logger.Infof(" ✓ %s 杠杆已切换为 %dx", symbol, leverage)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ func (t *HyperliquidTrader) refreshMetaIfNeeded(coin string) error {
|
||||
return nil // Meta 正常,无需刷新
|
||||
}
|
||||
|
||||
log.Printf("⚠️ %s 的 Asset ID 为 0,尝试刷新 Meta 信息...", coin)
|
||||
logger.Infof("⚠️ %s 的 Asset ID 为 0,尝试刷新 Meta 信息...", coin)
|
||||
|
||||
// 刷新 Meta 信息
|
||||
meta, err := t.exchange.Info().Meta(t.ctx)
|
||||
@@ -356,7 +356,7 @@ func (t *HyperliquidTrader) refreshMetaIfNeeded(coin string) error {
|
||||
t.meta = meta
|
||||
t.metaMutex.Unlock()
|
||||
|
||||
log.Printf("✅ Meta 信息已刷新,包含 %d 个资产", len(meta.Universe))
|
||||
logger.Infof("✅ Meta 信息已刷新,包含 %d 个资产", len(meta.Universe))
|
||||
|
||||
// 验证刷新后的 Asset ID
|
||||
assetID = t.exchange.Info().NameToAsset(coin)
|
||||
@@ -367,7 +367,7 @@ func (t *HyperliquidTrader) refreshMetaIfNeeded(coin string) error {
|
||||
" 3. API 连接问题", coin)
|
||||
}
|
||||
|
||||
log.Printf("✅ 刷新后 Asset ID 检查通过: %s -> %d", coin, assetID)
|
||||
logger.Infof("✅ 刷新后 Asset ID 检查通过: %s -> %d", coin, assetID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ func (t *HyperliquidTrader) refreshMetaIfNeeded(coin string) error {
|
||||
func (t *HyperliquidTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 先取消该币种的所有委托单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧委托单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消旧委托单失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置杠杆
|
||||
@@ -394,11 +394,11 @@ func (t *HyperliquidTrader) OpenLong(symbol string, quantity float64, leverage i
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
log.Printf(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
logger.Infof(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
aggressivePrice := t.roundPriceToSigfigs(price * 1.01)
|
||||
log.Printf(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*1.01, aggressivePrice)
|
||||
logger.Infof(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*1.01, aggressivePrice)
|
||||
|
||||
// 创建市价买入订单(使用IOC limit order with aggressive price)
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
@@ -419,7 +419,7 @@ func (t *HyperliquidTrader) OpenLong(symbol string, quantity float64, leverage i
|
||||
return nil, fmt.Errorf("开多仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 开多仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
logger.Infof("✓ 开多仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
|
||||
result := make(map[string]interface{})
|
||||
result["orderId"] = 0 // Hyperliquid没有返回order ID
|
||||
@@ -433,7 +433,7 @@ func (t *HyperliquidTrader) OpenLong(symbol string, quantity float64, leverage i
|
||||
func (t *HyperliquidTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// 先取消该币种的所有委托单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消旧委托单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消旧委托单失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置杠杆
|
||||
@@ -452,11 +452,11 @@ func (t *HyperliquidTrader) OpenShort(symbol string, quantity float64, leverage
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
log.Printf(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
logger.Infof(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
aggressivePrice := t.roundPriceToSigfigs(price * 0.99)
|
||||
log.Printf(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*0.99, aggressivePrice)
|
||||
logger.Infof(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*0.99, aggressivePrice)
|
||||
|
||||
// 创建市价卖出订单
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
@@ -477,7 +477,7 @@ func (t *HyperliquidTrader) OpenShort(symbol string, quantity float64, leverage
|
||||
return nil, fmt.Errorf("开空仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 开空仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
logger.Infof("✓ 开空仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
|
||||
result := make(map[string]interface{})
|
||||
result["orderId"] = 0
|
||||
@@ -519,11 +519,11 @@ func (t *HyperliquidTrader) CloseLong(symbol string, quantity float64) (map[stri
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
log.Printf(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
logger.Infof(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
aggressivePrice := t.roundPriceToSigfigs(price * 0.99)
|
||||
log.Printf(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*0.99, aggressivePrice)
|
||||
logger.Infof(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*0.99, aggressivePrice)
|
||||
|
||||
// 创建平仓订单(卖出 + ReduceOnly)
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
@@ -544,11 +544,11 @@ func (t *HyperliquidTrader) CloseLong(symbol string, quantity float64) (map[stri
|
||||
return nil, fmt.Errorf("平多仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 平多仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
logger.Infof("✓ 平多仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
|
||||
// 平仓后取消该币种的所有挂单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
@@ -591,11 +591,11 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str
|
||||
|
||||
// ⚠️ 关键:根据币种精度要求,四舍五入数量
|
||||
roundedQuantity := t.roundToSzDecimals(coin, quantity)
|
||||
log.Printf(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
logger.Infof(" 📏 数量精度处理: %.8f -> %.8f (szDecimals=%d)", quantity, roundedQuantity, t.getSzDecimals(coin))
|
||||
|
||||
// ⚠️ 关键:价格也需要处理为5位有效数字
|
||||
aggressivePrice := t.roundPriceToSigfigs(price * 1.01)
|
||||
log.Printf(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*1.01, aggressivePrice)
|
||||
logger.Infof(" 💰 价格精度处理: %.8f -> %.8f (5位有效数字)", price*1.01, aggressivePrice)
|
||||
|
||||
// 创建平仓订单(买入 + ReduceOnly)
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
@@ -616,11 +616,11 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str
|
||||
return nil, fmt.Errorf("平空仓失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 平空仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
logger.Infof("✓ 平空仓成功: %s 数量: %.4f", symbol, roundedQuantity)
|
||||
|
||||
// 平仓后取消该币种的所有挂单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
@@ -637,7 +637,7 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str
|
||||
func (t *HyperliquidTrader) CancelStopLossOrders(symbol string) error {
|
||||
// Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段
|
||||
// 无法区分止损和止盈单,因此取消该币种的所有挂单
|
||||
log.Printf(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单")
|
||||
logger.Infof(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单")
|
||||
return t.CancelStopOrders(symbol)
|
||||
}
|
||||
|
||||
@@ -645,7 +645,7 @@ func (t *HyperliquidTrader) CancelStopLossOrders(symbol string) error {
|
||||
func (t *HyperliquidTrader) CancelTakeProfitOrders(symbol string) error {
|
||||
// Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段
|
||||
// 无法区分止损和止盈单,因此取消该币种的所有挂单
|
||||
log.Printf(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单")
|
||||
logger.Infof(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单")
|
||||
return t.CancelStopOrders(symbol)
|
||||
}
|
||||
|
||||
@@ -664,12 +664,12 @@ func (t *HyperliquidTrader) CancelAllOrders(symbol string) error {
|
||||
if order.Coin == coin {
|
||||
_, err := t.exchange.Cancel(t.ctx, coin, order.Oid)
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err)
|
||||
logger.Infof(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf(" ✓ 已取消 %s 的所有挂单", symbol)
|
||||
logger.Infof(" ✓ 已取消 %s 的所有挂单", symbol)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -691,7 +691,7 @@ func (t *HyperliquidTrader) CancelStopOrders(symbol string) error {
|
||||
if order.Coin == coin {
|
||||
_, err := t.exchange.Cancel(t.ctx, coin, order.Oid)
|
||||
if err != nil {
|
||||
log.Printf(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err)
|
||||
logger.Infof(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err)
|
||||
continue
|
||||
}
|
||||
canceledCount++
|
||||
@@ -699,9 +699,9 @@ func (t *HyperliquidTrader) CancelStopOrders(symbol string) error {
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
log.Printf(" ℹ %s 没有挂单需要取消", symbol)
|
||||
logger.Infof(" ℹ %s 没有挂单需要取消", symbol)
|
||||
} else {
|
||||
log.Printf(" ✓ 已取消 %s 的 %d 个挂单(包括止盈/止损单)", symbol, canceledCount)
|
||||
logger.Infof(" ✓ 已取消 %s 的 %d 个挂单(包括止盈/止损单)", symbol, canceledCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -762,7 +762,7 @@ func (t *HyperliquidTrader) SetStopLoss(symbol string, positionSide string, quan
|
||||
return fmt.Errorf("设置止损失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" 止损价设置: %.4f", roundedStopPrice)
|
||||
logger.Infof(" 止损价设置: %.4f", roundedStopPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -799,7 +799,7 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu
|
||||
return fmt.Errorf("设置止盈失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" 止盈价设置: %.4f", roundedTakeProfitPrice)
|
||||
logger.Infof(" 止盈价设置: %.4f", roundedTakeProfitPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -820,7 +820,7 @@ func (t *HyperliquidTrader) getSzDecimals(coin string) int {
|
||||
defer t.metaMutex.RUnlock()
|
||||
|
||||
if t.meta == nil {
|
||||
log.Printf("⚠️ meta信息为空,使用默认精度4")
|
||||
logger.Infof("⚠️ meta信息为空,使用默认精度4")
|
||||
return 4 // 默认精度
|
||||
}
|
||||
|
||||
@@ -831,7 +831,7 @@ func (t *HyperliquidTrader) getSzDecimals(coin string) int {
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("⚠️ 未找到 %s 的精度信息,使用默认精度4", coin)
|
||||
logger.Infof("⚠️ 未找到 %s 的精度信息,使用默认精度4", coin)
|
||||
return 4 // 默认精度
|
||||
}
|
||||
|
||||
@@ -897,6 +897,53 @@ func convertSymbolToHyperliquid(symbol string) string {
|
||||
return symbol
|
||||
}
|
||||
|
||||
// GetOrderStatus 获取订单状态
|
||||
// Hyperliquid 使用 IOC 订单,通常立即成交或取消
|
||||
// 对于已完成的订单,需要查询历史记录
|
||||
func (t *HyperliquidTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) {
|
||||
// Hyperliquid 的 IOC 订单几乎立即完成
|
||||
// 如果订单是通过本系统下单的,返回的 status 都是 FILLED
|
||||
// 这里尝试查询开放订单来判断是否还在等待
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
|
||||
// 首先检查是否在开放订单中
|
||||
openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr)
|
||||
if err != nil {
|
||||
// 如果查询失败,假设订单已完成
|
||||
return map[string]interface{}{
|
||||
"orderId": orderID,
|
||||
"status": "FILLED",
|
||||
"avgPrice": 0.0,
|
||||
"executedQty": 0.0,
|
||||
"commission": 0.0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查订单是否在开放订单列表中
|
||||
for _, order := range openOrders {
|
||||
if order.Coin == coin && fmt.Sprintf("%d", order.Oid) == orderID {
|
||||
// 订单仍在等待
|
||||
return map[string]interface{}{
|
||||
"orderId": orderID,
|
||||
"status": "NEW",
|
||||
"avgPrice": 0.0,
|
||||
"executedQty": 0.0,
|
||||
"commission": 0.0,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 订单不在开放列表中,说明已完成或已取消
|
||||
// Hyperliquid IOC 订单如果不在开放列表中,通常是已成交
|
||||
return map[string]interface{}{
|
||||
"orderId": orderID,
|
||||
"status": "FILLED",
|
||||
"avgPrice": 0.0, // Hyperliquid 不直接返回成交价格,需要从持仓信息获取
|
||||
"executedQty": 0.0,
|
||||
"commission": 0.0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// absFloat 返回浮点数的绝对值
|
||||
func absFloat(x float64) float64 {
|
||||
if x < 0 {
|
||||
|
||||
@@ -50,4 +50,8 @@ type Trader interface {
|
||||
|
||||
// FormatQuantity 格式化数量到正确的精度
|
||||
FormatQuantity(symbol string, quantity float64) (string, error)
|
||||
|
||||
// GetOrderStatus 获取订单状态
|
||||
// 返回: status(FILLED/NEW/CANCELED), avgPrice, executedQty, commission
|
||||
GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ func (t *LighterTrader) CreateOrder(symbol, side string, quantity, price float64
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER订单已创建 - ID: %s, Symbol: %s, Side: %s, Qty: %.4f",
|
||||
logger.Infof("✓ LIGHTER订单已创建 - ID: %s, Symbol: %s, Side: %s, Qty: %.4f",
|
||||
orderResp.OrderID, symbol, side, quantity)
|
||||
|
||||
return orderResp.OrderID, nil
|
||||
@@ -143,7 +143,7 @@ func (t *LighterTrader) CancelOrder(symbol, orderID string) error {
|
||||
return fmt.Errorf("取消订单失败 (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER订单已取消 - ID: %s", orderID)
|
||||
logger.Infof("✓ LIGHTER订单已取消 - ID: %s", orderID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -160,18 +160,18 @@ func (t *LighterTrader) CancelAllOrders(symbol string) error {
|
||||
}
|
||||
|
||||
if len(orders) == 0 {
|
||||
log.Printf("✓ LIGHTER - 无需取消订单(无活跃订单)")
|
||||
logger.Infof("✓ LIGHTER - 无需取消订单(无活跃订单)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 批量取消
|
||||
for _, order := range orders {
|
||||
if err := t.CancelOrder(symbol, order.OrderID); err != nil {
|
||||
log.Printf("⚠️ 取消订单失败 (ID: %s): %v", order.OrderID, err)
|
||||
logger.Infof("⚠️ 取消订单失败 (ID: %s): %v", order.OrderID, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER - 已取消 %d 个订单", len(orders))
|
||||
logger.Infof("✓ LIGHTER - 已取消 %d 个订单", len(orders))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -223,8 +223,8 @@ func (t *LighterTrader) GetActiveOrders(symbol string) ([]OrderResponse, error)
|
||||
return orders, nil
|
||||
}
|
||||
|
||||
// GetOrderStatus 获取订单状态
|
||||
func (t *LighterTrader) GetOrderStatus(orderID string) (*OrderResponse, error) {
|
||||
// GetOrderStatus 获取订单状态(实现 Trader 接口)
|
||||
func (t *LighterTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) {
|
||||
if err := t.ensureAuthToken(); err != nil {
|
||||
return nil, fmt.Errorf("认证令牌无效: %w", err)
|
||||
}
|
||||
@@ -261,20 +261,37 @@ func (t *LighterTrader) GetOrderStatus(orderID string) (*OrderResponse, error) {
|
||||
return nil, fmt.Errorf("解析订单响应失败: %w", err)
|
||||
}
|
||||
|
||||
return &order, nil
|
||||
// 转换状态为统一格式
|
||||
unifiedStatus := order.Status
|
||||
switch order.Status {
|
||||
case "filled":
|
||||
unifiedStatus = "FILLED"
|
||||
case "open":
|
||||
unifiedStatus = "NEW"
|
||||
case "cancelled":
|
||||
unifiedStatus = "CANCELED"
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"orderId": order.OrderID,
|
||||
"status": unifiedStatus,
|
||||
"avgPrice": order.Price,
|
||||
"executedQty": order.FilledQty,
|
||||
"commission": 0.0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CancelStopLossOrders 仅取消止损单(LIGHTER 暂无法区分,取消所有止盈止损单)
|
||||
func (t *LighterTrader) CancelStopLossOrders(symbol string) error {
|
||||
// LIGHTER 暂时无法区分止损和止盈单,取消所有止盈止损单
|
||||
log.Printf(" ⚠️ LIGHTER 无法区分止损/止盈单,将取消所有止盈止损单")
|
||||
logger.Infof(" ⚠️ LIGHTER 无法区分止损/止盈单,将取消所有止盈止损单")
|
||||
return t.CancelStopOrders(symbol)
|
||||
}
|
||||
|
||||
// CancelTakeProfitOrders 仅取消止盈单(LIGHTER 暂无法区分,取消所有止盈止损单)
|
||||
func (t *LighterTrader) CancelTakeProfitOrders(symbol string) error {
|
||||
// LIGHTER 暂时无法区分止损和止盈单,取消所有止盈止损单
|
||||
log.Printf(" ⚠️ LIGHTER 无法区分止损/止盈单,将取消所有止盈止损单")
|
||||
logger.Infof(" ⚠️ LIGHTER 无法区分止损/止盈单,将取消所有止盈止损单")
|
||||
return t.CancelStopOrders(symbol)
|
||||
}
|
||||
|
||||
@@ -295,12 +312,12 @@ func (t *LighterTrader) CancelStopOrders(symbol string) error {
|
||||
// TODO: 需要检查订单类型,只取消止盈止损单
|
||||
// 暂时取消所有订单
|
||||
if err := t.CancelOrder(symbol, order.OrderID); err != nil {
|
||||
log.Printf("⚠️ 取消订单失败 (ID: %s): %v", order.OrderID, err)
|
||||
logger.Infof("⚠️ 取消订单失败 (ID: %s): %v", order.OrderID, err)
|
||||
} else {
|
||||
canceledCount++
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER - 已取消 %d 个止盈止损单", canceledCount)
|
||||
logger.Infof("✓ LIGHTER - 已取消 %d 个止盈止损单", canceledCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -59,7 +59,7 @@ func NewLighterTrader(privateKeyHex string, walletAddr string, testnet bool) (*L
|
||||
// 从私钥派生钱包地址(如果未提供)
|
||||
if walletAddr == "" {
|
||||
walletAddr = crypto.PubkeyToAddress(*privateKey.Public().(*ecdsa.PublicKey)).Hex()
|
||||
log.Printf("✓ 从私钥派生钱包地址: %s", walletAddr)
|
||||
logger.Infof("✓ 从私钥派生钱包地址: %s", walletAddr)
|
||||
}
|
||||
|
||||
// 选择API URL
|
||||
@@ -78,7 +78,7 @@ func NewLighterTrader(privateKeyHex string, walletAddr string, testnet bool) (*L
|
||||
symbolPrecision: make(map[string]SymbolPrecision),
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER交易器初始化成功 (testnet=%v, wallet=%s)", testnet, walletAddr)
|
||||
logger.Infof("✓ LIGHTER交易器初始化成功 (testnet=%v, wallet=%s)", testnet, walletAddr)
|
||||
|
||||
// 初始化账户信息(获取账户索引和API密钥)
|
||||
if err := trader.initializeAccount(); err != nil {
|
||||
@@ -100,7 +100,7 @@ func (t *LighterTrader) initializeAccount() error {
|
||||
t.accountIndex = accountInfo["index"].(int)
|
||||
t.accountMutex.Unlock()
|
||||
|
||||
log.Printf("✓ LIGHTER账户索引: %d", t.accountIndex)
|
||||
logger.Infof("✓ LIGHTER账户索引: %d", t.accountIndex)
|
||||
|
||||
// 2. 生成认证令牌(有效期8小时)
|
||||
if err := t.refreshAuthToken(); err != nil {
|
||||
@@ -153,7 +153,7 @@ func (t *LighterTrader) refreshAuthToken() error {
|
||||
|
||||
// 临时实现:设置过期时间为8小时后
|
||||
t.tokenExpiry = time.Now().Add(8 * time.Hour)
|
||||
log.Printf("✓ 认证令牌已生成(有效期至: %s)", t.tokenExpiry.Format(time.RFC3339))
|
||||
logger.Infof("✓ 认证令牌已生成(有效期至: %s)", t.tokenExpiry.Format(time.RFC3339))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func (t *LighterTrader) ensureAuthToken() error {
|
||||
t.accountMutex.RUnlock()
|
||||
|
||||
if expired {
|
||||
log.Println("🔄 认证令牌即将过期,刷新中...")
|
||||
logger.Info("🔄 认证令牌即将过期,刷新中...")
|
||||
return t.refreshAuthToken()
|
||||
}
|
||||
|
||||
@@ -204,12 +204,12 @@ func (t *LighterTrader) GetExchangeType() string {
|
||||
|
||||
// Close 关闭交易器
|
||||
func (t *LighterTrader) Close() error {
|
||||
log.Println("✓ LIGHTER交易器已关闭")
|
||||
logger.Info("✓ LIGHTER交易器已关闭")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run 运行交易器(实现Trader接口)
|
||||
func (t *LighterTrader) Run() error {
|
||||
log.Println("⚠️ LIGHTER交易器的Run方法应由AutoTrader调用")
|
||||
logger.Info("⚠️ LIGHTER交易器的Run方法应由AutoTrader调用")
|
||||
return fmt.Errorf("请使用AutoTrader管理交易器生命周期")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -76,7 +76,7 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string,
|
||||
// 2. 如果沒有提供錢包地址,從私鑰派生
|
||||
if walletAddr == "" {
|
||||
walletAddr = crypto.PubkeyToAddress(*l1PrivateKey.Public().(*ecdsa.PublicKey)).Hex()
|
||||
log.Printf("✓ 從私鑰派生錢包地址: %s", walletAddr)
|
||||
logger.Infof("✓ 從私鑰派生錢包地址: %s", walletAddr)
|
||||
}
|
||||
|
||||
// 3. 確定 API URL 和 Chain ID
|
||||
@@ -112,8 +112,8 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string,
|
||||
|
||||
// 6. 如果沒有 API Key,提示用戶需要生成
|
||||
if apiKeyPrivateKeyHex == "" {
|
||||
log.Printf("⚠️ 未提供 API Key 私鑰,請調用 GenerateAndRegisterAPIKey() 生成")
|
||||
log.Printf(" 或者從 LIGHTER 官網獲取現有的 API Key")
|
||||
logger.Infof("⚠️ 未提供 API Key 私鑰,請調用 GenerateAndRegisterAPIKey() 生成")
|
||||
logger.Infof(" 或者從 LIGHTER 官網獲取現有的 API Key")
|
||||
return trader, nil
|
||||
}
|
||||
|
||||
@@ -133,12 +133,12 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string,
|
||||
|
||||
// 8. 驗證 API Key 是否正確
|
||||
if err := trader.checkClient(); err != nil {
|
||||
log.Printf("⚠️ API Key 驗證失敗: %v", err)
|
||||
log.Printf(" 您可能需要重新生成 API Key 或檢查配置")
|
||||
logger.Infof("⚠️ API Key 驗證失敗: %v", err)
|
||||
logger.Infof(" 您可能需要重新生成 API Key 或檢查配置")
|
||||
return trader, err
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER 交易器初始化成功 (account=%d, apiKey=%d, testnet=%v)",
|
||||
logger.Infof("✓ LIGHTER 交易器初始化成功 (account=%d, apiKey=%d, testnet=%v)",
|
||||
trader.accountIndex, trader.apiKeyIndex, testnet)
|
||||
|
||||
return trader, nil
|
||||
@@ -156,7 +156,7 @@ func (t *LighterTraderV2) initializeAccount() error {
|
||||
t.accountIndex = accountInfo.AccountIndex
|
||||
t.accountMutex.Unlock()
|
||||
|
||||
log.Printf("✓ 賬戶索引: %d", t.accountIndex)
|
||||
logger.Infof("✓ 賬戶索引: %d", t.accountIndex)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ func (t *LighterTraderV2) checkClient() error {
|
||||
return fmt.Errorf("API Key 不匹配:本地=%s, 服務器=%s", localPubKey, publicKey)
|
||||
}
|
||||
|
||||
log.Printf("✓ API Key 驗證通過")
|
||||
logger.Infof("✓ API Key 驗證通過")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ func (t *LighterTraderV2) refreshAuthToken() error {
|
||||
t.tokenExpiry = deadline
|
||||
t.accountMutex.Unlock()
|
||||
|
||||
log.Printf("✓ 認證令牌已生成(有效期至: %s)", t.tokenExpiry.Format(time.RFC3339))
|
||||
logger.Infof("✓ 認證令牌已生成(有效期至: %s)", t.tokenExpiry.Format(time.RFC3339))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ func (t *LighterTraderV2) ensureAuthToken() error {
|
||||
t.accountMutex.RUnlock()
|
||||
|
||||
if expired {
|
||||
log.Println("🔄 認證令牌即將過期,刷新中...")
|
||||
logger.Info("🔄 認證令牌即將過期,刷新中...")
|
||||
return t.refreshAuthToken()
|
||||
}
|
||||
|
||||
@@ -274,6 +274,6 @@ func (t *LighterTraderV2) GetExchangeType() string {
|
||||
|
||||
// Cleanup 清理資源
|
||||
func (t *LighterTraderV2) Cleanup() error {
|
||||
log.Println("⏹ LIGHTER 交易器清理完成")
|
||||
logger.Info("⏹ LIGHTER 交易器清理完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -18,7 +18,7 @@ func (t *LighterTraderV2) SetStopLoss(symbol string, positionSide string, quanti
|
||||
return fmt.Errorf("TxClient 未初始化")
|
||||
}
|
||||
|
||||
log.Printf("🛑 LIGHTER 設置止損: %s %s qty=%.4f, stop=%.2f", symbol, positionSide, quantity, stopPrice)
|
||||
logger.Infof("🛑 LIGHTER 設置止損: %s %s qty=%.4f, stop=%.2f", symbol, positionSide, quantity, stopPrice)
|
||||
|
||||
// 確定訂單方向(做空止損用買單,做多止損用賣單)
|
||||
isAsk := (positionSide == "LONG" || positionSide == "long")
|
||||
@@ -29,7 +29,7 @@ func (t *LighterTraderV2) SetStopLoss(symbol string, positionSide string, quanti
|
||||
return fmt.Errorf("設置止損失敗: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER 止損已設置: %.2f", stopPrice)
|
||||
logger.Infof("✓ LIGHTER 止損已設置: %.2f", stopPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (t *LighterTraderV2) SetTakeProfit(symbol string, positionSide string, quan
|
||||
return fmt.Errorf("TxClient 未初始化")
|
||||
}
|
||||
|
||||
log.Printf("🎯 LIGHTER 設置止盈: %s %s qty=%.4f, tp=%.2f", symbol, positionSide, quantity, takeProfitPrice)
|
||||
logger.Infof("🎯 LIGHTER 設置止盈: %s %s qty=%.4f, tp=%.2f", symbol, positionSide, quantity, takeProfitPrice)
|
||||
|
||||
// 確定訂單方向(做空止盈用買單,做多止盈用賣單)
|
||||
isAsk := (positionSide == "LONG" || positionSide == "long")
|
||||
@@ -50,7 +50,7 @@ func (t *LighterTraderV2) SetTakeProfit(symbol string, positionSide string, quan
|
||||
return fmt.Errorf("設置止盈失敗: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER 止盈已設置: %.2f", takeProfitPrice)
|
||||
logger.Infof("✓ LIGHTER 止盈已設置: %.2f", takeProfitPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (t *LighterTraderV2) CancelAllOrders(symbol string) error {
|
||||
}
|
||||
|
||||
if len(orders) == 0 {
|
||||
log.Printf("✓ LIGHTER - 無需取消訂單(無活躍訂單)")
|
||||
logger.Infof("✓ LIGHTER - 無需取消訂單(無活躍訂單)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -79,27 +79,101 @@ func (t *LighterTraderV2) CancelAllOrders(symbol string) error {
|
||||
canceledCount := 0
|
||||
for _, order := range orders {
|
||||
if err := t.CancelOrder(symbol, order.OrderID); err != nil {
|
||||
log.Printf("⚠️ 取消訂單失敗 (ID: %s): %v", order.OrderID, err)
|
||||
logger.Infof("⚠️ 取消訂單失敗 (ID: %s): %v", order.OrderID, err)
|
||||
} else {
|
||||
canceledCount++
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER - 已取消 %d 個訂單", canceledCount)
|
||||
logger.Infof("✓ LIGHTER - 已取消 %d 個訂單", canceledCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrderStatus 獲取訂單狀態(實現 Trader 接口)
|
||||
func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) {
|
||||
// LIGHTER 使用市價單通常立即成交
|
||||
// 嘗試查詢訂單狀態
|
||||
if err := t.ensureAuthToken(); err != nil {
|
||||
return nil, fmt.Errorf("認證令牌無效: %w", err)
|
||||
}
|
||||
|
||||
// 構建請求 URL
|
||||
endpoint := fmt.Sprintf("%s/api/v1/order/%s", t.baseURL, orderID)
|
||||
|
||||
req, err := http.NewRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", t.authToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
if err != nil {
|
||||
// 如果查詢失敗,假設訂單已完成
|
||||
return map[string]interface{}{
|
||||
"orderId": orderID,
|
||||
"status": "FILLED",
|
||||
"avgPrice": 0.0,
|
||||
"executedQty": 0.0,
|
||||
"commission": 0.0,
|
||||
}, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return map[string]interface{}{
|
||||
"orderId": orderID,
|
||||
"status": "FILLED",
|
||||
"avgPrice": 0.0,
|
||||
"executedQty": 0.0,
|
||||
"commission": 0.0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var order OrderResponse
|
||||
if err := json.Unmarshal(body, &order); err != nil {
|
||||
return map[string]interface{}{
|
||||
"orderId": orderID,
|
||||
"status": "FILLED",
|
||||
"avgPrice": 0.0,
|
||||
"executedQty": 0.0,
|
||||
"commission": 0.0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 轉換狀態為統一格式
|
||||
unifiedStatus := order.Status
|
||||
switch order.Status {
|
||||
case "filled":
|
||||
unifiedStatus = "FILLED"
|
||||
case "open":
|
||||
unifiedStatus = "NEW"
|
||||
case "cancelled":
|
||||
unifiedStatus = "CANCELED"
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"orderId": order.OrderID,
|
||||
"status": unifiedStatus,
|
||||
"avgPrice": order.Price,
|
||||
"executedQty": order.FilledQty,
|
||||
"commission": 0.0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CancelStopLossOrders 僅取消止損單(實現 Trader 接口)
|
||||
func (t *LighterTraderV2) CancelStopLossOrders(symbol string) error {
|
||||
// LIGHTER 暫時無法區分止損和止盈單,取消所有止盈止損單
|
||||
log.Printf("⚠️ LIGHTER 無法區分止損/止盈單,將取消所有止盈止損單")
|
||||
logger.Infof("⚠️ LIGHTER 無法區分止損/止盈單,將取消所有止盈止損單")
|
||||
return t.CancelStopOrders(symbol)
|
||||
}
|
||||
|
||||
// CancelTakeProfitOrders 僅取消止盈單(實現 Trader 接口)
|
||||
func (t *LighterTraderV2) CancelTakeProfitOrders(symbol string) error {
|
||||
// LIGHTER 暫時無法區分止損和止盈單,取消所有止盈止損單
|
||||
log.Printf("⚠️ LIGHTER 無法區分止損/止盈單,將取消所有止盈止損單")
|
||||
logger.Infof("⚠️ LIGHTER 無法區分止損/止盈單,將取消所有止盈止損單")
|
||||
return t.CancelStopOrders(symbol)
|
||||
}
|
||||
|
||||
@@ -124,13 +198,13 @@ func (t *LighterTraderV2) CancelStopOrders(symbol string) error {
|
||||
// TODO: 檢查訂單類型,只取消止盈止損單
|
||||
// 暫時取消所有訂單
|
||||
if err := t.CancelOrder(symbol, order.OrderID); err != nil {
|
||||
log.Printf("⚠️ 取消訂單失敗 (ID: %s): %v", order.OrderID, err)
|
||||
logger.Infof("⚠️ 取消訂單失敗 (ID: %s): %v", order.OrderID, err)
|
||||
} else {
|
||||
canceledCount++
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER - 已取消 %d 個止盈止損單", canceledCount)
|
||||
logger.Infof("✓ LIGHTER - 已取消 %d 個止盈止損單", canceledCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -186,7 +260,7 @@ func (t *LighterTraderV2) GetActiveOrders(symbol string) ([]OrderResponse, error
|
||||
return nil, fmt.Errorf("獲取活躍訂單失敗 (code %d): %s", apiResp.Code, apiResp.Message)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER - 獲取到 %d 個活躍訂單", len(apiResp.Data))
|
||||
logger.Infof("✓ LIGHTER - 獲取到 %d 個活躍訂單", len(apiResp.Data))
|
||||
return apiResp.Data, nil
|
||||
}
|
||||
|
||||
@@ -235,7 +309,7 @@ func (t *LighterTraderV2) CancelOrder(symbol, orderID string) error {
|
||||
return fmt.Errorf("提交取消訂單失敗: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER訂單已取消 - ID: %s", orderID)
|
||||
logger.Infof("✓ LIGHTER訂單已取消 - ID: %s", orderID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -291,6 +365,6 @@ func (t *LighterTraderV2) submitCancelOrder(signedTx []byte) (map[string]interfa
|
||||
"status": "cancelled",
|
||||
}
|
||||
|
||||
log.Printf("✓ 取消訂單已提交到 LIGHTER - tx_hash: %v", sendResp.Data["tx_hash"])
|
||||
logger.Infof("✓ 取消訂單已提交到 LIGHTER - tx_hash: %v", sendResp.Data["tx_hash"])
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -18,11 +18,11 @@ func (t *LighterTraderV2) OpenLong(symbol string, quantity float64, leverage int
|
||||
return nil, fmt.Errorf("TxClient 未初始化,請先設置 API Key")
|
||||
}
|
||||
|
||||
log.Printf("📈 LIGHTER 開多倉: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage)
|
||||
logger.Infof("📈 LIGHTER 開多倉: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage)
|
||||
|
||||
// 1. 設置杠杆(如果需要)
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
log.Printf("⚠️ 設置杠杆失敗: %v", err)
|
||||
logger.Infof("⚠️ 設置杠杆失敗: %v", err)
|
||||
}
|
||||
|
||||
// 2. 獲取市場價格
|
||||
@@ -37,7 +37,7 @@ func (t *LighterTraderV2) OpenLong(symbol string, quantity float64, leverage int
|
||||
return nil, fmt.Errorf("開多倉失敗: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER 開多倉成功: %s @ %.2f", symbol, marketPrice)
|
||||
logger.Infof("✓ LIGHTER 開多倉成功: %s @ %.2f", symbol, marketPrice)
|
||||
|
||||
return map[string]interface{}{
|
||||
"orderId": orderResult["orderId"],
|
||||
@@ -54,11 +54,11 @@ func (t *LighterTraderV2) OpenShort(symbol string, quantity float64, leverage in
|
||||
return nil, fmt.Errorf("TxClient 未初始化,請先設置 API Key")
|
||||
}
|
||||
|
||||
log.Printf("📉 LIGHTER 開空倉: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage)
|
||||
logger.Infof("📉 LIGHTER 開空倉: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage)
|
||||
|
||||
// 1. 設置杠杆
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
log.Printf("⚠️ 設置杠杆失敗: %v", err)
|
||||
logger.Infof("⚠️ 設置杠杆失敗: %v", err)
|
||||
}
|
||||
|
||||
// 2. 獲取市場價格
|
||||
@@ -73,7 +73,7 @@ func (t *LighterTraderV2) OpenShort(symbol string, quantity float64, leverage in
|
||||
return nil, fmt.Errorf("開空倉失敗: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER 開空倉成功: %s @ %.2f", symbol, marketPrice)
|
||||
logger.Infof("✓ LIGHTER 開空倉成功: %s @ %.2f", symbol, marketPrice)
|
||||
|
||||
return map[string]interface{}{
|
||||
"orderId": orderResult["orderId"],
|
||||
@@ -105,7 +105,7 @@ func (t *LighterTraderV2) CloseLong(symbol string, quantity float64) (map[string
|
||||
quantity = pos.Size
|
||||
}
|
||||
|
||||
log.Printf("🔻 LIGHTER 平多倉: %s, qty=%.4f", symbol, quantity)
|
||||
logger.Infof("🔻 LIGHTER 平多倉: %s, qty=%.4f", symbol, quantity)
|
||||
|
||||
// 創建市價賣出單平倉(reduceOnly=true)
|
||||
orderResult, err := t.CreateOrder(symbol, true, quantity, 0, "market")
|
||||
@@ -115,10 +115,10 @@ func (t *LighterTraderV2) CloseLong(symbol string, quantity float64) (map[string
|
||||
|
||||
// 平倉後取消所有掛單
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf("⚠️ 取消掛單失敗: %v", err)
|
||||
logger.Infof("⚠️ 取消掛單失敗: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER 平多倉成功: %s", symbol)
|
||||
logger.Infof("✓ LIGHTER 平多倉成功: %s", symbol)
|
||||
|
||||
return map[string]interface{}{
|
||||
"orderId": orderResult["orderId"],
|
||||
@@ -148,7 +148,7 @@ func (t *LighterTraderV2) CloseShort(symbol string, quantity float64) (map[strin
|
||||
quantity = pos.Size
|
||||
}
|
||||
|
||||
log.Printf("🔺 LIGHTER 平空倉: %s, qty=%.4f", symbol, quantity)
|
||||
logger.Infof("🔺 LIGHTER 平空倉: %s, qty=%.4f", symbol, quantity)
|
||||
|
||||
// 創建市價買入單平倉(reduceOnly=true)
|
||||
orderResult, err := t.CreateOrder(symbol, false, quantity, 0, "market")
|
||||
@@ -158,10 +158,10 @@ func (t *LighterTraderV2) CloseShort(symbol string, quantity float64) (map[strin
|
||||
|
||||
// 平倉後取消所有掛單
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf("⚠️ 取消掛單失敗: %v", err)
|
||||
logger.Infof("⚠️ 取消掛單失敗: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER 平空倉成功: %s", symbol)
|
||||
logger.Infof("✓ LIGHTER 平空倉成功: %s", symbol)
|
||||
|
||||
return map[string]interface{}{
|
||||
"orderId": orderResult["orderId"],
|
||||
@@ -235,7 +235,7 @@ func (t *LighterTraderV2) CreateOrder(symbol string, isAsk bool, quantity float6
|
||||
if isAsk {
|
||||
side = "sell"
|
||||
}
|
||||
log.Printf("✓ LIGHTER訂單已創建: %s %s qty=%.4f", symbol, side, quantity)
|
||||
logger.Infof("✓ LIGHTER訂單已創建: %s %s qty=%.4f", symbol, side, quantity)
|
||||
|
||||
return orderResp, nil
|
||||
}
|
||||
@@ -315,7 +315,7 @@ func (t *LighterTraderV2) submitOrder(signedTx []byte) (map[string]interface{},
|
||||
result["orderId"] = txHash
|
||||
}
|
||||
|
||||
log.Printf("✓ 訂單已提交到 LIGHTER - tx_hash: %v", sendResp.Data["tx_hash"])
|
||||
logger.Infof("✓ 訂單已提交到 LIGHTER - tx_hash: %v", sendResp.Data["tx_hash"])
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -334,7 +334,7 @@ func (t *LighterTraderV2) getMarketIndex(symbol string) (uint8, error) {
|
||||
markets, err := t.fetchMarketList()
|
||||
if err != nil {
|
||||
// 如果 API 失敗,回退到硬編碼映射
|
||||
log.Printf("⚠️ 從 API 獲取市場列表失敗,使用硬編碼映射: %v", err)
|
||||
logger.Infof("⚠️ 從 API 獲取市場列表失敗,使用硬編碼映射: %v", err)
|
||||
return t.getFallbackMarketIndex(symbol)
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ func (t *LighterTraderV2) fetchMarketList() ([]MarketInfo, error) {
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✓ 獲取到 %d 個市場", len(markets))
|
||||
logger.Infof("✓ 獲取到 %d 個市場", len(markets))
|
||||
return markets, nil
|
||||
}
|
||||
|
||||
@@ -428,7 +428,7 @@ func (t *LighterTraderV2) getFallbackMarketIndex(symbol string) (uint8, error) {
|
||||
}
|
||||
|
||||
if index, ok := fallbackMap[symbol]; ok {
|
||||
log.Printf("✓ 使用硬編碼市場索引: %s -> %d", symbol, index)
|
||||
logger.Infof("✓ 使用硬編碼市場索引: %s -> %d", symbol, index)
|
||||
return index, nil
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@ func (t *LighterTraderV2) SetLeverage(symbol string, leverage int) error {
|
||||
}
|
||||
|
||||
// TODO: 使用SDK簽名並提交SetLeverage交易
|
||||
log.Printf("⚙️ 設置杠杆: %s = %dx", symbol, leverage)
|
||||
logger.Infof("⚙️ 設置杠杆: %s = %dx", symbol, leverage)
|
||||
|
||||
return nil // 暫時返回成功
|
||||
}
|
||||
@@ -458,7 +458,7 @@ func (t *LighterTraderV2) SetMarginMode(symbol string, isCrossMargin bool) error
|
||||
modeStr = "全倉"
|
||||
}
|
||||
|
||||
log.Printf("⚙️ 設置倉位模式: %s = %s", symbol, modeStr)
|
||||
logger.Infof("⚙️ 設置倉位模式: %s = %s", symbol, modeStr)
|
||||
|
||||
// TODO: 使用SDK簽名並提交SetMarginMode交易
|
||||
return nil
|
||||
|
||||
@@ -2,13 +2,13 @@ package trader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
)
|
||||
|
||||
// OpenLong 开多仓
|
||||
func (t *LighterTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// TODO: 实现完整的开多仓逻辑
|
||||
log.Printf("🚧 LIGHTER OpenLong 暂未完全实现 (symbol=%s, qty=%.4f, leverage=%d)", symbol, quantity, leverage)
|
||||
logger.Infof("🚧 LIGHTER OpenLong 暂未完全实现 (symbol=%s, qty=%.4f, leverage=%d)", symbol, quantity, leverage)
|
||||
|
||||
// 使用市价买入单
|
||||
orderID, err := t.CreateOrder(symbol, "buy", quantity, 0, "market")
|
||||
@@ -26,7 +26,7 @@ func (t *LighterTrader) OpenLong(symbol string, quantity float64, leverage int)
|
||||
// OpenShort 开空仓
|
||||
func (t *LighterTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
// TODO: 实现完整的开空仓逻辑
|
||||
log.Printf("🚧 LIGHTER OpenShort 暂未完全实现 (symbol=%s, qty=%.4f, leverage=%d)", symbol, quantity, leverage)
|
||||
logger.Infof("🚧 LIGHTER OpenShort 暂未完全实现 (symbol=%s, qty=%.4f, leverage=%d)", symbol, quantity, leverage)
|
||||
|
||||
// 使用市价卖出单
|
||||
orderID, err := t.CreateOrder(symbol, "sell", quantity, 0, "market")
|
||||
@@ -66,7 +66,7 @@ func (t *LighterTrader) CloseLong(symbol string, quantity float64) (map[string]i
|
||||
|
||||
// 平仓后取消所有挂单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
@@ -101,7 +101,7 @@ func (t *LighterTrader) CloseShort(symbol string, quantity float64) (map[string]
|
||||
|
||||
// 平仓后取消所有挂单
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
log.Printf(" ⚠ 取消挂单失败: %v", err)
|
||||
logger.Infof(" ⚠ 取消挂单失败: %v", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
@@ -114,7 +114,7 @@ func (t *LighterTrader) CloseShort(symbol string, quantity float64) (map[string]
|
||||
// SetStopLoss 设置止损单
|
||||
func (t *LighterTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
|
||||
// TODO: 实现完整的止损单逻辑
|
||||
log.Printf("🚧 LIGHTER SetStopLoss 暂未完全实现 (symbol=%s, side=%s, qty=%.4f, stop=%.2f)", symbol, positionSide, quantity, stopPrice)
|
||||
logger.Infof("🚧 LIGHTER SetStopLoss 暂未完全实现 (symbol=%s, side=%s, qty=%.4f, stop=%.2f)", symbol, positionSide, quantity, stopPrice)
|
||||
|
||||
// 确定订单方向(做空止损用买单,做多止损用卖单)
|
||||
side := "sell"
|
||||
@@ -128,14 +128,14 @@ func (t *LighterTrader) SetStopLoss(symbol string, positionSide string, quantity
|
||||
return fmt.Errorf("设置止损失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER - 止损已设置: %.2f (side: %s)", stopPrice, side)
|
||||
logger.Infof("✓ LIGHTER - 止损已设置: %.2f (side: %s)", stopPrice, side)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTakeProfit 设置止盈单
|
||||
func (t *LighterTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
|
||||
// TODO: 实现完整的止盈单逻辑
|
||||
log.Printf("🚧 LIGHTER SetTakeProfit 暂未完全实现 (symbol=%s, side=%s, qty=%.4f, tp=%.2f)", symbol, positionSide, quantity, takeProfitPrice)
|
||||
logger.Infof("🚧 LIGHTER SetTakeProfit 暂未完全实现 (symbol=%s, side=%s, qty=%.4f, tp=%.2f)", symbol, positionSide, quantity, takeProfitPrice)
|
||||
|
||||
// 确定订单方向(做空止盈用买单,做多止盈用卖单)
|
||||
side := "sell"
|
||||
@@ -149,7 +149,7 @@ func (t *LighterTrader) SetTakeProfit(symbol string, positionSide string, quanti
|
||||
return fmt.Errorf("设置止盈失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ LIGHTER - 止盈已设置: %.2f (side: %s)", takeProfitPrice, side)
|
||||
logger.Infof("✓ LIGHTER - 止盈已设置: %.2f (side: %s)", takeProfitPrice, side)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ func (t *LighterTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
|
||||
if isCrossMargin {
|
||||
modeStr = "全仓"
|
||||
}
|
||||
log.Printf("🚧 LIGHTER SetMarginMode 暂未实现 (symbol=%s, mode=%s)", symbol, modeStr)
|
||||
logger.Infof("🚧 LIGHTER SetMarginMode 暂未实现 (symbol=%s, mode=%s)", symbol, modeStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
309
trader/order_sync.go
Normal file
309
trader/order_sync.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package trader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"nofx/store"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OrderSyncManager 订单状态同步管理器
|
||||
// 负责定期扫描所有 NEW 状态的订单,并更新其状态
|
||||
type OrderSyncManager struct {
|
||||
store *store.Store
|
||||
interval time.Duration
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
traderCache map[string]Trader // trader_id -> Trader 实例缓存
|
||||
configCache map[string]*store.TraderFullConfig // trader_id -> 配置缓存
|
||||
cacheMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewOrderSyncManager 创建订单同步管理器
|
||||
func NewOrderSyncManager(st *store.Store, interval time.Duration) *OrderSyncManager {
|
||||
if interval == 0 {
|
||||
interval = 10 * time.Second
|
||||
}
|
||||
return &OrderSyncManager{
|
||||
store: st,
|
||||
interval: interval,
|
||||
stopCh: make(chan struct{}),
|
||||
traderCache: make(map[string]Trader),
|
||||
configCache: make(map[string]*store.TraderFullConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动订单同步服务
|
||||
func (m *OrderSyncManager) Start() {
|
||||
m.wg.Add(1)
|
||||
go m.run()
|
||||
logger.Info("📦 订单同步管理器已启动")
|
||||
}
|
||||
|
||||
// Stop 停止订单同步服务
|
||||
func (m *OrderSyncManager) Stop() {
|
||||
close(m.stopCh)
|
||||
m.wg.Wait()
|
||||
|
||||
// 清理缓存
|
||||
m.cacheMutex.Lock()
|
||||
m.traderCache = make(map[string]Trader)
|
||||
m.configCache = make(map[string]*store.TraderFullConfig)
|
||||
m.cacheMutex.Unlock()
|
||||
|
||||
logger.Info("📦 订单同步管理器已停止")
|
||||
}
|
||||
|
||||
// run 主循环
|
||||
func (m *OrderSyncManager) run() {
|
||||
defer m.wg.Done()
|
||||
|
||||
// 启动时立即执行一次
|
||||
m.syncOrders()
|
||||
|
||||
ticker := time.NewTicker(m.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
m.syncOrders()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syncOrders 同步所有待处理订单
|
||||
func (m *OrderSyncManager) syncOrders() {
|
||||
// 获取所有 NEW 状态的订单
|
||||
orders, err := m.store.Order().GetAllPendingOrders()
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取待处理订单失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(orders) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("📦 开始同步 %d 个待处理订单...", len(orders))
|
||||
|
||||
// 按 trader_id 分组
|
||||
ordersByTrader := make(map[string][]*store.TraderOrder)
|
||||
for _, order := range orders {
|
||||
ordersByTrader[order.TraderID] = append(ordersByTrader[order.TraderID], order)
|
||||
}
|
||||
|
||||
// 逐个 trader 处理
|
||||
for traderID, traderOrders := range ordersByTrader {
|
||||
m.syncTraderOrders(traderID, traderOrders)
|
||||
}
|
||||
}
|
||||
|
||||
// syncTraderOrders 同步单个 trader 的订单
|
||||
func (m *OrderSyncManager) syncTraderOrders(traderID string, orders []*store.TraderOrder) {
|
||||
// 获取或创建 trader 实例
|
||||
trader, err := m.getOrCreateTrader(traderID)
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取 trader 实例失败 (ID: %s): %v", traderID, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, order := range orders {
|
||||
m.syncSingleOrder(trader, order)
|
||||
}
|
||||
}
|
||||
|
||||
// syncSingleOrder 同步单个订单状态
|
||||
func (m *OrderSyncManager) syncSingleOrder(trader Trader, order *store.TraderOrder) {
|
||||
status, err := trader.GetOrderStatus(order.Symbol, order.OrderID)
|
||||
if err != nil {
|
||||
// 查询失败,检查订单创建时间,超过一定时间假设已成交
|
||||
if time.Since(order.CreatedAt) > 5*time.Minute {
|
||||
logger.Infof("⚠️ 订单查询超时,假设已成交 (ID: %s)", order.OrderID)
|
||||
m.markOrderFilled(order, 0, 0, 0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
statusStr, _ := status["status"].(string)
|
||||
|
||||
switch statusStr {
|
||||
case "FILLED":
|
||||
avgPrice, _ := status["avgPrice"].(float64)
|
||||
executedQty, _ := status["executedQty"].(float64)
|
||||
commission, _ := status["commission"].(float64)
|
||||
|
||||
// 如果 API 未返回数量,使用原始数量
|
||||
if executedQty == 0 {
|
||||
executedQty = order.Quantity
|
||||
}
|
||||
|
||||
m.markOrderFilled(order, avgPrice, executedQty, commission)
|
||||
|
||||
case "CANCELED", "EXPIRED":
|
||||
order.Status = statusStr
|
||||
if err := m.store.Order().Update(order); err != nil {
|
||||
logger.Infof("⚠️ 更新订单状态失败: %v", err)
|
||||
} else {
|
||||
logger.Infof("📦 订单状态更新: %s (ID: %s)", statusStr, order.OrderID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// markOrderFilled 标记订单已成交
|
||||
func (m *OrderSyncManager) markOrderFilled(order *store.TraderOrder, avgPrice, executedQty, commission float64) {
|
||||
// 如果 avgPrice 为 0,使用订单价格
|
||||
if avgPrice == 0 {
|
||||
avgPrice = order.Price
|
||||
}
|
||||
if executedQty == 0 {
|
||||
executedQty = order.Quantity
|
||||
}
|
||||
|
||||
// 计算已实现盈亏(仅平仓订单)
|
||||
var realizedPnL float64
|
||||
if (order.Action == "close_long" || order.Action == "close_short") && order.EntryPrice > 0 && avgPrice > 0 {
|
||||
if order.Action == "close_long" {
|
||||
// 平多盈亏 = (平仓价 - 开仓价) * 数量
|
||||
realizedPnL = (avgPrice - order.EntryPrice) * executedQty
|
||||
} else {
|
||||
// 平空盈亏 = (开仓价 - 平仓价) * 数量
|
||||
realizedPnL = (order.EntryPrice - avgPrice) * executedQty
|
||||
}
|
||||
}
|
||||
|
||||
order.AvgPrice = avgPrice
|
||||
order.ExecutedQty = executedQty
|
||||
order.Status = "FILLED"
|
||||
order.Fee = commission
|
||||
order.RealizedPnL = realizedPnL
|
||||
order.FilledAt = time.Now()
|
||||
|
||||
if err := m.store.Order().Update(order); err != nil {
|
||||
logger.Infof("⚠️ 更新订单状态失败: %v", err)
|
||||
} else {
|
||||
if realizedPnL != 0 {
|
||||
logger.Infof("✅ 订单已成交 (ID: %s, avgPrice: %.4f, qty: %.4f, PnL: %.2f)",
|
||||
order.OrderID, avgPrice, executedQty, realizedPnL)
|
||||
} else {
|
||||
logger.Infof("✅ 订单已成交 (ID: %s, avgPrice: %.4f, qty: %.4f)",
|
||||
order.OrderID, avgPrice, executedQty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getOrCreateTrader 获取或创建 trader 实例
|
||||
func (m *OrderSyncManager) getOrCreateTrader(traderID string) (Trader, error) {
|
||||
m.cacheMutex.RLock()
|
||||
trader, exists := m.traderCache[traderID]
|
||||
m.cacheMutex.RUnlock()
|
||||
|
||||
if exists && trader != nil {
|
||||
return trader, nil
|
||||
}
|
||||
|
||||
// 需要创建新的 trader 实例
|
||||
// 首先获取 trader 配置
|
||||
config, err := m.getTraderConfig(traderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取 trader 配置失败: %w", err)
|
||||
}
|
||||
|
||||
// 根据交易所类型创建 trader
|
||||
trader, err = m.createTrader(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 trader 实例失败: %w", err)
|
||||
}
|
||||
|
||||
m.cacheMutex.Lock()
|
||||
m.traderCache[traderID] = trader
|
||||
m.cacheMutex.Unlock()
|
||||
|
||||
return trader, nil
|
||||
}
|
||||
|
||||
// getTraderConfig 获取 trader 配置
|
||||
func (m *OrderSyncManager) getTraderConfig(traderID string) (*store.TraderFullConfig, error) {
|
||||
m.cacheMutex.RLock()
|
||||
config, exists := m.configCache[traderID]
|
||||
m.cacheMutex.RUnlock()
|
||||
|
||||
if exists {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// 从数据库获取 - 需要找到 trader 对应的 userID
|
||||
// 首先查询所有 traders 找到对应的 userID
|
||||
traders, err := m.store.Trader().ListAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取 trader 列表失败: %w", err)
|
||||
}
|
||||
|
||||
var userID string
|
||||
for _, t := range traders {
|
||||
if t.ID == traderID {
|
||||
userID = t.UserID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("找不到 trader: %s", traderID)
|
||||
}
|
||||
|
||||
config, err = m.store.Trader().GetFullConfig(userID, traderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.cacheMutex.Lock()
|
||||
m.configCache[traderID] = config
|
||||
m.cacheMutex.Unlock()
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// createTrader 根据配置创建 trader 实例
|
||||
func (m *OrderSyncManager) createTrader(config *store.TraderFullConfig) (Trader, error) {
|
||||
exchange := config.Exchange
|
||||
|
||||
switch exchange.Type {
|
||||
case "binance":
|
||||
return NewFuturesTrader(exchange.APIKey, exchange.SecretKey, config.Trader.UserID), nil
|
||||
|
||||
case "bybit":
|
||||
return NewBybitTrader(exchange.APIKey, exchange.SecretKey), nil
|
||||
|
||||
case "hyperliquid":
|
||||
return NewHyperliquidTrader(exchange.SecretKey, exchange.HyperliquidWalletAddr, exchange.Testnet)
|
||||
|
||||
case "aster":
|
||||
return NewAsterTrader(exchange.AsterUser, exchange.AsterSigner, exchange.AsterPrivateKey)
|
||||
|
||||
case "lighter":
|
||||
if exchange.LighterAPIKeyPrivateKey != "" {
|
||||
return NewLighterTraderV2(
|
||||
exchange.LighterPrivateKey,
|
||||
exchange.LighterWalletAddr,
|
||||
exchange.LighterAPIKeyPrivateKey,
|
||||
exchange.Testnet,
|
||||
)
|
||||
}
|
||||
return NewLighterTrader(exchange.LighterPrivateKey, exchange.LighterWalletAddr, exchange.Testnet)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的交易所类型: %s", exchange.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateCache 使缓存失效(当配置变更时调用)
|
||||
func (m *OrderSyncManager) InvalidateCache(traderID string) {
|
||||
m.cacheMutex.Lock()
|
||||
defer m.cacheMutex.Unlock()
|
||||
|
||||
delete(m.traderCache, traderID)
|
||||
delete(m.configCache, traderID)
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
package trader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/decision"
|
||||
"nofx/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// MockPartialCloseTrader 用於測試 partial close 邏輯
|
||||
type MockPartialCloseTrader struct {
|
||||
positions []map[string]interface{}
|
||||
closePartialCalled bool
|
||||
closeLongCalled bool
|
||||
closeShortCalled bool
|
||||
stopLossCalled bool
|
||||
takeProfitCalled bool
|
||||
lastStopLoss float64
|
||||
lastTakeProfit float64
|
||||
}
|
||||
|
||||
func (m *MockPartialCloseTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
return m.positions, nil
|
||||
}
|
||||
|
||||
func (m *MockPartialCloseTrader) ClosePartialLong(symbol string, quantity float64) (map[string]interface{}, error) {
|
||||
m.closePartialCalled = true
|
||||
return map[string]interface{}{"orderId": "12345"}, nil
|
||||
}
|
||||
|
||||
func (m *MockPartialCloseTrader) ClosePartialShort(symbol string, quantity float64) (map[string]interface{}, error) {
|
||||
m.closePartialCalled = true
|
||||
return map[string]interface{}{"orderId": "12345"}, nil
|
||||
}
|
||||
|
||||
func (m *MockPartialCloseTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) {
|
||||
m.closeLongCalled = true
|
||||
return map[string]interface{}{"orderId": "12346"}, nil
|
||||
}
|
||||
|
||||
func (m *MockPartialCloseTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) {
|
||||
m.closeShortCalled = true
|
||||
return map[string]interface{}{"orderId": "12346"}, nil
|
||||
}
|
||||
|
||||
func (m *MockPartialCloseTrader) SetStopLoss(symbol, side string, quantity, price float64) error {
|
||||
m.stopLossCalled = true
|
||||
m.lastStopLoss = price
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPartialCloseTrader) SetTakeProfit(symbol, side string, quantity, price float64) error {
|
||||
m.takeProfitCalled = true
|
||||
m.lastTakeProfit = price
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestPartialCloseMinPositionCheck 測試最小倉位檢查邏輯
|
||||
func TestPartialCloseMinPositionCheck(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
totalQuantity float64
|
||||
markPrice float64
|
||||
closePercentage float64
|
||||
expectFullClose bool // 是否應該觸發全平邏輯
|
||||
expectRemainValue float64
|
||||
}{
|
||||
{
|
||||
name: "正常部分平倉_剩餘價值充足",
|
||||
totalQuantity: 1.0,
|
||||
markPrice: 100.0,
|
||||
closePercentage: 50.0,
|
||||
expectFullClose: false,
|
||||
expectRemainValue: 50.0, // 剩餘 0.5 * 100 = 50 USDT
|
||||
},
|
||||
{
|
||||
name: "部分平倉_剩餘價值小於10USDT_應該全平",
|
||||
totalQuantity: 0.2,
|
||||
markPrice: 100.0,
|
||||
closePercentage: 95.0, // 平倉 95%,剩餘 1 USDT (0.2 * 5% * 100)
|
||||
expectFullClose: true,
|
||||
expectRemainValue: 1.0,
|
||||
},
|
||||
{
|
||||
name: "部分平倉_剩餘價值剛好10USDT_應該全平",
|
||||
totalQuantity: 1.0,
|
||||
markPrice: 100.0,
|
||||
closePercentage: 90.0, // 剩餘 10 USDT (1.0 * 10% * 100),邊界測試 (<=)
|
||||
expectFullClose: true,
|
||||
expectRemainValue: 10.0,
|
||||
},
|
||||
{
|
||||
name: "部分平倉_剩餘價值11USDT_不應全平",
|
||||
totalQuantity: 1.1,
|
||||
markPrice: 100.0,
|
||||
closePercentage: 90.0, // 剩餘 11 USDT (1.1 * 10% * 100)
|
||||
expectFullClose: false,
|
||||
expectRemainValue: 11.0,
|
||||
},
|
||||
{
|
||||
name: "大倉位部分平倉_剩餘價值遠大於10USDT",
|
||||
totalQuantity: 10.0,
|
||||
markPrice: 1000.0,
|
||||
closePercentage: 80.0,
|
||||
expectFullClose: false,
|
||||
expectRemainValue: 2000.0, // 剩餘 2 * 1000 = 2000 USDT
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 計算剩餘價值
|
||||
closeQuantity := tt.totalQuantity * (tt.closePercentage / 100.0)
|
||||
remainingQuantity := tt.totalQuantity - closeQuantity
|
||||
remainingValue := remainingQuantity * tt.markPrice
|
||||
|
||||
// 驗證計算(使用浮點數比較允許微小誤差)
|
||||
const epsilon = 0.001
|
||||
if remainingValue-tt.expectRemainValue > epsilon || tt.expectRemainValue-remainingValue > epsilon {
|
||||
t.Errorf("計算錯誤: 剩餘價值 = %.2f, 期望 = %.2f",
|
||||
remainingValue, tt.expectRemainValue)
|
||||
}
|
||||
|
||||
// 驗證最小倉位檢查邏輯
|
||||
const MIN_POSITION_VALUE = 10.0
|
||||
shouldFullClose := remainingValue > 0 && remainingValue <= MIN_POSITION_VALUE
|
||||
|
||||
if shouldFullClose != tt.expectFullClose {
|
||||
t.Errorf("最小倉位檢查失敗: shouldFullClose = %v, 期望 = %v (剩餘價值 = %.2f USDT)",
|
||||
shouldFullClose, tt.expectFullClose, remainingValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPartialCloseWithStopLossTakeProfitRecovery 測試止盈止損恢復邏輯
|
||||
func TestPartialCloseWithStopLossTakeProfitRecovery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
newStopLoss float64
|
||||
newTakeProfit float64
|
||||
expectStopLoss bool
|
||||
expectTakeProfit bool
|
||||
}{
|
||||
{
|
||||
name: "有新止損和止盈_應該恢復兩者",
|
||||
newStopLoss: 95.0,
|
||||
newTakeProfit: 110.0,
|
||||
expectStopLoss: true,
|
||||
expectTakeProfit: true,
|
||||
},
|
||||
{
|
||||
name: "只有新止損_僅恢復止損",
|
||||
newStopLoss: 95.0,
|
||||
newTakeProfit: 0,
|
||||
expectStopLoss: true,
|
||||
expectTakeProfit: false,
|
||||
},
|
||||
{
|
||||
name: "只有新止盈_僅恢復止盈",
|
||||
newStopLoss: 0,
|
||||
newTakeProfit: 110.0,
|
||||
expectStopLoss: false,
|
||||
expectTakeProfit: true,
|
||||
},
|
||||
{
|
||||
name: "沒有新止損止盈_不恢復",
|
||||
newStopLoss: 0,
|
||||
newTakeProfit: 0,
|
||||
expectStopLoss: false,
|
||||
expectTakeProfit: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 模擬止盈止損恢復邏輯
|
||||
stopLossRecovered := tt.newStopLoss > 0
|
||||
takeProfitRecovered := tt.newTakeProfit > 0
|
||||
|
||||
if stopLossRecovered != tt.expectStopLoss {
|
||||
t.Errorf("止損恢復邏輯錯誤: recovered = %v, 期望 = %v",
|
||||
stopLossRecovered, tt.expectStopLoss)
|
||||
}
|
||||
|
||||
if takeProfitRecovered != tt.expectTakeProfit {
|
||||
t.Errorf("止盈恢復邏輯錯誤: recovered = %v, 期望 = %v",
|
||||
takeProfitRecovered, tt.expectTakeProfit)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPartialCloseEdgeCases 測試邊界情況
|
||||
func TestPartialCloseEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
closePercentage float64
|
||||
totalQuantity float64
|
||||
markPrice float64
|
||||
expectError bool
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "平倉百分比為0_應該報錯",
|
||||
closePercentage: 0,
|
||||
totalQuantity: 1.0,
|
||||
markPrice: 100.0,
|
||||
expectError: true,
|
||||
errorContains: "0-100",
|
||||
},
|
||||
{
|
||||
name: "平倉百分比超過100_應該報錯",
|
||||
closePercentage: 101.0,
|
||||
totalQuantity: 1.0,
|
||||
markPrice: 100.0,
|
||||
expectError: true,
|
||||
errorContains: "0-100",
|
||||
},
|
||||
{
|
||||
name: "平倉百分比為負數_應該報錯",
|
||||
closePercentage: -10.0,
|
||||
totalQuantity: 1.0,
|
||||
markPrice: 100.0,
|
||||
expectError: true,
|
||||
errorContains: "0-100",
|
||||
},
|
||||
{
|
||||
name: "正常範圍_不應報錯",
|
||||
closePercentage: 50.0,
|
||||
totalQuantity: 1.0,
|
||||
markPrice: 100.0,
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 模擬百分比驗證邏輯
|
||||
var err error
|
||||
if tt.closePercentage <= 0 || tt.closePercentage > 100 {
|
||||
err = fmt.Errorf("平仓百分比必须在 0-100 之间,当前: %.1f", tt.closePercentage)
|
||||
}
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("期望報錯但沒有報錯")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("不應報錯但報錯了: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPartialCloseIntegration 整合測試(使用 mock trader)
|
||||
func TestPartialCloseIntegration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symbol string
|
||||
side string
|
||||
totalQuantity float64
|
||||
markPrice float64
|
||||
closePercentage float64
|
||||
newStopLoss float64
|
||||
newTakeProfit float64
|
||||
expectFullClose bool
|
||||
expectStopLossCall bool
|
||||
expectTakeProfitCall bool
|
||||
}{
|
||||
{
|
||||
name: "LONG倉_正常部分平倉_有止盈止損",
|
||||
symbol: "BTCUSDT",
|
||||
side: "LONG",
|
||||
totalQuantity: 1.0,
|
||||
markPrice: 50000.0,
|
||||
closePercentage: 50.0,
|
||||
newStopLoss: 48000.0,
|
||||
newTakeProfit: 52000.0,
|
||||
expectFullClose: false,
|
||||
expectStopLossCall: true,
|
||||
expectTakeProfitCall: true,
|
||||
},
|
||||
{
|
||||
name: "SHORT倉_剩餘價值過小_應自動全平",
|
||||
symbol: "ETHUSDT",
|
||||
side: "SHORT",
|
||||
totalQuantity: 0.02,
|
||||
markPrice: 3000.0, // 總價值 60 USDT
|
||||
closePercentage: 95.0, // 剩餘 3 USDT < 10 USDT
|
||||
newStopLoss: 3100.0,
|
||||
newTakeProfit: 2900.0,
|
||||
expectFullClose: true,
|
||||
expectStopLossCall: false, // 全平不需要恢復止盈止損
|
||||
expectTakeProfitCall: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 創建 mock trader
|
||||
mockTrader := &MockPartialCloseTrader{
|
||||
positions: []map[string]interface{}{
|
||||
{
|
||||
"symbol": tt.symbol,
|
||||
"side": tt.side,
|
||||
"quantity": tt.totalQuantity,
|
||||
"markPrice": tt.markPrice,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 創建決策
|
||||
dec := &decision.Decision{
|
||||
Symbol: tt.symbol,
|
||||
Action: "partial_close",
|
||||
ClosePercentage: tt.closePercentage,
|
||||
NewStopLoss: tt.newStopLoss,
|
||||
NewTakeProfit: tt.newTakeProfit,
|
||||
}
|
||||
|
||||
// 創建 actionRecord
|
||||
actionRecord := &logger.DecisionAction{}
|
||||
|
||||
// 計算剩餘價值
|
||||
closeQuantity := tt.totalQuantity * (tt.closePercentage / 100.0)
|
||||
remainingQuantity := tt.totalQuantity - closeQuantity
|
||||
remainingValue := remainingQuantity * tt.markPrice
|
||||
|
||||
// 驗證最小倉位檢查
|
||||
const MIN_POSITION_VALUE = 10.0
|
||||
shouldFullClose := remainingValue > 0 && remainingValue <= MIN_POSITION_VALUE
|
||||
|
||||
if shouldFullClose != tt.expectFullClose {
|
||||
t.Errorf("最小倉位檢查不符: shouldFullClose = %v, 期望 = %v (剩餘 %.2f USDT)",
|
||||
shouldFullClose, tt.expectFullClose, remainingValue)
|
||||
}
|
||||
|
||||
// 模擬執行邏輯
|
||||
if shouldFullClose {
|
||||
// 應該轉為全平
|
||||
if tt.side == "LONG" {
|
||||
mockTrader.CloseLong(tt.symbol, tt.totalQuantity)
|
||||
} else {
|
||||
mockTrader.CloseShort(tt.symbol, tt.totalQuantity)
|
||||
}
|
||||
} else {
|
||||
// 正常部分平倉
|
||||
if tt.side == "LONG" {
|
||||
mockTrader.ClosePartialLong(tt.symbol, closeQuantity)
|
||||
} else {
|
||||
mockTrader.ClosePartialShort(tt.symbol, closeQuantity)
|
||||
}
|
||||
|
||||
// 恢復止盈止損
|
||||
if dec.NewStopLoss > 0 {
|
||||
mockTrader.SetStopLoss(tt.symbol, tt.side, remainingQuantity, dec.NewStopLoss)
|
||||
}
|
||||
if dec.NewTakeProfit > 0 {
|
||||
mockTrader.SetTakeProfit(tt.symbol, tt.side, remainingQuantity, dec.NewTakeProfit)
|
||||
}
|
||||
}
|
||||
|
||||
// 驗證調用
|
||||
if tt.expectFullClose {
|
||||
if !mockTrader.closeLongCalled && !mockTrader.closeShortCalled {
|
||||
t.Error("期望調用全平但沒有調用")
|
||||
}
|
||||
if mockTrader.closePartialCalled {
|
||||
t.Error("不應該調用部分平倉")
|
||||
}
|
||||
} else {
|
||||
if !mockTrader.closePartialCalled {
|
||||
t.Error("期望調用部分平倉但沒有調用")
|
||||
}
|
||||
}
|
||||
|
||||
if mockTrader.stopLossCalled != tt.expectStopLossCall {
|
||||
t.Errorf("止損調用不符: called = %v, 期望 = %v",
|
||||
mockTrader.stopLossCalled, tt.expectStopLossCall)
|
||||
}
|
||||
|
||||
if mockTrader.takeProfitCalled != tt.expectTakeProfitCall {
|
||||
t.Errorf("止盈調用不符: called = %v, 期望 = %v",
|
||||
mockTrader.takeProfitCalled, tt.expectTakeProfitCall)
|
||||
}
|
||||
|
||||
_ = actionRecord // 避免未使用警告
|
||||
})
|
||||
}
|
||||
}
|
||||
318
trader/position_sync.go
Normal file
318
trader/position_sync.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package trader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"nofx/store"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PositionSyncManager 仓位状态同步管理器
|
||||
// 负责定期同步交易所仓位,检测手动平仓等变化
|
||||
type PositionSyncManager struct {
|
||||
store *store.Store
|
||||
interval time.Duration
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
traderCache map[string]Trader // trader_id -> Trader 实例缓存
|
||||
configCache map[string]*store.TraderFullConfig // trader_id -> 配置缓存
|
||||
cacheMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPositionSyncManager 创建仓位同步管理器
|
||||
func NewPositionSyncManager(st *store.Store, interval time.Duration) *PositionSyncManager {
|
||||
if interval == 0 {
|
||||
interval = 10 * time.Second
|
||||
}
|
||||
return &PositionSyncManager{
|
||||
store: st,
|
||||
interval: interval,
|
||||
stopCh: make(chan struct{}),
|
||||
traderCache: make(map[string]Trader),
|
||||
configCache: make(map[string]*store.TraderFullConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动仓位同步服务
|
||||
func (m *PositionSyncManager) Start() {
|
||||
m.wg.Add(1)
|
||||
go m.run()
|
||||
logger.Info("📊 仓位同步管理器已启动")
|
||||
}
|
||||
|
||||
// Stop 停止仓位同步服务
|
||||
func (m *PositionSyncManager) Stop() {
|
||||
close(m.stopCh)
|
||||
m.wg.Wait()
|
||||
|
||||
// 清理缓存
|
||||
m.cacheMutex.Lock()
|
||||
m.traderCache = make(map[string]Trader)
|
||||
m.configCache = make(map[string]*store.TraderFullConfig)
|
||||
m.cacheMutex.Unlock()
|
||||
|
||||
logger.Info("📊 仓位同步管理器已停止")
|
||||
}
|
||||
|
||||
// run 主循环
|
||||
func (m *PositionSyncManager) run() {
|
||||
defer m.wg.Done()
|
||||
|
||||
// 启动时立即执行一次
|
||||
m.syncPositions()
|
||||
|
||||
ticker := time.NewTicker(m.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
m.syncPositions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syncPositions 同步所有仓位状态
|
||||
func (m *PositionSyncManager) syncPositions() {
|
||||
// 获取所有 OPEN 状态的仓位
|
||||
localPositions, err := m.store.Position().GetAllOpenPositions()
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取本地仓位失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(localPositions) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 按 trader_id 分组
|
||||
positionsByTrader := make(map[string][]*store.TraderPosition)
|
||||
for _, pos := range localPositions {
|
||||
positionsByTrader[pos.TraderID] = append(positionsByTrader[pos.TraderID], pos)
|
||||
}
|
||||
|
||||
// 逐个 trader 处理
|
||||
for traderID, traderPositions := range positionsByTrader {
|
||||
m.syncTraderPositions(traderID, traderPositions)
|
||||
}
|
||||
}
|
||||
|
||||
// syncTraderPositions 同步单个 trader 的仓位
|
||||
func (m *PositionSyncManager) syncTraderPositions(traderID string, localPositions []*store.TraderPosition) {
|
||||
// 获取或创建 trader 实例
|
||||
trader, err := m.getOrCreateTrader(traderID)
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取 trader 实例失败 (ID: %s): %v", traderID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取交易所当前仓位
|
||||
exchangePositions, err := trader.GetPositions()
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 获取交易所仓位失败 (ID: %s): %v", traderID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建交易所仓位 map: symbol_side -> position
|
||||
exchangeMap := make(map[string]map[string]interface{})
|
||||
for _, pos := range exchangePositions {
|
||||
symbol, _ := pos["symbol"].(string)
|
||||
side, _ := pos["positionSide"].(string)
|
||||
if symbol == "" || side == "" {
|
||||
continue
|
||||
}
|
||||
key := fmt.Sprintf("%s_%s", symbol, side)
|
||||
exchangeMap[key] = pos
|
||||
}
|
||||
|
||||
// 对比本地和交易所仓位
|
||||
for _, localPos := range localPositions {
|
||||
key := fmt.Sprintf("%s_%s", localPos.Symbol, localPos.Side)
|
||||
exchangePos, exists := exchangeMap[key]
|
||||
|
||||
if !exists {
|
||||
// 交易所没有这个仓位了 → 已被平仓
|
||||
m.closeLocalPosition(localPos, trader, "manual")
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查数量是否为0或很小
|
||||
qty := getFloatFromMap(exchangePos, "positionAmt")
|
||||
if qty < 0 {
|
||||
qty = -qty // 空仓数量是负的
|
||||
}
|
||||
|
||||
if qty < 0.0000001 {
|
||||
// 数量为0,仓位已平
|
||||
m.closeLocalPosition(localPos, trader, "manual")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// closeLocalPosition 标记本地仓位为已平仓
|
||||
func (m *PositionSyncManager) closeLocalPosition(pos *store.TraderPosition, trader Trader, reason string) {
|
||||
// 尝试获取最后成交价作为平仓价
|
||||
exitPrice := pos.EntryPrice // 默认用开仓价
|
||||
|
||||
// 尝试从交易所获取最新价格
|
||||
if price, err := trader.GetMarketPrice(pos.Symbol); err == nil && price > 0 {
|
||||
exitPrice = price
|
||||
}
|
||||
|
||||
// 计算盈亏
|
||||
var realizedPnL float64
|
||||
if pos.Side == "LONG" {
|
||||
realizedPnL = (exitPrice - pos.EntryPrice) * pos.Quantity
|
||||
} else {
|
||||
realizedPnL = (pos.EntryPrice - exitPrice) * pos.Quantity
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
err := m.store.Position().ClosePosition(
|
||||
pos.ID,
|
||||
exitPrice,
|
||||
"", // 手动平仓没有订单ID
|
||||
realizedPnL,
|
||||
0, // 手动平仓无法获取手续费
|
||||
reason,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ 更新仓位状态失败: %v", err)
|
||||
} else {
|
||||
logger.Infof("📊 仓位已平仓 [%s] %s %s @ %.4f → %.4f, PnL: %.2f (%s)",
|
||||
pos.TraderID[:8], pos.Symbol, pos.Side, pos.EntryPrice, exitPrice, realizedPnL, reason)
|
||||
}
|
||||
}
|
||||
|
||||
// getOrCreateTrader 获取或创建 trader 实例
|
||||
func (m *PositionSyncManager) getOrCreateTrader(traderID string) (Trader, error) {
|
||||
m.cacheMutex.RLock()
|
||||
trader, exists := m.traderCache[traderID]
|
||||
m.cacheMutex.RUnlock()
|
||||
|
||||
if exists && trader != nil {
|
||||
return trader, nil
|
||||
}
|
||||
|
||||
// 需要创建新的 trader 实例
|
||||
config, err := m.getTraderConfig(traderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取 trader 配置失败: %w", err)
|
||||
}
|
||||
|
||||
trader, err = m.createTrader(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 trader 实例失败: %w", err)
|
||||
}
|
||||
|
||||
m.cacheMutex.Lock()
|
||||
m.traderCache[traderID] = trader
|
||||
m.cacheMutex.Unlock()
|
||||
|
||||
return trader, nil
|
||||
}
|
||||
|
||||
// getTraderConfig 获取 trader 配置
|
||||
func (m *PositionSyncManager) getTraderConfig(traderID string) (*store.TraderFullConfig, error) {
|
||||
m.cacheMutex.RLock()
|
||||
config, exists := m.configCache[traderID]
|
||||
m.cacheMutex.RUnlock()
|
||||
|
||||
if exists {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
traders, err := m.store.Trader().ListAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取 trader 列表失败: %w", err)
|
||||
}
|
||||
|
||||
var userID string
|
||||
for _, t := range traders {
|
||||
if t.ID == traderID {
|
||||
userID = t.UserID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("找不到 trader: %s", traderID)
|
||||
}
|
||||
|
||||
config, err = m.store.Trader().GetFullConfig(userID, traderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.cacheMutex.Lock()
|
||||
m.configCache[traderID] = config
|
||||
m.cacheMutex.Unlock()
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// createTrader 根据配置创建 trader 实例
|
||||
func (m *PositionSyncManager) createTrader(config *store.TraderFullConfig) (Trader, error) {
|
||||
exchange := config.Exchange
|
||||
|
||||
switch exchange.Type {
|
||||
case "binance":
|
||||
return NewFuturesTrader(exchange.APIKey, exchange.SecretKey, config.Trader.UserID), nil
|
||||
|
||||
case "bybit":
|
||||
return NewBybitTrader(exchange.APIKey, exchange.SecretKey), nil
|
||||
|
||||
case "hyperliquid":
|
||||
return NewHyperliquidTrader(exchange.SecretKey, exchange.HyperliquidWalletAddr, exchange.Testnet)
|
||||
|
||||
case "aster":
|
||||
return NewAsterTrader(exchange.AsterUser, exchange.AsterSigner, exchange.AsterPrivateKey)
|
||||
|
||||
case "lighter":
|
||||
if exchange.LighterAPIKeyPrivateKey != "" {
|
||||
return NewLighterTraderV2(
|
||||
exchange.LighterPrivateKey,
|
||||
exchange.LighterWalletAddr,
|
||||
exchange.LighterAPIKeyPrivateKey,
|
||||
exchange.Testnet,
|
||||
)
|
||||
}
|
||||
return NewLighterTrader(exchange.LighterPrivateKey, exchange.LighterWalletAddr, exchange.Testnet)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的交易所类型: %s", exchange.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateCache 使缓存失效
|
||||
func (m *PositionSyncManager) InvalidateCache(traderID string) {
|
||||
m.cacheMutex.Lock()
|
||||
defer m.cacheMutex.Unlock()
|
||||
|
||||
delete(m.traderCache, traderID)
|
||||
delete(m.configCache, traderID)
|
||||
}
|
||||
|
||||
// getFloatFromMap 从 map 中获取 float64 值
|
||||
func getFloatFromMap(m map[string]interface{}, key string) float64 {
|
||||
if v, ok := m[key]; ok {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return val
|
||||
case int64:
|
||||
return float64(val)
|
||||
case int:
|
||||
return float64(val)
|
||||
case string:
|
||||
var f float64
|
||||
fmt.Sscanf(val, "%f", &f)
|
||||
return f
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user