fix: prevent DeepSeek token overflow with product-level limits (#1431)

* feat: enforce strategy limits to prevent token overflow

* fix: tune token limits after real-world testing

- Relax kline max 20→30, timeframes 3→4 (tested ~41K tokens, safe under 131K)
- Restore ranking limits to original [5,10,15,20] options (only ~1.5K token impact)
- Add static coins limit (max 3) with toast notification
- Add timeframe limit toast when exceeding 4
- Log SSE token usage (prompt/completion/total) from API response
- Fix nil logger crash in claw402 data client (engine.go)

* feat: add token estimation functionality for strategy configurations

* feat: add discard changes button in Strategy Studio for unsaved modifications

* feat: retain selected strategy after saving in Strategy Studio

* feat: enhance strategy display in Strategy Studio with improved layout and sorting of token limits

* refactor: improve layout and styling of stats display in CompetitionPage

* refactor: replace select elements with NofxSelect component for improved consistency in strategy configuration forms

* style: update NofxSelect component to use smaller text size for improved readability

* feat: implement token overflow handling in strategy updates and UI

---------

Co-authored-by: Dean <afei.wuhao@gmail.com>
This commit is contained in:
deanokk
2026-03-27 00:26:40 +08:00
committed by shinchan-zhai
parent b705810ec2
commit 79a3be1874
20 changed files with 1124 additions and 338 deletions

View File

@@ -29,6 +29,7 @@ import {
Download,
Upload,
Globe,
X,
} from 'lucide-react'
import type { Strategy, StrategyConfig, AIModel } from '../types'
import { confirmToast, notify } from '../lib/notify'
@@ -38,8 +39,10 @@ import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor'
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
import { GridConfigEditor, defaultGridConfig } from '../components/strategy/GridConfigEditor'
import { TokenEstimateBar } from '../components/strategy/TokenEstimateBar'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
import { t } from '../i18n/translations'
import { NofxSelect } from '../components/ui/select'
const API_BASE = import.meta.env.VITE_API_BASE || ''
@@ -52,6 +55,7 @@ export function StrategyStudioPage() {
const [editingConfig, setEditingConfig] = useState<StrategyConfig | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [tokenOverflow, setTokenOverflow] = useState(false)
const [error, setError] = useState<string | null>(null)
const [hasChanges, setHasChanges] = useState(false)
@@ -378,6 +382,10 @@ export function StrategyStudioPage() {
// Save strategy
const handleSaveStrategy = async () => {
if (!token || !selectedStrategy || !editingConfig) return
if (tokenOverflow && currentStrategyType === 'ai_trading') {
notify.error(tr('tokenExceedWarning'))
return
}
setIsSaving(true)
try {
// Always sync the config language with the current interface language
@@ -405,7 +413,17 @@ export function StrategyStudioPage() {
if (!response.ok) throw new Error('Failed to save strategy')
setHasChanges(false)
notify.success(tr('strategySaved'))
const savedId = selectedStrategy.id
await fetchStrategies()
// Stay on the strategy we just saved instead of jumping to active
setStrategies(prev => {
const saved = prev.find(s => s.id === savedId)
if (saved) {
setSelectedStrategy(saved)
setEditingConfig(saved.config)
}
return prev
})
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
@@ -641,7 +659,7 @@ export function StrategyStudioPage() {
<Sparkles className="w-5 h-5 text-black" />
</div>
<div>
<h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
<h1 className="text-lg font-bold text-nofx-text">{tr('title')}</h1>
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
</div>
</div>
@@ -756,34 +774,24 @@ export function StrategyStudioPage() {
{selectedStrategy && editingConfig ? (
<div className="p-4">
{/* Strategy Name & Actions */}
<div className="flex items-center justify-between mb-4">
<div className="flex-1 min-w-0">
<input
type="text"
value={selectedStrategy.name}
onChange={(e) => {
setSelectedStrategy({ ...selectedStrategy, name: e.target.value })
setHasChanges(true)
}}
disabled={selectedStrategy.is_default}
className="text-lg font-bold bg-transparent border-none outline-none w-full text-nofx-text placeholder-nofx-text-muted"
/>
<input
type="text"
value={selectedStrategy.description || ''}
onChange={(e) => {
setSelectedStrategy({ ...selectedStrategy, description: e.target.value })
setHasChanges(true)
}}
disabled={selectedStrategy.is_default}
placeholder={tr('addDescription')}
className="text-xs bg-transparent border-none outline-none w-full text-nofx-text-muted placeholder-nofx-text-muted/50 mt-1"
/>
{hasChanges && (
<span className="text-xs text-nofx-gold"> {tr('unsaved')}</span>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="mb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 flex-1 min-w-0">
<input
type="text"
value={selectedStrategy.name}
onChange={(e) => {
setSelectedStrategy({ ...selectedStrategy, name: e.target.value })
setHasChanges(true)
}}
disabled={selectedStrategy.is_default}
className="text-lg font-bold bg-transparent border-none outline-none flex-1 min-w-0 text-nofx-text placeholder-nofx-text-muted"
/>
{hasChanges && (
<span className="text-xs text-nofx-gold whitespace-nowrap"> {tr('unsaved')}</span>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
{!selectedStrategy.is_active && (
<button
onClick={() => handleActivateStrategy(selectedStrategy.id)}
@@ -793,10 +801,22 @@ export function StrategyStudioPage() {
{tr('activate')}
</button>
)}
{!selectedStrategy.is_default && hasChanges && (
<button
onClick={() => {
setEditingConfig(selectedStrategy.config)
setHasChanges(false)
}}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors text-nofx-text-muted hover:text-nofx-text hover:bg-nofx-bg-lighter border border-nofx-border"
>
<X className="w-3 h-3" />
{tr('discardChanges')}
</button>
)}
{!selectedStrategy.is_default && (
<button
onClick={handleSaveStrategy}
disabled={isSaving || !hasChanges}
disabled={isSaving || !hasChanges || (tokenOverflow && currentStrategyType === 'ai_trading')}
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors disabled:opacity-50
${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`}
>
@@ -805,8 +825,27 @@ export function StrategyStudioPage() {
</button>
)}
</div>
</div>
<input
type="text"
value={selectedStrategy.description || ''}
onChange={(e) => {
setSelectedStrategy({ ...selectedStrategy, description: e.target.value })
setHasChanges(true)
}}
disabled={selectedStrategy.is_default}
placeholder={tr('addDescription')}
className="text-xs bg-transparent border-none outline-none w-full text-nofx-text-muted placeholder-nofx-text-muted/50 mt-1"
/>
</div>
{/* Token Estimate Bar */}
{currentStrategyType === 'ai_trading' && (
<div className="mb-4">
<TokenEstimateBar config={editingConfig} language={language} onOverflowChange={setTokenOverflow} />
</div>
)}
{/* Strategy Type Selector */}
{editingConfig && (
<div className="mb-4 p-4 rounded-lg bg-nofx-bg-lighter border border-nofx-gold/20">
@@ -818,9 +857,12 @@ export function StrategyStudioPage() {
<button
onClick={() => {
if (!selectedStrategy?.is_default) {
updateConfig('strategy_type', 'ai_trading')
// Clear grid config when switching to AI trading
updateConfig('grid_config', undefined)
setEditingConfig(prev => prev ? {
...prev,
strategy_type: 'ai_trading',
grid_config: undefined,
} : prev)
setHasChanges(true)
}
}}
disabled={selectedStrategy?.is_default}
@@ -839,11 +881,12 @@ export function StrategyStudioPage() {
<button
onClick={() => {
if (!selectedStrategy?.is_default) {
updateConfig('strategy_type', 'grid_trading')
// Initialize grid config if not exists
if (!editingConfig.grid_config) {
updateConfig('grid_config', defaultGridConfig)
}
setEditingConfig(prev => prev ? {
...prev,
strategy_type: 'grid_trading',
grid_config: prev.grid_config || defaultGridConfig,
} : prev)
setHasChanges(true)
}
}}
disabled={selectedStrategy?.is_default}
@@ -934,15 +977,16 @@ export function StrategyStudioPage() {
<div className="p-3 space-y-3">
{/* Controls */}
<div className="flex items-center gap-2 flex-wrap">
<select
<NofxSelect
value={selectedVariant}
onChange={(e) => setSelectedVariant(e.target.value)}
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text outline-none focus:border-nofx-gold"
>
<option value="balanced">{tr('balanced')}</option>
<option value="aggressive">{tr('aggressive')}</option>
<option value="conservative">{tr('conservative')}</option>
</select>
onChange={(val) => setSelectedVariant(val)}
options={[
{ value: 'balanced', label: tr('balanced') },
{ value: 'aggressive', label: tr('aggressive') },
{ value: 'conservative', label: tr('conservative') },
]}
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
/>
<button
onClick={fetchPromptPreview}
disabled={isLoadingPrompt || !editingConfig}
@@ -1007,17 +1051,15 @@ export function StrategyStudioPage() {
<span className="text-xs font-medium text-nofx-text">{tr('selectModel')}</span>
</div>
{aiModels.length > 0 ? (
<select
<NofxSelect
value={selectedModelId}
onChange={(e) => setSelectedModelId(e.target.value)}
onChange={(val) => setSelectedModelId(val)}
options={aiModels.map((model) => ({
value: model.id,
label: `${model.name} (${model.provider})`,
}))}
className="w-full px-3 py-2 rounded-lg text-sm bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
>
{aiModels.map((model) => (
<option key={model.id} value={model.id}>
{model.name} ({model.provider})
</option>
))}
</select>
/>
) : (
<div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger">
{tr('noModel')}
@@ -1025,15 +1067,16 @@ export function StrategyStudioPage() {
)}
<div className="flex items-center gap-2">
<select
<NofxSelect
value={selectedVariant}
onChange={(e) => setSelectedVariant(e.target.value)}
onChange={(val) => setSelectedVariant(val)}
options={[
{ value: 'balanced', label: tr('balanced') },
{ value: 'aggressive', label: tr('aggressive') },
{ value: 'conservative', label: tr('conservative') },
]}
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
>
<option value="balanced">{tr('balanced')}</option>
<option value="aggressive">{tr('aggressive')}</option>
<option value="conservative">{tr('conservative')}</option>
</select>
/>
<button
onClick={runAiTest}
disabled={isRunningAiTest || !editingConfig || !selectedModelId}

View File

@@ -10,6 +10,7 @@ import { formatPrice, formatQuantity } from '../utils/format'
import { t, type Language } from '../i18n/translations'
import { LogOut, Loader2, Eye, EyeOff, Copy, Check } from 'lucide-react'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
import { NofxSelect } from '../components/ui/select'
import { GridRiskPanel } from '../components/strategy/GridRiskPanel'
import type {
SystemStatus,
@@ -376,17 +377,12 @@ export function TraderDashboardPage({
{/* Trader Selector */}
{traders && traders.length > 0 && (
<div className="flex items-center gap-2 nofx-glass px-1 py-1 rounded-lg border border-white/5">
<select
value={selectedTraderId}
onChange={(e) => onTraderSelect(e.target.value)}
className="bg-transparent text-sm font-medium cursor-pointer transition-colors text-nofx-text-main focus:outline-none px-2 py-1"
>
{traders.map((trader) => (
<option key={trader.trader_id} value={trader.trader_id} className="bg-[#0B0E11]">
{trader.trader_name}
</option>
))}
</select>
<NofxSelect
value={selectedTraderId || ''}
onChange={(val) => onTraderSelect(val)}
options={traders.map(t => ({ value: t.trader_id, label: t.trader_name }))}
className="bg-transparent text-sm font-medium cursor-pointer transition-colors text-nofx-text-main px-2 py-1"
/>
</div>
)}
@@ -484,16 +480,22 @@ export function TraderDashboardPage({
</div>
{/* Debug Info */}
{account && (
<div className="mb-4 px-3 py-1.5 rounded bg-black/40 border border-white/5 text-[10px] font-mono text-nofx-text-muted flex justify-between items-center opacity-60 hover:opacity-100 transition-opacity">
<span>SYSTEM_STATUS::ONLINE</span>
<div className="mb-4 px-3 py-1.5 rounded bg-black/40 border border-white/5 text-[10px] font-mono text-nofx-text-muted flex justify-between items-center opacity-60 hover:opacity-100 transition-opacity">
<span style={{ color: '#0ECB81' }}>SYSTEM_STATUS::ONLINE</span>
{account ? (
<div className="flex gap-4">
<span>LAST_UPDATE::{lastUpdate}</span>
<span>EQ::{account?.total_equity?.toFixed(2)}</span>
<span>PNL::{account?.total_pnl?.toFixed(2)}</span>
<span>EQ::{account.total_equity?.toFixed(2)}</span>
<span>PNL::{account.total_pnl?.toFixed(2)}</span>
</div>
</div>
)}
) : (
<div className="flex gap-4">
<span className="inline-block w-32 h-3 rounded bg-white/5 animate-pulse" />
<span className="inline-block w-16 h-3 rounded bg-white/5 animate-pulse" />
<span className="inline-block w-16 h-3 rounded bg-white/5 animate-pulse" />
</div>
)}
</div>
{/* Account Overview */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
@@ -504,6 +506,7 @@ export function TraderDashboardPage({
change={account?.total_pnl_pct || 0}
positive={(account?.total_pnl ?? 0) > 0}
icon="💰"
loading={!account}
/>
<StatCard
title={t('availableBalance', language)}
@@ -511,6 +514,7 @@ export function TraderDashboardPage({
unit="USDT"
subtitle={`${account?.available_balance && account?.total_equity ? ((account.available_balance / account.total_equity) * 100).toFixed(1) : '0.0'}% ${t('free', language)}`}
icon="💳"
loading={!account}
/>
<StatCard
title={t('totalPnL', language)}
@@ -519,6 +523,7 @@ export function TraderDashboardPage({
change={account?.total_pnl_pct || 0}
positive={(account?.total_pnl ?? 0) >= 0}
icon="📈"
loading={!account}
/>
<StatCard
title={t('positions', language)}
@@ -526,6 +531,7 @@ export function TraderDashboardPage({
unit="ACTIVE"
subtitle={`${t('margin', language)}: ${account?.margin_used_pct?.toFixed(1) || '0.0'}%`}
icon="📊"
loading={!account}
/>
</div>
@@ -671,15 +677,12 @@ export function TraderDashboardPage({
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span>{t('traderDashboard.perPage', language)}:</span>
<select
<NofxSelect
value={positionsPageSize}
onChange={(e) => setPositionsPageSize(Number(e.target.value))}
className="bg-black/40 border border-white/10 rounded px-2 py-1 text-xs text-nofx-text-main focus:outline-none focus:border-nofx-gold/50 transition-colors"
>
<option value={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
onChange={(val) => setPositionsPageSize(Number(val))}
options={[{ value: 20, label: '20' }, { value: 50, label: '50' }, { value: 100, label: '100' }]}
className="bg-black/40 border border-white/10 rounded px-2 py-1 text-xs text-nofx-text-main transition-colors"
/>
</div>
{totalPositionPages > 1 && (
<div className="flex items-center gap-1">
@@ -752,17 +755,12 @@ export function TraderDashboardPage({
)}
</div>
{/* Limit Selector */}
<select
<NofxSelect
value={decisionsLimit}
onChange={(e) => onDecisionsLimitChange(Number(e.target.value))}
className="px-3 py-1.5 rounded-lg text-sm font-medium cursor-pointer transition-all bg-black/40 text-nofx-text-main border border-white/10 hover:border-nofx-accent focus:outline-none"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
onChange={(val) => onDecisionsLimitChange(Number(val))}
options={[{ value: 5, label: '5' }, { value: 10, label: '10' }, { value: 20, label: '20' }, { value: 50, label: '50' }, { value: 100, label: '100' }]}
className="px-3 py-1.5 rounded-lg text-sm font-medium cursor-pointer transition-all bg-black/40 text-nofx-text-main border border-white/10 hover:border-nofx-accent"
/>
</div>
{/* Decisions List - Scrollable */}
@@ -818,6 +816,7 @@ function StatCard({
positive,
subtitle,
icon,
loading,
}: {
title: string
value: string
@@ -826,6 +825,7 @@ function StatCard({
positive?: boolean
subtitle?: string
icon?: string
loading?: boolean
}) {
return (
<div className="group nofx-glass p-5 rounded-lg transition-all duration-300 hover:bg-white/5 hover:translate-y-[-2px] border border-white/5 hover:border-nofx-gold/20 relative overflow-hidden">
@@ -835,27 +835,35 @@ function StatCard({
<div className="text-xs mb-2 font-mono uppercase tracking-wider text-nofx-text-muted flex items-center gap-2">
{title}
</div>
<div className="flex items-baseline gap-1 mb-1">
<div className="text-2xl font-bold font-mono text-nofx-text-main tracking-tight group-hover:text-white transition-colors">
{value}
{loading ? (
<div className="space-y-2">
<div className="h-7 w-24 rounded bg-white/5 animate-pulse" />
<div className="h-3 w-16 rounded bg-white/5 animate-pulse" />
</div>
{unit && <span className="text-xs font-mono text-nofx-text-muted opacity-60">{unit}</span>}
</div>
{change !== undefined && (
<div className="flex items-center gap-1">
<div
className={`text-sm mono font-bold flex items-center gap-1 ${positive ? 'text-nofx-green' : 'text-nofx-red'}`}
>
<span>{positive ? '▲' : '▼'}</span>
<span>{positive ? '+' : ''}{change.toFixed(2)}%</span>
) : (
<>
<div className="flex items-baseline gap-1 mb-1">
<div className="text-2xl font-bold font-mono text-nofx-text-main tracking-tight group-hover:text-white transition-colors">
{value}
</div>
{unit && <span className="text-xs font-mono text-nofx-text-muted opacity-60">{unit}</span>}
</div>
</div>
)}
{subtitle && (
<div className="text-xs mt-2 mono text-nofx-text-muted opacity-80">
{subtitle}
</div>
{change !== undefined && (
<div className="flex items-center gap-1">
<div
className={`text-sm mono font-bold flex items-center gap-1 ${positive ? 'text-nofx-green' : 'text-nofx-red'}`}
>
<span>{positive ? '▲' : '▼'}</span>
<span>{positive ? '+' : ''}{change.toFixed(2)}%</span>
</div>
</div>
)}
{subtitle && (
<div className="text-xs mt-2 mono text-nofx-text-muted opacity-80">
{subtitle}
</div>
)}
</>
)}
</div>
)