Skip to content

Commit 02cb78a

Browse files
fix: improve frontend UX and fix OKX close position
Frontend improvements: - Replace window.location.reload() with SWR mutate() for data refresh - Replace native alert/confirm with toast notifications (confirmToast, notify) - Add loading skeletons to AITradersPage and EquityChart - Fix flash of empty state during initial load OKX fixes: - Fix proxy issue in Docker by using explicit no-proxy function - Fix CloseShort sz parameter error - ensure quantity is always positive - Fix GetPositions to return absolute value for positionAmt
1 parent 084554a commit 02cb78a

File tree

8 files changed

+132
-30
lines changed

8 files changed

+132
-30
lines changed

img_1.png

-305 KB
Binary file not shown.

trader/okx_trader.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"io"
1313
"net/http"
14+
neturl "net/url"
1415
"nofx/logger"
1516
"strconv"
1617
"strings"
@@ -95,11 +96,17 @@ func genOkxClOrdID() string {
9596
return orderID
9697
}
9798

99+
// noProxyFunc 返回一个始终返回 nil 的代理函数,用于禁用代理
100+
func noProxyFunc(req *http.Request) (*neturl.URL, error) {
101+
return nil, nil
102+
}
103+
98104
// NewOKXTrader 创建OKX交易器
99105
func NewOKXTrader(apiKey, secretKey, passphrase string) *OKXTrader {
100-
// 创建禁用代理的 HTTP 客户端
106+
// 创建完全禁用代理的 HTTP 客户端
107+
// 这对于 Docker 容器环境很重要,因为容器可能继承宿主机的代理环境变量
101108
transport := &http.Transport{
102-
Proxy: nil, // 显式禁用代理
109+
Proxy: noProxyFunc,
103110
}
104111
httpClient := &http.Client{
105112
Timeout: 30 * time.Second,
@@ -341,11 +348,14 @@ func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
341348
// 转换symbol格式
342349
symbol := t.convertSymbolBack(pos.InstId)
343350

344-
// 确定方向
351+
// 确定方向,并确保 posAmt 是正数
345352
side := "long"
346-
if pos.PosSide == "short" || posAmt < 0 {
353+
if pos.PosSide == "short" {
347354
side = "short"
348-
posAmt = -posAmt // 取绝对值
355+
}
356+
// OKX 空仓的 pos 是负数,需要取绝对值
357+
if posAmt < 0 {
358+
posAmt = -posAmt
349359
}
350360

351361
posMap := map[string]interface{}{
@@ -726,9 +736,13 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
726736
if err != nil {
727737
return nil, err
728738
}
739+
logger.Infof("🔍 OKX CloseShort 查找持仓: symbol=%s, 当前持仓数=%d", symbol, len(positions))
729740
for _, pos := range positions {
741+
logger.Infof("🔍 OKX 持仓: symbol=%v, side=%v, positionAmt=%v",
742+
pos["symbol"], pos["side"], pos["positionAmt"])
730743
if pos["symbol"] == symbol && pos["side"] == "short" {
731-
quantity = pos["positionAmt"].(float64) // 这已经是张数
744+
quantity = pos["positionAmt"].(float64)
745+
logger.Infof("🔍 OKX 找到空仓: quantity=%f", quantity)
732746
break
733747
}
734748
}
@@ -737,12 +751,20 @@ func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]inte
737751
}
738752
}
739753

754+
// 确保 quantity 是正数(OKX sz 参数必须为正)
755+
if quantity < 0 {
756+
quantity = -quantity
757+
}
758+
740759
// 获取合约信息用于格式化张数
741760
inst, err := t.getInstrument(symbol)
742761
if err != nil {
743762
return nil, fmt.Errorf("获取合约信息失败: %w", err)
744763
}
745764

765+
logger.Infof("🔍 OKX 合约信息: instId=%s, lotSz=%f, minSz=%f, ctVal=%f",
766+
inst.InstID, inst.LotSz, inst.MinSz, inst.CtVal)
767+
746768
// quantity 已经是张数,直接格式化
747769
szStr := t.formatSize(quantity, inst)
748770

web/src/App.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState } from 'react'
2-
import useSWR from 'swr'
2+
import useSWR, { mutate } from 'swr'
33
import { api } from './lib/api'
44
import { ChartTabs } from './components/ChartTabs'
55
import { AITradersPage } from './components/AITradersPage'
@@ -15,6 +15,7 @@ import { LanguageProvider, useLanguage } from './contexts/LanguageContext'
1515
import { AuthProvider, useAuth } from './contexts/AuthContext'
1616
import { ConfirmDialogProvider } from './components/ConfirmDialog'
1717
import { t, type Language } from './i18n/translations'
18+
import { confirmToast, notify } from './lib/notify'
1819
import { useSystemConfig } from './hooks/useSystemConfig'
1920
import { DecisionCard } from './components/DecisionCard'
2021
import { BacktestPage } from './components/BacktestPage'
@@ -535,17 +536,26 @@ function TraderDetailsPage({
535536
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
536537
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
537538

538-
if (!confirm(confirmMsg)) return
539+
const confirmed = await confirmToast(confirmMsg, {
540+
title: language === 'zh' ? '确认平仓' : 'Confirm Close',
541+
okText: language === 'zh' ? '确认' : 'Confirm',
542+
cancelText: language === 'zh' ? '取消' : 'Cancel',
543+
})
544+
545+
if (!confirmed) return
539546

540547
setClosingPosition(symbol)
541548
try {
542549
await api.closePosition(selectedTraderId, symbol, side)
543-
const successMsg = language === 'zh' ? '平仓成功' : 'Position closed successfully'
544-
alert(successMsg)
545-
window.location.reload()
550+
notify.success(language === 'zh' ? '平仓成功' : 'Position closed successfully')
551+
// 使用 SWR mutate 刷新数据而非重新加载页面
552+
await Promise.all([
553+
mutate(`positions-${selectedTraderId}`),
554+
mutate(`account-${selectedTraderId}`),
555+
])
546556
} catch (err: unknown) {
547557
const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position')
548-
alert(errorMsg)
558+
notify.error(errorMsg)
549559
} finally {
550560
setClosingPosition(null)
551561
}

web/src/components/AITradersPage.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
8787
oiTopUrl: '',
8888
})
8989

