From c4e79d9579b596f11873d5586763f37e349704f4 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Sun, 28 Jun 2026 11:36:56 +0800 Subject: [PATCH] Fix Claw402 autopilot launch and accounting --- api/handler_competition.go | 98 +++-- store/equity.go | 23 +- trader/auto_trader.go | 7 +- trader/auto_trader_decision.go | 17 +- trader/hyperliquid/trader_account.go | 121 +++++- trader/hyperliquid/trader_account_test.go | 48 +++ web/src/components/trader/AITradersPage.tsx | 85 +++- .../trader/AutopilotLaunchPanel.tsx | 61 ++- .../components/trader/ConfigStatusGrid.tsx | 28 +- .../components/trader/ExchangeConfigModal.tsx | 10 +- .../components/trader/ModelConfigModal.tsx | 367 +++++++++++++----- .../trader/TraderLaunchGuestPage.tsx | 64 ++- 12 files changed, 724 insertions(+), 205 deletions(-) create mode 100644 trader/hyperliquid/trader_account_test.go diff --git a/api/handler_competition.go b/api/handler_competition.go index a0354167..65bdfa2c 100644 --- a/api/handler_competition.go +++ b/api/handler_competition.go @@ -165,34 +165,80 @@ func (s *Server) handleEquityHistory(c *gin.Context) { MarginUsedPct float64 `json:"margin_used_pct"` // Margin used percentage } - // Use the balance of the first record as initial balance to calculate return rate - initialBalance := snapshots[0].Balance + initialBalance := trader.InitialBalance + if initialBalance <= 0 { + initialBalance = snapshots[0].TotalEquity + } if initialBalance == 0 { initialBalance = 1 // Avoid division by zero } var history []EquityPoint + var lastSnapshotTime time.Time for _, snap := range snapshots { - // Calculate PnL percentage + totalPnL := snap.TotalEquity - initialBalance totalPnLPct := 0.0 if initialBalance > 0 { - totalPnLPct = (snap.UnrealizedPnL / initialBalance) * 100 + totalPnLPct = (totalPnL / initialBalance) * 100 } history = append(history, EquityPoint{ Timestamp: snap.Timestamp.Format("2006-01-02 15:04:05"), TotalEquity: snap.TotalEquity, - AvailableBalance: snap.Balance, - TotalPnL: snap.UnrealizedPnL, + AvailableBalance: equitySnapshotAvailableBalance(snap), + TotalPnL: totalPnL, TotalPnLPct: totalPnLPct, PositionCount: snap.PositionCount, MarginUsedPct: snap.MarginUsedPct, }) + if snap.Timestamp.After(lastSnapshotTime) { + lastSnapshotTime = snap.Timestamp + } + } + + if runtimeTrader, err := s.traderManager.GetTrader(traderID); err == nil { + if accountInfo, err := runtimeTrader.GetAccountInfo(); err == nil && time.Since(lastSnapshotTime) > 30*time.Second { + totalEquity := floatFromMap(accountInfo, "total_equity") + totalPnL := totalEquity - initialBalance + totalPnLPct := 0.0 + if initialBalance > 0 { + totalPnLPct = (totalPnL / initialBalance) * 100 + } + history = append(history, EquityPoint{ + Timestamp: time.Now().UTC().Format("2006-01-02 15:04:05"), + TotalEquity: totalEquity, + AvailableBalance: floatFromMap(accountInfo, "available_balance"), + TotalPnL: totalPnL, + TotalPnLPct: totalPnLPct, + PositionCount: int(floatFromMap(accountInfo, "position_count")), + MarginUsedPct: floatFromMap(accountInfo, "margin_used_pct"), + }) + } } c.JSON(http.StatusOK, history) } +func equitySnapshotAvailableBalance(snap *store.EquitySnapshot) float64 { + if snap == nil { + return 0 + } + if snap.AvailableBalance != 0 || snap.PositionCount > 0 { + return snap.AvailableBalance + } + return snap.Balance +} + +func floatFromMap(values map[string]interface{}, key string) float64 { + if value, ok := values[key].(float64); ok { + return value + } + if value, ok := values[key].(int); ok { + return float64(value) + } + return 0 +} + // handlePublicTraderList Get public trader list (no authentication required) func (s *Server) handlePublicTraderList(c *gin.Context) { // Get trader information from all users @@ -386,18 +432,20 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string, hours int) map[s history := make([]map[string]interface{}, 0, len(snapshots)+1) var lastSnapshotTime time.Time for _, snap := range snapshots { + totalPnL := snap.TotalEquity - initialBalance // Calculate PnL percentage: (current_equity - initial_balance) / initial_balance * 100 pnlPct := 0.0 if initialBalance > 0 { - pnlPct = (snap.TotalEquity - initialBalance) / initialBalance * 100 + pnlPct = totalPnL / initialBalance * 100 } history = append(history, map[string]interface{}{ - "timestamp": snap.Timestamp, - "total_equity": snap.TotalEquity, - "total_pnl": snap.UnrealizedPnL, - "total_pnl_pct": pnlPct, - "balance": snap.Balance, + "timestamp": snap.Timestamp, + "total_equity": snap.TotalEquity, + "available_balance": equitySnapshotAvailableBalance(snap), + "total_pnl": totalPnL, + "total_pnl_pct": pnlPct, + "balance": snap.Balance, }) if snap.Timestamp.After(lastSnapshotTime) { lastSnapshotTime = snap.Timestamp @@ -410,29 +458,21 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string, hours int) map[s if accountInfo, err := trader.GetAccountInfo(); err == nil { // Only append if it's been more than 30 seconds since last snapshot if now.Sub(lastSnapshotTime) > 30*time.Second { - totalEquity := 0.0 - if v, ok := accountInfo["total_equity"].(float64); ok { - totalEquity = v - } - totalPnL := 0.0 - if v, ok := accountInfo["total_pnl"].(float64); ok { - totalPnL = v - } - walletBalance := 0.0 - if v, ok := accountInfo["wallet_balance"].(float64); ok { - walletBalance = v - } + totalEquity := floatFromMap(accountInfo, "total_equity") + totalPnL := totalEquity - initialBalance + walletBalance := floatFromMap(accountInfo, "wallet_balance") pnlPct := 0.0 if initialBalance > 0 { pnlPct = (totalEquity - initialBalance) / initialBalance * 100 } history = append(history, map[string]interface{}{ - "timestamp": now, - "total_equity": totalEquity, - "total_pnl": totalPnL, - "total_pnl_pct": pnlPct, - "balance": walletBalance, + "timestamp": now, + "total_equity": totalEquity, + "available_balance": floatFromMap(accountInfo, "available_balance"), + "total_pnl": totalPnL, + "total_pnl_pct": pnlPct, + "balance": walletBalance, }) } } diff --git a/store/equity.go b/store/equity.go index db5b0b37..2d093083 100644 --- a/store/equity.go +++ b/store/equity.go @@ -14,15 +14,16 @@ type EquityStore struct { // EquitySnapshot equity snapshot type EquitySnapshot struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - TraderID string `gorm:"column:trader_id;not null;index:idx_equity_trader_time" json:"trader_id"` - Timestamp time.Time `gorm:"not null;index:idx_equity_trader_time,sort:desc;index:idx_equity_timestamp,sort:desc" json:"timestamp"` - TotalEquity float64 `gorm:"column:total_equity;not null;default:0" json:"total_equity"` - Balance float64 `gorm:"not null;default:0" json:"balance"` - UnrealizedPnL float64 `gorm:"column:unrealized_pnl;not null;default:0" json:"unrealized_pnl"` - PositionCount int `gorm:"column:position_count;default:0" json:"position_count"` - MarginUsedPct float64 `gorm:"column:margin_used_pct;default:0" json:"margin_used_pct"` - CreatedAt time.Time `json:"created_at"` + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + TraderID string `gorm:"column:trader_id;not null;index:idx_equity_trader_time" json:"trader_id"` + Timestamp time.Time `gorm:"not null;index:idx_equity_trader_time,sort:desc;index:idx_equity_timestamp,sort:desc" json:"timestamp"` + TotalEquity float64 `gorm:"column:total_equity;not null;default:0" json:"total_equity"` + Balance float64 `gorm:"not null;default:0" json:"balance"` + AvailableBalance float64 `gorm:"column:available_balance;not null;default:0" json:"available_balance"` + UnrealizedPnL float64 `gorm:"column:unrealized_pnl;not null;default:0" json:"unrealized_pnl"` + PositionCount int `gorm:"column:position_count;default:0" json:"position_count"` + MarginUsedPct float64 `gorm:"column:margin_used_pct;default:0" json:"margin_used_pct"` + CreatedAt time.Time `json:"created_at"` } func (EquitySnapshot) TableName() string { return "trader_equity_snapshots" } @@ -98,6 +99,7 @@ func (s *EquityStore) GetAllTradersLatest() (map[string]*EquitySnapshot, error) var snapshots []*EquitySnapshot err := s.db.Raw(` SELECT e.id, e.trader_id, e.timestamp, e.total_equity, e.balance, + e.available_balance, e.unrealized_pnl, e.position_count, e.margin_used_pct, e.created_at FROM trader_equity_snapshots e INNER JOIN ( @@ -159,12 +161,13 @@ func (s *EquityStore) MigrateFromDecision() (int64, error) { result := s.db.Exec(` INSERT INTO trader_equity_snapshots ( trader_id, timestamp, total_equity, balance, - unrealized_pnl, position_count, margin_used_pct + available_balance, unrealized_pnl, position_count, margin_used_pct ) SELECT dr.trader_id, dr.timestamp, das.total_balance, + das.total_balance - das.total_unrealized_profit, das.available_balance, das.total_unrealized_profit, das.position_count, diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 6c29fdd8..9a0fc8f2 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -524,9 +524,6 @@ func (at *AutoTrader) Run() error { } } - ticker := time.NewTicker(at.config.ScanInterval) - defer ticker.Stop() - // Check if this is a grid trading strategy isGridStrategy := at.IsGridStrategy() if isGridStrategy { @@ -538,6 +535,7 @@ func (at *AutoTrader) Run() error { } // Execute immediately on first run + at.logInfof("▶️ Running first trading cycle immediately; next cycle starts after %v", at.config.ScanInterval) if isGridStrategy { if err := at.RunGridCycle(); err != nil { at.logErrorf("❌ Grid execution failed: %v", err) @@ -548,6 +546,9 @@ func (at *AutoTrader) Run() error { } } + ticker := time.NewTicker(at.config.ScanInterval) + defer ticker.Stop() + for { at.isRunningMutex.RLock() running := at.isRunning diff --git a/trader/auto_trader_decision.go b/trader/auto_trader_decision.go index b2898174..c6ac3159 100644 --- a/trader/auto_trader_decision.go +++ b/trader/auto_trader_decision.go @@ -3,11 +3,11 @@ package trader import ( "fmt" "math" - "nofx/telemetry" "nofx/kernel" "nofx/logger" "nofx/market" "nofx/store" + "nofx/telemetry" "time" ) @@ -18,13 +18,14 @@ func (at *AutoTrader) saveEquitySnapshot(ctx *kernel.Context) { } snapshot := &store.EquitySnapshot{ - TraderID: at.id, - Timestamp: time.Now().UTC(), - TotalEquity: ctx.Account.TotalEquity, - Balance: ctx.Account.TotalEquity - ctx.Account.UnrealizedPnL, - UnrealizedPnL: ctx.Account.UnrealizedPnL, - PositionCount: ctx.Account.PositionCount, - MarginUsedPct: ctx.Account.MarginUsedPct, + TraderID: at.id, + Timestamp: time.Now().UTC(), + TotalEquity: ctx.Account.TotalEquity, + Balance: ctx.Account.TotalEquity - ctx.Account.UnrealizedPnL, + AvailableBalance: ctx.Account.AvailableBalance, + UnrealizedPnL: ctx.Account.UnrealizedPnL, + PositionCount: ctx.Account.PositionCount, + MarginUsedPct: ctx.Account.MarginUsedPct, } if err := at.store.Equity().Save(snapshot); err != nil { diff --git a/trader/hyperliquid/trader_account.go b/trader/hyperliquid/trader_account.go index 5104c2ef..6215516a 100644 --- a/trader/hyperliquid/trader_account.go +++ b/trader/hyperliquid/trader_account.go @@ -129,23 +129,29 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { logger.Infof(" └─ %s: size=%s, entryPx=%s, posValue=%s, pnl=%s", pos.Position.Coin, pos.Position.Szi, entryPx, pos.Position.PositionValue, pos.Position.UnrealizedPnl) } - xyzWalletBalance := xyzAccountValue - xyzUnrealizedPnl - - // Step 6: Correctly handle Spot + Perpetuals + xyz dex balance - // Important: Each account is independent, manual transfers required - totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance + xyzWalletBalance - totalUnrealizedPnlAll := totalUnrealizedPnl + xyzUnrealizedPnl - - // Calculate total equity properly: perpAccountValue + spotUSDCBalance + xyzAccountValue - // Note: totalWalletBalance + totalUnrealizedPnlAll should equal this - totalEquityCalculated := accountValue + spotUSDCBalance + xyzAccountValue + xyzMarginUsed := calculateXYZMarginUsed(xyzPositions) + balanceBreakdown := calculateHyperliquidBalanceBreakdown( + t.isUnifiedAccount, + spotUSDCBalance, + accountValue, + totalUnrealizedPnl, + totalMarginUsed, + availableBalance, + xyzAccountValue, + xyzUnrealizedPnl, + xyzMarginUsed, + ) + totalWalletBalance := balanceBreakdown.TotalWalletBalance + totalUnrealizedPnlAll := balanceBreakdown.TotalUnrealizedProfit + totalEquityCalculated := balanceBreakdown.TotalEquity + availableBalance = balanceBreakdown.AvailableBalance // Step 7: Unified Account mode - Spot USDC is used as collateral for Perps - // In this mode, available balance includes Spot USDC since it can be used for Perp margin + // In this mode, xyz/core account values are collateral views backed by the + // same Spot USDC. They must not be added on top of Spot or the dashboard + // will double count equity after a position opens. if t.isUnifiedAccount && spotUSDCBalance > 0 { - // Add Spot balance to available balance for trading - availableBalance = availableBalance + spotUSDCBalance - logger.Infof("✓ Unified Account: Spot %.2f USDC added to available balance (total: %.2f)", + logger.Infof("✓ Unified Account: Spot %.2f USDC used as shared collateral (available: %.2f)", spotUSDCBalance, availableBalance) } @@ -160,6 +166,7 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { result["xyzDexBalance"] = xyzAccountValue // xyz dex equity (stock perps, forex, commodities) result["xyzDexUnrealizedPnl"] = xyzUnrealizedPnl // xyz dex unrealized PnL result["perpAccountValue"] = accountValue // Perp account value for debugging + result["totalMarginUsed"] = balanceBreakdown.TotalMarginUsed logger.Infof("✓ Hyperliquid complete account:") logger.Infof(" • Spot balance: %.2f USDC", spotUSDCBalance) @@ -171,15 +178,93 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { logger.Infof(" • Margin used: %.2f USDC", totalMarginUsed) logger.Infof(" • xyz dex equity: %.2f USDC (wallet %.2f + unrealized %.2f)", xyzAccountValue, - xyzWalletBalance, + balanceBreakdown.XYZWalletBalance, xyzUnrealizedPnl) - logger.Infof(" • Total assets (Perp+Spot+xyz): %.2f USDC", totalWalletBalance) - logger.Infof(" ⭐ Total: %.2f USDC | Perp: %.2f | Spot: %.2f | xyz: %.2f", - totalWalletBalance, availableBalance, spotUSDCBalance, xyzAccountValue) + logger.Infof(" • Total wallet balance: %.2f USDC", totalWalletBalance) + logger.Infof(" ⭐ Total equity: %.2f USDC | Available: %.2f | Spot: %.2f | xyz view: %.2f", + totalEquityCalculated, availableBalance, spotUSDCBalance, xyzAccountValue) return result, nil } +type hyperliquidBalanceBreakdown struct { + TotalWalletBalance float64 + TotalEquity float64 + AvailableBalance float64 + TotalUnrealizedProfit float64 + TotalMarginUsed float64 + PerpWalletBalance float64 + XYZWalletBalance float64 +} + +func calculateHyperliquidBalanceBreakdown( + isUnifiedAccount bool, + spotUSDCBalance float64, + perpAccountValue float64, + perpUnrealizedPnl float64, + perpMarginUsed float64, + perpWithdrawable float64, + xyzAccountValue float64, + xyzUnrealizedPnl float64, + xyzMarginUsed float64, +) hyperliquidBalanceBreakdown { + perpWalletBalance := perpAccountValue - perpUnrealizedPnl + xyzWalletBalance := xyzAccountValue - xyzUnrealizedPnl + totalUnrealizedPnl := perpUnrealizedPnl + xyzUnrealizedPnl + totalMarginUsed := perpMarginUsed + xyzMarginUsed + + if isUnifiedAccount && spotUSDCBalance > 0 { + totalEquity := spotUSDCBalance + totalUnrealizedPnl + availableBalance := totalEquity - totalMarginUsed + if availableBalance < 0 { + availableBalance = 0 + } + return hyperliquidBalanceBreakdown{ + TotalWalletBalance: spotUSDCBalance, + TotalEquity: totalEquity, + AvailableBalance: availableBalance, + TotalUnrealizedProfit: totalUnrealizedPnl, + TotalMarginUsed: totalMarginUsed, + PerpWalletBalance: perpWalletBalance, + XYZWalletBalance: xyzWalletBalance, + } + } + + availableBalance := perpWithdrawable + if availableBalance == 0 { + availableBalance = perpAccountValue - perpMarginUsed + } + if availableBalance < 0 { + availableBalance = 0 + } + + return hyperliquidBalanceBreakdown{ + TotalWalletBalance: perpWalletBalance + spotUSDCBalance + xyzWalletBalance, + TotalEquity: perpAccountValue + spotUSDCBalance + xyzAccountValue, + AvailableBalance: availableBalance, + TotalUnrealizedProfit: totalUnrealizedPnl, + TotalMarginUsed: totalMarginUsed, + PerpWalletBalance: perpWalletBalance, + XYZWalletBalance: xyzWalletBalance, + } +} + +func calculateXYZMarginUsed(positions []xyzAssetPosition) float64 { + total := 0.0 + for _, pos := range positions { + positionValue, _ := strconv.ParseFloat(pos.Position.PositionValue, 64) + if positionValue < 0 { + positionValue = -positionValue + } + leverage := float64(pos.Position.Leverage.Value) + if leverage <= 0 { + leverage = 1 + } + total += positionValue / leverage + } + return total +} + // xyzDexState represents the clearinghouse state for xyz dex type xyzDexState struct { MarginSummary *xyzMarginSummary `json:"marginSummary,omitempty"` diff --git a/trader/hyperliquid/trader_account_test.go b/trader/hyperliquid/trader_account_test.go new file mode 100644 index 00000000..17144ad9 --- /dev/null +++ b/trader/hyperliquid/trader_account_test.go @@ -0,0 +1,48 @@ +package hyperliquid + +import "testing" + +func TestUnifiedAccountDoesNotDoubleCountXYZAccountValue(t *testing.T) { + breakdown := calculateHyperliquidBalanceBreakdown( + true, + 26.33, // Spot USDC collateral + 0, + 0, + 0, + 0, + 25.96, // xyz account value is a view of the same shared collateral + -0.32, + 25.96, + ) + + if breakdown.TotalEquity < 25.99 || breakdown.TotalEquity > 26.02 { + t.Fatalf("expected total equity to be spot + unrealized pnl, got %.4f", breakdown.TotalEquity) + } + if breakdown.TotalEquity > 40 { + t.Fatalf("unified collateral was double-counted: %.4f", breakdown.TotalEquity) + } + if breakdown.AvailableBalance > 0.1 { + t.Fatalf("expected almost no free collateral with full-size margin, got %.4f", breakdown.AvailableBalance) + } +} + +func TestSeparateAccountsStillAddIndependentBalances(t *testing.T) { + breakdown := calculateHyperliquidBalanceBreakdown( + false, + 30, + 10, + 1, + 2, + 8, + 5, + -0.5, + 1, + ) + + if breakdown.TotalEquity != 45 { + t.Fatalf("expected independent accounts to add to 45, got %.4f", breakdown.TotalEquity) + } + if breakdown.TotalWalletBalance != 44.5 { + t.Fatalf("expected wallet balance 44.5, got %.4f", breakdown.TotalWalletBalance) + } +} diff --git a/web/src/components/trader/AITradersPage.tsx b/web/src/components/trader/AITradersPage.tsx index adc87c67..39ea1c18 100644 --- a/web/src/components/trader/AITradersPage.tsx +++ b/web/src/components/trader/AITradersPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useSearchParams } from 'react-router-dom' import useSWR from 'swr' import { api } from '../../lib/api' import type { @@ -32,6 +32,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const { language } = useLanguage() const { user, token } = useAuth() const navigate = useNavigate() + const [searchParams, setSearchParams] = useSearchParams() const [showCreateModal, setShowCreateModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false) const [showModelModal, setShowModelModal] = useState(false) @@ -39,6 +40,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const [showTelegramModal, setShowTelegramModal] = useState(false) const [editingModel, setEditingModel] = useState(null) const [editingExchange, setEditingExchange] = useState(null) + const [initialModelId, setInitialModelId] = useState(null) + const [initialExchangeType, setInitialExchangeType] = useState( + null + ) const [editingTrader, setEditingTrader] = useState(null) const [allModels, setAllModels] = useState([]) const [allExchanges, setAllExchanges] = useState([]) @@ -166,10 +171,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { return true }) || [] - const isModelInUse = (modelId: string) => { - return traders?.some((tr) => tr.ai_model === modelId && tr.is_running) - } - const getModelUsageInfo = (modelId: string) => { const usingTraders = traders?.filter((tr) => tr.ai_model === modelId) || [] const runningCount = usingTraders.filter((tr) => tr.is_running).length @@ -177,10 +178,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { return { runningCount, totalCount, usingTraders } } - const isExchangeInUse = (exchangeId: string) => { - return traders?.some((tr) => tr.exchange_id === exchangeId && tr.is_running) - } - const getExchangeUsageInfo = (exchangeId: string) => { const usingTraders = traders?.filter((tr) => tr.exchange_id === exchangeId) || [] @@ -334,17 +331,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { } const handleModelClick = (modelId: string) => { - if (!isModelInUse(modelId)) { - setEditingModel(modelId) - setShowModelModal(true) - } + setInitialModelId(null) + setEditingModel(modelId) + setShowModelModal(true) } const handleExchangeClick = (exchangeId: string) => { - if (!isExchangeInUse(exchangeId)) { - setEditingExchange(exchangeId) - setShowExchangeModal(true) - } + setInitialExchangeType(null) + setEditingExchange(exchangeId) + setShowExchangeModal(true) } const handleDeleteConfig = async (config: { @@ -619,15 +614,63 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { } const handleAddModel = () => { + setInitialModelId(null) setEditingModel(null) setShowModelModal(true) } const handleAddExchange = () => { + setInitialExchangeType(null) setEditingExchange(null) setShowExchangeModal(true) } + const handleOpenClaw402Config = () => { + const configuredClaw402 = allModels?.find( + (model) => model.provider === 'claw402' + ) + const supportedClaw402 = supportedModels?.find( + (model) => model.provider === 'claw402' + ) + const modelId = configuredClaw402?.id || supportedClaw402?.id || 'claw402' + + setEditingModel(configuredClaw402?.id || null) + setInitialModelId(modelId) + setShowModelModal(true) + } + + const handleOpenHyperliquidConfig = () => { + const existingHyperliquid = allExchanges?.find( + (exchange) => + exchange.exchange_type === 'hyperliquid' || + exchange.id === 'hyperliquid' + ) + + setEditingExchange(existingHyperliquid?.id || null) + setInitialExchangeType(existingHyperliquid ? null : 'hyperliquid') + setShowExchangeModal(true) + } + + useEffect(() => { + if (!user || !token) return + + const setupTarget = searchParams.get('setup') + if (!setupTarget) return + + if (setupTarget === 'claw402') { + if (supportedModels.length === 0 && allModels.length === 0) return + handleOpenClaw402Config() + } else if (setupTarget === 'hyperliquid') { + handleOpenHyperliquidConfig() + } else { + return + } + + const nextParams = new URLSearchParams(searchParams) + nextParams.delete('setup') + setSearchParams(nextParams, { replace: true }) + }, [allExchanges, allModels, searchParams, setSearchParams, supportedModels, token, user]) + const refreshLaunchState = async () => { await Promise.all([loadConfigs(), mutateTraders()]) } @@ -715,9 +758,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { visibleExchangeAddresses={visibleExchangeAddresses} copiedId={copiedId} language={language} - isModelInUse={isModelInUse} getModelUsageInfo={getModelUsageInfo} - isExchangeInUse={isExchangeInUse} getExchangeUsageInfo={getExchangeUsageInfo} onModelClick={handleModelClick} onExchangeClick={handleExchangeClick} @@ -733,6 +774,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { isLoggedIn={Boolean(user && token)} language={language} onRefresh={refreshLaunchState} + onOpenClaw402Config={handleOpenClaw402Config} + onOpenHyperliquidConfig={handleOpenHyperliquidConfig} /> {/* Traders List */} @@ -789,11 +832,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { allModels={supportedModels} configuredModels={allModels} editingModelId={editingModel} + initialModelId={initialModelId} onSave={handleSaveModelConfig} onDelete={handleDeleteModelConfig} onClose={() => { setShowModelModal(false) setEditingModel(null) + setInitialModelId(null) }} language={language} /> @@ -804,11 +849,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { { setShowExchangeModal(false) setEditingExchange(null) + setInitialExchangeType(null) }} language={language} /> diff --git a/web/src/components/trader/AutopilotLaunchPanel.tsx b/web/src/components/trader/AutopilotLaunchPanel.tsx index 6b67208c..6bcabe48 100644 --- a/web/src/components/trader/AutopilotLaunchPanel.tsx +++ b/web/src/components/trader/AutopilotLaunchPanel.tsx @@ -37,6 +37,8 @@ interface AutopilotLaunchPanelProps { isLoggedIn: boolean language: string onRefresh: () => Promise + onOpenClaw402Config?: () => void + onOpenHyperliquidConfig?: () => void } const MIN_AI_FEE_USDC = 1 @@ -186,6 +188,8 @@ export function AutopilotLaunchPanel({ isLoggedIn, language, onRefresh, + onOpenClaw402Config, + onOpenHyperliquidConfig, }: AutopilotLaunchPanelProps) { const navigate = useNavigate() const [wallet, setWallet] = useState( @@ -371,15 +375,34 @@ export function AutopilotLaunchPanel({ ? `${shortAddress(feeWalletAddress)} · ${formatUSDC(feeWalletBalance)} USDC` : 'Base USDC wallet required', action: feeWalletAddress ? ( +
+ + +
+ ) : ( - ) : undefined, + ), }, { title: 'Hyperliquid trading wallet', @@ -389,6 +412,16 @@ export function AutopilotLaunchPanel({ meta: hyperliquidExchange?.hyperliquidWalletAddr ? `${shortAddress(hyperliquidExchange.hyperliquidWalletAddr)} · authorized` : 'Agent and trading authorization required', + action: ( + + ), }, { title: 'Trading balance', @@ -421,10 +454,16 @@ export function AutopilotLaunchPanel({ return ( ) @@ -435,9 +474,13 @@ export function AutopilotLaunchPanel({ @@ -692,9 +749,21 @@ function Claw402ConfigForm({ {/* New wallet backup warning */} {showNewWalletBackup && newWalletKey && ( -
-
- 🚨 {language === 'zh' ? '重要:请立即备份私钥!' : 'Important: Backup your private key NOW!'} +
+
+ 🚨{' '} + {language === 'zh' + ? '重要:请立即备份私钥!' + : 'Important: Backup your private key NOW!'}
{language === 'zh' @@ -702,7 +771,10 @@ function Claw402ConfigForm({ : 'This is your wallet private key. If lost, it cannot be recovered and all assets will be permanently lost. Copy and save it securely.'}
- + {newWalletKey}
-
-
✅ {language === 'zh' ? '建议保存到密码管理器(1Password / Bitwarden)' : 'Save to a password manager (1Password / Bitwarden)'}
-
✅ {language === 'zh' ? '或抄在纸上放安全的地方' : 'Or write it down and store it safely'}
-
❌ {language === 'zh' ? '不要截图发给别人' : 'Do NOT screenshot or share with anyone'}
+
+
+ ✅{' '} + {language === 'zh' + ? '建议保存到密码管理器(1Password / Bitwarden)' + : 'Save to a password manager (1Password / Bitwarden)'} +
+
+ ✅{' '} + {language === 'zh' + ? '或抄在纸上放安全的地方' + : 'Or write it down and store it safely'} +
+
+ ❌{' '} + {language === 'zh' + ? '不要截图发给别人' + : 'Do NOT screenshot or share with anyone'} +
)} @@ -736,7 +831,7 @@ function Claw402ConfigForm({
{/* Wallet Validation Results */} - {apiKey && ( + {(apiKey || walletAddress) && (
{/* Validating spinner */} {validating && ( @@ -792,15 +887,28 @@ function Claw402ConfigForm({ {copiedAddr ? '✅' : '📋'}
- {walletAddress} -
- ⚠️ {language === 'zh' ? '请确认这是你的钱包地址(可在 MetaMask 中核对)' : 'Please confirm this is your wallet address (verify in MetaMask)'} + + {walletAddress} + +
+ ⚠️{' '} + {language === 'zh' + ? '请确认这是你的钱包地址(可在 MetaMask 中核对)' + : 'Please confirm this is your wallet address (verify in MetaMask)'}
{usdcBalance !== null && (
💰 - 0 ? '#00E096' : '#F59E0B' }}> + 0 ? '#00E096' : '#F59E0B' }} + > {t('modelConfig.usdcBalance', language)}: ${usdcBalance} @@ -1119,9 +1240,14 @@ function StandardProviderConfigForm({ {editingModelId && selectedModel && 'has_api_key' in selectedModel && (
- 当前模型密钥状态:{selectedModel.has_api_key ? '已配置 API Key' : '未配置 API Key'} + 当前模型密钥状态: + {selectedModel.has_api_key ? '已配置 API Key' : '未配置 API Key'}
)} @@ -1156,10 +1282,10 @@ function StandardProviderConfigForm({ editingModelId && selectedModel.has_api_key ? '已保存,如需更换请重新输入' : selectedModel.provider === 'blockrun-base' - ? '0x... (EVM private key)' - : selectedModel.provider === 'blockrun-sol' - ? 'bs58 encoded key (Solana)' - : t('enterAPIKey', language) + ? '0x... (EVM private key)' + : selectedModel.provider === 'blockrun-sol' + ? 'bs58 encoded key (Solana)' + : t('enterAPIKey', language) } className="w-full px-4 py-3 rounded-xl" style={{ @@ -1174,9 +1300,23 @@ function StandardProviderConfigForm({ {/* Custom Base URL (hidden for BlockRun) */} {!selectedModel.provider?.startsWith('blockrun') && (
-