fix: update token limits and error handling in Trader Dashboard

This commit is contained in:
Dean
2026-03-28 00:04:14 +08:00
committed by shinchan-zhai
parent fbca4166a1
commit 2e2598e4e0
5 changed files with 64 additions and 23 deletions

View File

@@ -82,12 +82,12 @@ maxSafeCoins = floor((budget - staticTokens) / perCoinTokens)
### 各模型下的最大安全币数 ### 各模型下的最大安全币数
| 模型上限 | 最小配置 | 默认配置 | 最大配置 | | 模型上限 | 最小配置 | 默认配置 | 最大配置 |
| ------------------------------ | ----------- | --------------- | ----------- | | ------------------------------ | ------------ | ------------ | ----------- |
| 131KDeepSeek / Grok / Qwen | ≥50封顶 | **58** | **14** | | 131KDeepSeek / Grok / Qwen | ≥10封顶 | ≥10封顶 | **14** |
| 128KOpenAI GPT-4 | ≥50封顶 | **57** | **14** | | 128KOpenAI GPT-4 | ≥10封顶 | ≥10封顶 | **14** |
| 200KClaude | ≥50封顶 | **89 → 封顶50** | **22** | | 200KClaude | ≥10封顶 | ≥10封顶 | ≥10封顶 |
| 1MGemini / Minimax | ≥50封顶 | ≥50封顶 | ≥50封顶 | | 1MGemini / Minimax | ≥10封顶 | ≥10封顶 | ≥10封顶 |
--- ---
@@ -114,7 +114,7 @@ maxSafeCoins = floor((budget - staticTokens) / perCoinTokens)
```go ```go
const ( const (
MaxCandidateCoins = 50 // UI 硬限制:用户最多设定的候选币数量 MaxCandidateCoins = 10 // UI 硬限制:用户最多设定的候选币数量
MaxPositions = 3 // 最大同时持仓数 MaxPositions = 3 // 最大同时持仓数
MaxTimeframes = 4 // 最大时间框架数 MaxTimeframes = 4 // 最大时间框架数
MinKlineCount = 10 // 最少 K 线数 MinKlineCount = 10 // 最少 K 线数
@@ -122,11 +122,11 @@ const (
) )
``` ```
### 为什么 MaxCandidateCoins = 50 ### 为什么 MaxCandidateCoins = 10
- **默认配置**下 50 枚币约用 **~8,000 tokens**~6% of 131K完全安全 - **默认配置**下 10 枚币约用 **~15,000 tokens**~12% of 131K完全安全
- **极端配置**4TF + 全指标)50 枚币会超过 131K 限制,但 **runtime token-blocking** 会在分析前拦截并报错 - **极端配置**4TF + 全指标)10 枚币约用 **~60,000 tokens**~46% of 131K仍有充足余量
- 因此 50 是合理的 UI 上限:一方面给用户足够灵活性,另一方面依赖运行时保护防止真正的溢出 - 因此 10 是保守且安全的 UI 上限:在所有模型和配置组合下均不会触发 token 限制
### 建议使用范围 ### 建议使用范围

View File

