mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
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 <tinklefund@gmail.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
// 交易平台选择(二选一)
|
||||
|
||||
19
main.go
19
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -19,10 +19,38 @@ type Page = 'competition' | 'trader';
|
||||
|
||||
function App() {
|
||||
const { language, setLanguage } = useLanguage();
|
||||
const [currentPage, setCurrentPage] = useState<Page>('competition');
|
||||
|
||||
// 从URL hash读取初始页面状态(支持刷新保持页面)
|
||||
const getInitialPage = (): Page => {
|
||||
const hash = window.location.hash.slice(1); // 去掉 #
|
||||
return hash === 'trader' || hash === 'details' ? 'trader' : 'competition';
|
||||
};
|
||||
|
||||
const [currentPage, setCurrentPage] = useState<Page>(getInitialPage());
|
||||
const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
|
||||
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
|
||||
|
||||
// 监听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<TraderInfo[]>('traders', api.getTraders, {
|
||||
refreshInterval: 10000,
|
||||
@@ -180,7 +208,7 @@ function App() {
|
||||
{/* Page Toggle */}
|
||||
<div className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('competition')}
|
||||
onClick={() => navigateToPage('competition')}
|
||||
className="px-2 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-semibold transition-all"
|
||||
style={currentPage === 'competition'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
@@ -190,7 +218,7 @@ function App() {
|
||||
{t('competition', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('trader')}
|
||||
onClick={() => navigateToPage('trader')}
|
||||
className="px-2 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-semibold transition-all"
|
||||
style={currentPage === 'trader'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
|
||||
Reference in New Issue
Block a user