mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
* feat: 添加 OKX 交易所支持(USDT Perpetual Swap) ## 新增功能 - 實現完整的 OKX API v5 REST 客戶端(純 Go 標準庫,無外部依賴) - 支持 USDT 永續合約交易(BTC-USDT-SWAP 等) - 實現 Trader 接口的 13 個核心方法 ## 技術細節 ### trader/okx_trader.go (NEW) - HMAC-SHA256 簽名機制(完全符合 OKX API v5 規範) - 餘額和持倉緩存(15秒,參考 Binance 實現) - 支持 Demo Trading(testnet 模式) - Symbol 格式轉換(BTCUSDT ↔ BTC-USDT-SWAP) - 全倉模式(Cross Margin)支持 - 自動槓桿設置 ### 實現的接口方法: - ✅ GetBalance() - 獲取賬戶餘額 - ✅ GetPositions() - 獲取所有持倉 - ✅ OpenLong() / OpenShort() - 開倉 - ✅ CloseLong() / CloseShort() - 平倉 - ✅ SetLeverage() - 設置槓桿 - ✅ SetMarginMode() - 設置保證金模式 - ✅ GetMarketPrice() - 獲取市場價格 - ✅ FormatQuantity() - 格式化數量 - ⚠️ 止盈止損功能標記為 TODO(非核心交易功能) ### config/database.go (MODIFIED) - 添加 "okx" 到預設交易所列表 - 新增 okx_passphrase 字段(OKX 需要 3 個認證參數) - 更新 ExchangeConfig 結構 - 添加數據庫遷移語句(ALTER TABLE) ### api/server.go (MODIFIED) - 在 handleCreateTrader() 添加 OKX 初始化邏輯 - switch case "okx" 分支 ## 代碼品質 - 代碼行數:~450 行 - 外部依賴:0 個 - 編譯狀態:✅ 通過 - 測試覆蓋:待實現(下一步) ## 待完成事項 - [ ] 撰寫單元測試(目標 >80% 覆蓋率) - [ ] 完善數據庫查詢邏輯(GetExchanges 添加 OKX passphrase 掃描) - [ ] 實現止盈止損功能(可選) * refactor: 完善 OKX passphrase 數據庫和 API 支持 - config/database.go: • GetExchanges() 添加 okx_passphrase 查詢和解密 • UpdateExchange() 函數簽名添加 okxPassphrase 參數 • UpdateExchange() UPDATE 邏輯添加 okx_passphrase SET 子句 • UpdateExchange() INSERT 添加 okx_passphrase 加密和列 - api/server.go: • UpdateExchangeConfigRequest 添加 OKXPassphrase 字段 • UpdateExchange 調用添加 OKXPassphrase 參數 - api/utils.go: • SanitizeExchangeConfigForLog 添加 OKXPassphrase 脫敏 ✅ 編譯測試通過,OKX 完整功能支持完成 * test: 添加 OKX Trader 完整單元測試套件 📊 測試覆蓋率:92.6% (遠超 80% 目標) ✅ 完成的測試: - 接口兼容性測試 - NewOKXTrader 構造函數測試(5個場景) - 符號格式轉換測試(5個場景) - HMAC-SHA256 簽名一致性測試 - GetBalance 測試(含字段驗證) - GetPositions 測試(含標準化數據驗證) - GetMarketPrice 測試(3個場景) - FormatQuantity 測試(5個場景) - SetLeverage/SetMarginMode 測試 - OpenLong/OpenShort 測試 - CloseLong/CloseShort 測試 - 緩存機制測試 - 錯誤處理測試(API錯誤、網絡錯誤、JSON錯誤) 🔧 測試套件架構: - OKXTraderTestSuite 繼承 TraderTestSuite - Mock HTTP 服務器模擬 OKX API v5 響應 - 完整覆蓋所有公開方法 - 包含邊界條件和錯誤場景測試 📈 方法覆蓋率明細: - request: 90.0% - GetBalance: 97.0% - GetPositions: 83.3% - formatSymbol, OpenLong, OpenShort, CloseLong, CloseShort: 100% - placeOrder, SetMarginMode, FormatQuantity, clearCache: 100% - Cancel* 方法系列: 100% - SetLeverage: 81.8% - GetMarketPrice: 85.7% --------- Co-authored-by: the-dev-z <the-dev-z@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
110 lines
3.3 KiB
Go
110 lines
3.3 KiB
Go
package api
|
||
|
||
import "strings"
|
||
|
||
// MaskSensitiveString 脱敏敏感字符串,只显示前4位和后4位
|
||
// 用于脱敏 API Key、Secret Key、Private Key 等敏感信息
|
||
func MaskSensitiveString(s string) string {
|
||
if s == "" {
|
||
return ""
|
||
}
|
||
length := len(s)
|
||
if length <= 8 {
|
||
return "****" // 字符串太短,全部隐藏
|
||
}
|
||
return s[:4] + "****" + s[length-4:]
|
||
}
|
||
|
||
// SanitizeModelConfigForLog 脱敏模型配置用于日志输出
|
||
func SanitizeModelConfigForLog(models map[string]struct {
|
||
Enabled bool `json:"enabled"`
|
||
APIKey string `json:"api_key"`
|
||
CustomAPIURL string `json:"custom_api_url"`
|
||
CustomModelName string `json:"custom_model_name"`
|
||
}) map[string]interface{} {
|
||
safe := make(map[string]interface{})
|
||
for modelID, cfg := range models {
|
||
safe[modelID] = map[string]interface{}{
|
||
"enabled": cfg.Enabled,
|
||
"api_key": MaskSensitiveString(cfg.APIKey),
|
||
"custom_api_url": cfg.CustomAPIURL,
|
||
"custom_model_name": cfg.CustomModelName,
|
||
}
|
||
}
|
||
return safe
|
||
}
|
||
|
||
// SanitizeExchangeConfigForLog 脱敏交易所配置用于日志输出
|
||
func SanitizeExchangeConfigForLog(exchanges map[string]struct {
|
||
Enabled bool `json:"enabled"`
|
||
APIKey string `json:"api_key"`
|
||
SecretKey string `json:"secret_key"`
|
||
Testnet bool `json:"testnet"`
|
||
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
||
AsterUser string `json:"aster_user"`
|
||
AsterSigner string `json:"aster_signer"`
|
||
AsterPrivateKey string `json:"aster_private_key"`
|
||
LighterWalletAddr string `json:"lighter_wallet_addr"`
|
||
LighterPrivateKey string `json:"lighter_private_key"`
|
||
OKXPassphrase string `json:"okx_passphrase"`
|
||
}) map[string]interface{} {
|
||
safe := make(map[string]interface{})
|
||
for exchangeID, cfg := range exchanges {
|
||
safeExchange := map[string]interface{}{
|
||
"enabled": cfg.Enabled,
|
||
"testnet": cfg.Testnet,
|
||
}
|
||
|
||
// 只在有值时才添加脱敏后的敏感字段
|
||
if cfg.APIKey != "" {
|
||
safeExchange["api_key"] = MaskSensitiveString(cfg.APIKey)
|
||
}
|
||
if cfg.SecretKey != "" {
|
||
safeExchange["secret_key"] = MaskSensitiveString(cfg.SecretKey)
|
||
}
|
||
if cfg.AsterPrivateKey != "" {
|
||
safeExchange["aster_private_key"] = MaskSensitiveString(cfg.AsterPrivateKey)
|
||
}
|
||
if cfg.LighterPrivateKey != "" {
|
||
safeExchange["lighter_private_key"] = MaskSensitiveString(cfg.LighterPrivateKey)
|
||
}
|
||
if cfg.OKXPassphrase != "" {
|
||
safeExchange["okx_passphrase"] = MaskSensitiveString(cfg.OKXPassphrase)
|
||
}
|
||
|
||
// 非敏感字段直接添加
|
||
if cfg.HyperliquidWalletAddr != "" {
|
||
safeExchange["hyperliquid_wallet_addr"] = cfg.HyperliquidWalletAddr
|
||
}
|
||
if cfg.AsterUser != "" {
|
||
safeExchange["aster_user"] = cfg.AsterUser
|
||
}
|
||
if cfg.AsterSigner != "" {
|
||
safeExchange["aster_signer"] = cfg.AsterSigner
|
||
}
|
||
if cfg.LighterWalletAddr != "" {
|
||
safeExchange["lighter_wallet_addr"] = cfg.LighterWalletAddr
|
||
}
|
||
|
||
safe[exchangeID] = safeExchange
|
||
}
|
||
return safe
|
||
}
|
||
|
||
// MaskEmail 脱敏邮箱地址,保留前2位和@后部分
|
||
func MaskEmail(email string) string {
|
||
if email == "" {
|
||
return ""
|
||
}
|
||
parts := strings.Split(email, "@")
|
||
if len(parts) != 2 {
|
||
return "****" // 格式不正确
|
||
}
|
||
username := parts[0]
|
||
domain := parts[1]
|
||
if len(username) <= 2 {
|
||
return "**@" + domain
|
||
}
|
||
return username[:2] + "****@" + domain
|
||
}
|