90-
const { data: traders, mutate: mutateTraders } = useSWR<TraderInfo[]>(
90+
const { data: traders, mutate: mutateTraders, isLoading: isTradersLoading } = useSWR<TraderInfo[]>(
9191
user && token ? 'traders' : null,
9292
api.getTraders,
9393
{ refreshInterval: 5000 }
@@ -1047,7 +1047,31 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
10471047
</h2>
10481048
</div>
10491049

1050-
{traders && traders.length > 0 ? (
1050+
{isTradersLoading ? (
1051+
/* Loading Skeleton */
1052+
<div className="space-y-3 md:space-y-4">
1053+
{[1, 2, 3].map((i) => (
1054+
<div
1055+
key={i}
1056+
className="flex flex-col md:flex-row md:items-center justify-between p-3 md:p-4 rounded gap-3 md:gap-4 animate-pulse"
1057+
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
1058+
>
1059+
<div className="flex items-center gap-3 md:gap-4">
1060+
<div className="w-10 h-10 md:w-12 md:h-12 rounded-full skeleton"></div>
1061+
<div className="min-w-0 space-y-2">
1062+
<div className="skeleton h-5 w-32"></div>
1063+
<div className="skeleton h-3 w-24"></div>
1064+
</div>
1065+
</div>
1066+
<div className="flex items-center gap-3 md:gap-4">
1067+
<div className="skeleton h-6 w-16"></div>
1068+
<div className="skeleton h-6 w-16"></div>
1069+
<div className="skeleton h-8 w-20"></div>
1070+
</div>
1071+
</div>
1072+
))}
1073+
</div>
1074+
) : traders && traders.length > 0 ? (
10511075
<div className="space-y-3 md:space-y-4">
10521076
{traders.map((trader) => (
10531077
<div

web/src/components/BacktestPage.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { api } from '../lib/api'
1313
import { useLanguage } from '../contexts/LanguageContext'
1414
import { t } from '../i18n/translations'
15+
import { confirmToast } from '../lib/notify'
1516
import { DecisionCard } from './DecisionCard'
1617
import type {
1718
BacktestStatusPayload,
@@ -308,12 +309,17 @@ export function BacktestPage() {
308309

309310
const handleDeleteRun = async () => {
310311
if (!selectedRunId) return
311-
if (
312-
typeof window !== 'undefined' &&
313-
!window.confirm(tr('toasts.confirmDelete', { id: selectedRunId }))
314-
) {
315-
return
316-
}
312+
313+
const confirmed = await confirmToast(
314+
tr('toasts.confirmDelete', { id: selectedRunId }),
315+
{
316+
title: language === 'zh' ? '确认删除' : 'Confirm Delete',
317+
okText: language === 'zh' ? '删除' : 'Delete',
318+
cancelText: language === 'zh' ? '取消' : 'Cancel',
319+
}
320+
)
321+
if (!confirmed) return
322+
317323
try {
318324
await api.deleteBacktestRun(selectedRunId)
319325
setToast({ text: tr('toasts.deleteSuccess'), tone: 'success' })

web/src/components/EquityChart.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
4141
const { user, token } = useAuth()
4242
const [displayMode, setDisplayMode] = useState<'dollar' | 'percent'>('dollar')
4343

44-
const { data: history, error } = useSWR<EquityPoint[]>(
44+
const { data: history, error, isLoading } = useSWR<EquityPoint[]>(
4545
user && token && traderId ? `equity-history-${traderId}` : null,
4646
() => api.getEquityHistory(traderId),
4747
{
@@ -61,6 +61,22 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
6161
}
6262
)
6363

64+
// Loading state - show skeleton
65+
if (isLoading) {
66+
return (
67+
<div className={embedded ? 'p-6' : 'binance-card p-6'}>
68+
{!embedded && (
69+
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>
70+
{t('accountEquityCurve', language)}
71+
</h3>
72+
)}
73+
<div className="animate-pulse">
74+
<div className="skeleton h-64 w-full rounded"></div>
75+
</div>
76+
</div>
77+
)
78+
}
79+
6480
if (error) {
6581
return (
6682
<div className={embedded ? 'p-6' : 'binance-card p-6'}>

web/src/pages/StrategyStudioPage.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
Send,
2929
} from 'lucide-react'
3030
import type { Strategy, StrategyConfig, AIModel } from '../types'
31+
import { confirmToast, notify } from '../lib/notify'
3132
import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor'
3233
import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
3334
import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
@@ -175,16 +176,30 @@ export function StrategyStudioPage() {
175176

176177
// Delete strategy
177178
const handleDeleteStrategy = async (id: string) => {
178-
if (!token || !confirm(language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?')) return
179+
if (!token) return
180+
181+
const confirmed = await confirmToast(
182+
language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?',
183+
{
184+
title: language === 'zh' ? '确认删除' : 'Confirm Delete',
185+
okText: language === 'zh' ? '删除' : 'Delete',
186+
cancelText: language === 'zh' ? '取消' : 'Cancel',
187+
}
188+
)
189+
if (!confirmed) return
190+
179191
try {
180192
const response = await fetch(`${API_BASE}/api/strategies/${id}`, {
181193
method: 'DELETE',
182194
headers: { Authorization: `Bearer ${token}` },
183195
})
184196
if (!response.ok) throw new Error('Failed to delete strategy')
197+
notify.success(language === 'zh' ? '策略已删除' : 'Strategy deleted')
185198
await fetchStrategies()
186199
} catch (err) {
187-
setError(err instanceof Error ? err.message : 'Unknown error')
200+
const errorMsg = err instanceof Error ? err.message : 'Unknown error'
201+
setError(errorMsg)
202+
notify.error(errorMsg)
188203
}
189204
}
190205

web/src/pages/TraderDashboard.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState } from 'react'
22
import { useNavigate, useSearchParams } from 'react-router-dom'
3-
import useSWR from 'swr'
3+
import useSWR, { mutate } from 'swr'
44
import { api } from '../lib/api'
55
import { ChartTabs } from '../components/ChartTabs'
66
import { useLanguage } from '../contexts/LanguageContext'
@@ -22,6 +22,7 @@ import {
2222
Loader2,
2323
} from 'lucide-react'
2424
import { stripLeadingIcons } from '../lib/text'
25+
import { confirmToast, notify } from '../lib/notify'
2526
import type {
2627
SystemStatus,
2728
AccountInfo,
@@ -88,18 +89,26 @@ export default function TraderDashboard() {
8889
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
8990
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
9091

91-
if (!confirm(confirmMsg)) return
92+
const confirmed = await confirmToast(confirmMsg, {
93+
title: language === 'zh' ? '确认平仓' : 'Confirm Close',
94+
okText: language === 'zh' ? '确认' : 'Confirm',
95+
cancelText: language === 'zh' ? '取消' : 'Cancel',
96+
})
97+
98+
if (!confirmed) return
9299

93100
setClosingPosition(symbol)
94101
try {
95102
await api.closePosition(selectedTraderId, symbol, side)
96-
const successMsg = language === 'zh' ? '平仓成功' : 'Position closed successfully'
97-
alert(successMsg)
98-
// 刷新持仓数据
99-
window.location.reload()
103+
notify.success(language === 'zh' ? '平仓成功' : 'Position closed successfully')
104+
// 使用 SWR mutate 刷新数据而非重新加载页面
105+
await Promise.all([
106+
mutate(`positions-${selectedTraderId}`),
107+
mutate(`account-${selectedTraderId}`),
108+
])
100109
} catch (err: unknown) {
101110
const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position')
102-
alert(errorMsg)
111+
notify.error(errorMsg)
103112
} finally {
104113
setClosingPosition(null)
105114
}

0 commit comments

Comments
 (0)