diff --git a/img_1.png b/img_1.png deleted file mode 100644 index 14f3112a..00000000 Binary files a/img_1.png and /dev/null differ diff --git a/trader/okx_trader.go b/trader/okx_trader.go index 0ab9028b..51af9d50 100644 --- a/trader/okx_trader.go +++ b/trader/okx_trader.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + neturl "net/url" "nofx/logger" "strconv" "strings" @@ -95,11 +96,17 @@ func genOkxClOrdID() string { return orderID } +// noProxyFunc 返回一个始终返回 nil 的代理函数,用于禁用代理 +func noProxyFunc(req *http.Request) (*neturl.URL, error) { + return nil, nil +} + // NewOKXTrader 创建OKX交易器 func NewOKXTrader(apiKey, secretKey, passphrase string) *OKXTrader { - // 创建禁用代理的 HTTP 客户端 + // 创建完全禁用代理的 HTTP 客户端 + // 这对于 Docker 容器环境很重要,因为容器可能继承宿主机的代理环境变量 transport := &http.Transport{ - Proxy: nil, // 显式禁用代理 + Proxy: noProxyFunc, } httpClient := &http.Client{ Timeout: 30 * time.Second, @@ -341,11 +348,14 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) { // 转换symbol格式 symbol := t.convertSymbolBack(pos.InstId) - // 确定方向 + // 确定方向,并确保 posAmt 是正数 side := "long" - if pos.PosSide == "short" || posAmt < 0 { + if pos.PosSide == "short" { side = "short" - posAmt = -posAmt // 取绝对值 + } + // OKX 空仓的 pos 是负数,需要取绝对值 + if posAmt < 0 { + posAmt = -posAmt } posMap := map[string]interface{}{ @@ -726,9 +736,13 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte if err != nil { return nil, err } + logger.Infof("🔍 OKX CloseShort 查找持仓: symbol=%s, 当前持仓数=%d", symbol, len(positions)) for _, pos := range positions { + logger.Infof("🔍 OKX 持仓: symbol=%v, side=%v, positionAmt=%v", + pos["symbol"], pos["side"], pos["positionAmt"]) if pos["symbol"] == symbol && pos["side"] == "short" { - quantity = pos["positionAmt"].(float64) // 这已经是张数 + quantity = pos["positionAmt"].(float64) + logger.Infof("🔍 OKX 找到空仓: quantity=%f", quantity) break } } @@ -737,12 +751,20 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte } } + // 确保 quantity 是正数(OKX sz 参数必须为正) + if quantity < 0 { + quantity = -quantity + } + // 获取合约信息用于格式化张数 inst, err := t.getInstrument(symbol) if err != nil { return nil, fmt.Errorf("获取合约信息失败: %w", err) } + logger.Infof("🔍 OKX 合约信息: instId=%s, lotSz=%f, minSz=%f, ctVal=%f", + inst.InstID, inst.LotSz, inst.MinSz, inst.CtVal) + // quantity 已经是张数,直接格式化 szStr := t.formatSize(quantity, inst) diff --git a/web/src/App.tsx b/web/src/App.tsx index aee15153..822f8fc4 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import useSWR from 'swr' +import useSWR, { mutate } from 'swr' import { api } from './lib/api' import { ChartTabs } from './components/ChartTabs' import { AITradersPage } from './components/AITradersPage' @@ -15,6 +15,7 @@ import { LanguageProvider, useLanguage } from './contexts/LanguageContext' import { AuthProvider, useAuth } from './contexts/AuthContext' import { ConfirmDialogProvider } from './components/ConfirmDialog' import { t, type Language } from './i18n/translations' +import { confirmToast, notify } from './lib/notify' import { useSystemConfig } from './hooks/useSystemConfig' import { DecisionCard } from './components/DecisionCard' import { BacktestPage } from './components/BacktestPage' @@ -535,17 +536,26 @@ function TraderDetailsPage({ ? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?` : `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?` - if (!confirm(confirmMsg)) return + const confirmed = await confirmToast(confirmMsg, { + title: language === 'zh' ? '确认平仓' : 'Confirm Close', + okText: language === 'zh' ? '确认' : 'Confirm', + cancelText: language === 'zh' ? '取消' : 'Cancel', + }) + + if (!confirmed) return setClosingPosition(symbol) try { await api.closePosition(selectedTraderId, symbol, side) - const successMsg = language === 'zh' ? '平仓成功' : 'Position closed successfully' - alert(successMsg) - window.location.reload() + notify.success(language === 'zh' ? '平仓成功' : 'Position closed successfully') + // 使用 SWR mutate 刷新数据而非重新加载页面 + await Promise.all([ + mutate(`positions-${selectedTraderId}`), + mutate(`account-${selectedTraderId}`), + ]) } catch (err: unknown) { const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position') - alert(errorMsg) + notify.error(errorMsg) } finally { setClosingPosition(null) } diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index cf1c9ab5..536be28e 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -87,7 +87,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { oiTopUrl: '', }) - const { data: traders, mutate: mutateTraders } = useSWR( + const { data: traders, mutate: mutateTraders, isLoading: isTradersLoading } = useSWR( user && token ? 'traders' : null, api.getTraders, { refreshInterval: 5000 } @@ -1047,7 +1047,31 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { - {traders && traders.length > 0 ? ( + {isTradersLoading ? ( + /* Loading Skeleton */ +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : traders && traders.length > 0 ? (
{traders.map((trader) => (
{ if (!selectedRunId) return - if ( - typeof window !== 'undefined' && - !window.confirm(tr('toasts.confirmDelete', { id: selectedRunId })) - ) { - return - } + + const confirmed = await confirmToast( + tr('toasts.confirmDelete', { id: selectedRunId }), + { + title: language === 'zh' ? '确认删除' : 'Confirm Delete', + okText: language === 'zh' ? '删除' : 'Delete', + cancelText: language === 'zh' ? '取消' : 'Cancel', + } + ) + if (!confirmed) return + try { await api.deleteBacktestRun(selectedRunId) setToast({ text: tr('toasts.deleteSuccess'), tone: 'success' }) diff --git a/web/src/components/EquityChart.tsx b/web/src/components/EquityChart.tsx index 13de9921..8cf97916 100644 --- a/web/src/components/EquityChart.tsx +++ b/web/src/components/EquityChart.tsx @@ -41,7 +41,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) { const { user, token } = useAuth() const [displayMode, setDisplayMode] = useState<'dollar' | 'percent'>('dollar') - const { data: history, error } = useSWR( + const { data: history, error, isLoading } = useSWR( user && token && traderId ? `equity-history-${traderId}` : null, () => api.getEquityHistory(traderId), { @@ -61,6 +61,22 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) { } ) + // Loading state - show skeleton + if (isLoading) { + return ( +
+ {!embedded && ( +

+ {t('accountEquityCurve', language)} +

+ )} +
+
+
+
+ ) + } + if (error) { return (
diff --git a/web/src/pages/StrategyStudioPage.tsx b/web/src/pages/StrategyStudioPage.tsx index cd6b34b2..a333d11e 100644 --- a/web/src/pages/StrategyStudioPage.tsx +++ b/web/src/pages/StrategyStudioPage.tsx @@ -28,6 +28,7 @@ import { Send, } from 'lucide-react' import type { Strategy, StrategyConfig, AIModel } from '../types' +import { confirmToast, notify } from '../lib/notify' import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor' import { IndicatorEditor } from '../components/strategy/IndicatorEditor' import { RiskControlEditor } from '../components/strategy/RiskControlEditor' @@ -175,16 +176,30 @@ export function StrategyStudioPage() { // Delete strategy const handleDeleteStrategy = async (id: string) => { - if (!token || !confirm(language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?')) return + if (!token) return + + const confirmed = await confirmToast( + language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?', + { + title: language === 'zh' ? '确认删除' : 'Confirm Delete', + okText: language === 'zh' ? '删除' : 'Delete', + cancelText: language === 'zh' ? '取消' : 'Cancel', + } + ) + if (!confirmed) return + try { const response = await fetch(`${API_BASE}/api/strategies/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` }, }) if (!response.ok) throw new Error('Failed to delete strategy') + notify.success(language === 'zh' ? '策略已删除' : 'Strategy deleted') await fetchStrategies() } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') + const errorMsg = err instanceof Error ? err.message : 'Unknown error' + setError(errorMsg) + notify.error(errorMsg) } } diff --git a/web/src/pages/TraderDashboard.tsx b/web/src/pages/TraderDashboard.tsx index 131574a9..1239d144 100644 --- a/web/src/pages/TraderDashboard.tsx +++ b/web/src/pages/TraderDashboard.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' -import useSWR from 'swr' +import useSWR, { mutate } from 'swr' import { api } from '../lib/api' import { ChartTabs } from '../components/ChartTabs' import { useLanguage } from '../contexts/LanguageContext' @@ -22,6 +22,7 @@ import { Loader2, } from 'lucide-react' import { stripLeadingIcons } from '../lib/text' +import { confirmToast, notify } from '../lib/notify' import type { SystemStatus, AccountInfo, @@ -88,18 +89,26 @@ export default function TraderDashboard() { ? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?` : `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?` - if (!confirm(confirmMsg)) return + const confirmed = await confirmToast(confirmMsg, { + title: language === 'zh' ? '确认平仓' : 'Confirm Close', + okText: language === 'zh' ? '确认' : 'Confirm', + cancelText: language === 'zh' ? '取消' : 'Cancel', + }) + + if (!confirmed) return setClosingPosition(symbol) try { await api.closePosition(selectedTraderId, symbol, side) - const successMsg = language === 'zh' ? '平仓成功' : 'Position closed successfully' - alert(successMsg) - // 刷新持仓数据 - window.location.reload() + notify.success(language === 'zh' ? '平仓成功' : 'Position closed successfully') + // 使用 SWR mutate 刷新数据而非重新加载页面 + await Promise.all([ + mutate(`positions-${selectedTraderId}`), + mutate(`account-${selectedTraderId}`), + ]) } catch (err: unknown) { const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position') - alert(errorMsg) + notify.error(errorMsg) } finally { setClosingPosition(null) }