@@ -602,16 +602,28 @@ type ModelLimit struct {
Level string `json:"level"` // "ok" | "warning" | "danger" Level string `json:"level"` // "ok" | "warning" | "danger"
} }
// Context window sizes (tokens) for each model family
const (
contextLimitDeepSeek = 131_072 // 128K
contextLimitOpenAI = 128_000 // 128K
contextLimitClaude = 200_000 // 200K
contextLimitQwen = 131_072 // 128K
contextLimitGemini = 1_000_000 // 1M
contextLimitGrok = 131_072 // 128K
contextLimitKimi = 131_072 // 128K
contextLimitMinimax = 1_000_000 // 1M
)
// ModelContextLimits maps provider names to their context window sizes (in tokens) // ModelContextLimits maps provider names to their context window sizes (in tokens)
var ModelContextLimits = map[string]int{ var ModelContextLimits = map[string]int{
"deepseek": 131072, "deepseek": contextLimitDeepSeek,
"openai": 128000, "openai": contextLimitOpenAI,
"claude": 200000, "claude": contextLimitClaude,
"qwen": 131072, "qwen": contextLimitQwen,
"gemini": 1000000, "gemini": contextLimitGemini,
"grok": 131072, "grok": contextLimitGrok,
"kimi": 131072, "kimi": contextLimitKimi,
"minimax": 1000000, "minimax": contextLimitMinimax,
} }
// GetContextLimit returns the context limit for a given provider // GetContextLimit returns the context limit for a given provider
@@ -619,7 +631,7 @@ func GetContextLimit(provider string) int {
if limit, ok := ModelContextLimits[provider]; ok { if limit, ok := ModelContextLimits[provider]; ok {
return limit return limit
} }
return 131072 // safe default return contextLimitDeepSeek // safe default
} }
// GetContextLimitForClient returns context limit for a provider+model pair. // GetContextLimitForClient returns context limit for a provider+model pair.
@@ -639,6 +651,10 @@ func GetContextLimitForClient(provider, model string) int {
return ModelContextLimits["kimi"] return ModelContextLimits["kimi"]
case strings.HasPrefix(model, "qwen"): case strings.HasPrefix(model, "qwen"):
return ModelContextLimits["qwen"] return ModelContextLimits["qwen"]
case strings.HasPrefix(model, "minimax"):
return ModelContextLimits["minimax"]
case strings.HasPrefix(model, "deepseek"):
return ModelContextLimits["deepseek"]
default: default:
return ModelContextLimits["deepseek"] return ModelContextLimits["deepseek"]
} }

View File

@@ -323,8 +323,8 @@ function App() {
const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId) const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId)
const effectiveAccount = account const effectiveAccount = account
const effectivePositions = (positionsPollOff && !positions) ? [] as Position[] : positions const effectivePositions = positions
const effectiveDecisions = (decisionsPollOff && !decisions) ? [] as DecisionRecord[] : decisions const effectiveDecisions = decisions
// Handle routing // Handle routing
useEffect(() => { useEffect(() => {
@@ -544,7 +544,9 @@ function App() {
account={effectiveAccount} account={effectiveAccount}
accountFailed={accountPollOff} accountFailed={accountPollOff}
positions={effectivePositions} positions={effectivePositions}
positionsFailed={positionsPollOff}
decisions={effectiveDecisions} decisions={effectiveDecisions}
decisionsFailed={decisionsPollOff}
decisionsLimit={decisionsLimit} decisionsLimit={decisionsLimit}
onDecisionsLimitChange={setDecisionsLimit} onDecisionsLimitChange={setDecisionsLimit}
stats={stats} stats={stats}

View File

@@ -1167,6 +1167,9 @@ export const translations = {
close: 'Close', close: 'Close',
showingPositions: 'Showing {shown} of {total} positions', showingPositions: 'Showing {shown} of {total} positions',
perPage: 'Per page', perPage: 'Per page',
accountFetchFailed: 'DATA_FETCH::FAILED — Account data unavailable, check connection',
positionsFetchFailed: 'Position data unavailable',
decisionsFetchFailed: 'Decision data unavailable',
}, },
// AITradersPage toast messages // AITradersPage toast messages
@@ -2467,6 +2470,9 @@ export const translations = {
close: '平仓', close: '平仓',
showingPositions: '显示 {shown} / {total} 个持仓', showingPositions: '显示 {shown} / {total} 个持仓',
perPage: '每页', perPage: '每页',
accountFetchFailed: 'DATA_FETCH::FAILED — 账户数据请求失败,请检查连接',
positionsFetchFailed: '持仓数据请求失败',
decisionsFetchFailed: '决策记录请求失败',
}, },
aiTradersToast: { aiTradersToast: {
@@ -3570,6 +3576,9 @@ export const translations = {
close: 'Tutup', close: 'Tutup',
showingPositions: 'Menampilkan {shown} dari {total} posisi', showingPositions: 'Menampilkan {shown} dari {total} posisi',
perPage: 'Per halaman', perPage: 'Per halaman',
accountFetchFailed: 'DATA_FETCH::FAILED — Data akun tidak tersedia, periksa koneksi',
positionsFetchFailed: 'Data posisi tidak tersedia',
decisionsFetchFailed: 'Data keputusan tidak tersedia',
}, },
aiTradersToast: { aiTradersToast: {

View File

@@ -105,7 +105,9 @@ interface TraderDashboardPageProps {
account?: AccountInfo account?: AccountInfo
accountFailed?: boolean accountFailed?: boolean
positions?: Position[] positions?: Position[]
positionsFailed?: boolean
decisions?: DecisionRecord[] decisions?: DecisionRecord[]
decisionsFailed?: boolean
decisionsLimit: number decisionsLimit: number
onDecisionsLimitChange: (limit: number) => void onDecisionsLimitChange: (limit: number) => void
stats?: Statistics stats?: Statistics
@@ -120,7 +122,9 @@ export function TraderDashboardPage({
account, account,
accountFailed, accountFailed,
positions, positions,
positionsFailed,
decisions, decisions,
decisionsFailed,
decisionsLimit, decisionsLimit,
onDecisionsLimitChange, onDecisionsLimitChange,
lastUpdate, lastUpdate,
@@ -491,7 +495,7 @@ export function TraderDashboardPage({
<span>PNL::{account.total_pnl?.toFixed(2)}</span> <span>PNL::{account.total_pnl?.toFixed(2)}</span>
</div> </div>
) : accountFailed ? ( ) : accountFailed ? (
<span style={{ color: '#F6465D' }}>DATA_FETCH::FAILED </span> <span style={{ color: '#F6465D' }}>{t('traderDashboard.accountFetchFailed', language)}</span>
) : ( ) : (
<div className="flex gap-4"> <div className="flex gap-4">
<span className="inline-block w-32 h-3 rounded bg-white/5 animate-pulse" /> <span className="inline-block w-32 h-3 rounded bg-white/5 animate-pulse" />
@@ -723,6 +727,11 @@ export function TraderDashboardPage({
</div> </div>
)} )}
</div> </div>
) : positionsFailed ? (
<div className="text-center py-16 text-nofx-text-muted opacity-60">
<div className="text-4xl mb-4"></div>
<div className="text-lg font-semibold mb-2">{t('traderDashboard.positionsFetchFailed', language)}</div>
</div>
) : ( ) : (
<div className="text-center py-16 text-nofx-text-muted opacity-60"> <div className="text-center py-16 text-nofx-text-muted opacity-60">
<div className="text-6xl mb-4 opacity-50 grayscale">📊</div> <div className="text-6xl mb-4 opacity-50 grayscale">📊</div>
@@ -776,6 +785,11 @@ export function TraderDashboardPage({
decisions.map((decision, i) => ( decisions.map((decision, i) => (
<DecisionCard key={i} decision={decision} language={language} onSymbolClick={handleSymbolClick} /> <DecisionCard key={i} decision={decision} language={language} onSymbolClick={handleSymbolClick} />
)) ))
) : decisionsFailed ? (
<div className="py-16 text-center text-nofx-text-muted opacity-60">
<div className="text-4xl mb-4"></div>
<div className="text-lg font-semibold mb-2">{t('traderDashboard.decisionsFetchFailed', language)}</div>
</div>
) : ( ) : (
<div className="py-16 text-center text-nofx-text-muted opacity-60"> <div className="py-16 text-center text-nofx-text-muted opacity-60">
<div className="text-6xl mb-4 opacity-30 grayscale">🧠</div> <div className="text-6xl mb-4 opacity-30 grayscale">🧠</div>