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

@@ -281,14 +281,14 @@ export function CompetitionPage() {
</div>
{/* Stats */}
<div className="flex items-center gap-2 md:gap-3 flex-wrap md:flex-nowrap">
<div className="flex items-center gap-4 md:gap-6">
{/* Total Equity */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-right min-w-[60px] md:min-w-[80px]">
<div className="text-[10px] mb-0.5" style={{ color: '#848E9C' }}>
{t('equity', language)}
</div>
<div
className="text-xs md:text-sm font-bold mono"
className="text-sm md:text-base font-bold mono"
style={{ color: '#EAECEF' }}
>
{trader.total_equity?.toFixed(2) || '0.00'}
@@ -297,11 +297,11 @@ export function CompetitionPage() {
{/* P&L */}
<div className="text-right min-w-[70px] md:min-w-[90px]">
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-[10px] mb-0.5" style={{ color: '#848E9C' }}>
{t('pnl', language)}
</div>
<div
className="text-base md:text-lg font-bold mono"
className="text-sm md:text-base font-bold mono"
style={{
color:
(trader.total_pnl ?? 0) >= 0
@@ -313,7 +313,7 @@ export function CompetitionPage() {
{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
</div>
<div
className="text-xs mono"
className="text-[10px] mono"
style={{ color: '#848E9C' }}
>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
@@ -322,17 +322,17 @@ export function CompetitionPage() {
</div>
{/* Positions */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-right min-w-[40px] md:min-w-[50px]">
<div className="text-[10px] mb-0.5" style={{ color: '#848E9C' }}>
{t('pos', language)}
</div>
<div
className="text-xs md:text-sm font-bold mono"
className="text-sm md:text-base font-bold mono"
style={{ color: '#EAECEF' }}
>
{trader.position_count}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="text-[10px]" style={{ color: '#848E9C' }}>
{trader.margin_used_pct.toFixed(1)}%
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { useLanguage } from '../../contexts/LanguageContext'
import { t, type Language } from '../../i18n/translations'
import { MetricTooltip } from '../common/MetricTooltip'
import { formatPrice, formatQuantity } from '../../utils/format'
import { NofxSelect } from '../ui/select'
import type {
HistoricalPosition,
TraderStats,
@@ -664,23 +665,20 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
<span className="text-sm" style={{ color: '#848E9C' }}>
{t('positionHistory.symbol', language)}:
</span>
<select
<NofxSelect
value={filterSymbol}
onChange={(e) => setFilterSymbol(e.target.value)}
onChange={(val) => setFilterSymbol(val)}
options={[
{ value: 'all', label: t('positionHistory.allSymbols', language) },
...uniqueSymbols.map(s => ({ value: s, label: (s || '').replace('USDT', '') }))
]}
className="rounded px-3 py-1.5 text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
>
<option value="all">{t('positionHistory.allSymbols', language)}</option>
{uniqueSymbols.map((symbol) => (
<option key={symbol} value={symbol}>
{(symbol || '').replace('USDT', '')}
</option>
))}
</select>
/>
</div>
<div className="flex items-center gap-2">
@@ -708,28 +706,26 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
<span className="text-sm" style={{ color: '#848E9C' }}>
{t('positionHistory.sort', language)}:
</span>
<select
<NofxSelect
value={`${sortBy}-${sortOrder}`}
onChange={(e) => {
const [by, order] = e.target.value.split('-') as [
'time' | 'pnl' | 'pnl_pct',
'asc' | 'desc',
]
onChange={(val) => {
const [by, order] = val.split('-') as ['time' | 'pnl' | 'pnl_pct', 'asc' | 'desc']
setSortBy(by)
setSortOrder(order)
}}
options={[
{ value: 'time-desc', label: t('positionHistory.latestFirst', language) },
{ value: 'time-asc', label: t('positionHistory.oldestFirst', language) },
{ value: 'pnl-desc', label: t('positionHistory.highestPnL', language) },
{ value: 'pnl-asc', label: t('positionHistory.lowestPnL', language) },
]}
className="rounded px-3 py-1.5 text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
>
<option value="time-desc">{t('positionHistory.latestFirst', language)}</option>
<option value="time-asc">{t('positionHistory.oldestFirst', language)}</option>
<option value="pnl-desc">{t('positionHistory.highestPnL', language)}</option>
<option value="pnl-asc">{t('positionHistory.lowestPnL', language)}</option>
</select>
/>
</div>
</div>
@@ -841,20 +837,21 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
<span className="text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '每页' : 'Per page'}:
</span>
<select
<NofxSelect
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
onChange={(val) => setPageSize(Number(val))}
options={[
{ value: 20, label: '20' },
{ value: 50, label: '50' },
{ value: 100, label: '100' },
]}
className="rounded px-2 py-1 text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
>
<option value={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
/>
</div>
{/* Page navigation */}

View File

@@ -4,6 +4,7 @@ import { toast } from 'sonner'
import { api } from '../../lib/api'
import type { TelegramConfig, AIModel } from '../../types'
import { t, type Language } from '../../i18n/translations'
import { NofxSelect } from '../ui/select'
// Step indicator (reused pattern from ExchangeConfigModal)
function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) {
@@ -133,23 +134,20 @@ export function TelegramConfigModal({ onClose, language }: TelegramConfigModalPr
{t('telegram.noEnabledModels', language)}
</div>
) : (
<select
<NofxSelect
value={selectedModelId}
onChange={(e) => setSelectedModelId(e.target.value)}
className="w-full px-4 py-3 rounded-xl text-sm appearance-none"
onChange={(val) => setSelectedModelId(val)}
options={[
{ value: '', label: t('telegram.autoSelect', language) },
...models.map(m => ({ value: m.id, label: `${m.name} (${m.provider}${m.customModelName ? ` · ${m.customModelName}` : ''})` }))
]}
className="w-full px-4 py-3 rounded-xl text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: selectedModelId ? '#EAECEF' : '#848E9C',
}}
>
<option value="">{t('telegram.autoSelect', language)}</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name} ({m.provider}{m.customModelName ? ` · ${m.customModelName}` : ''})
</option>
))}
</select>
/>
)}
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('telegram.autoUseEnabled', language)}
@@ -489,23 +487,20 @@ function BoundModelSelector({
{t('telegram.aiModelLabel', language)}
</label>
<div className="flex gap-2">
<select
<NofxSelect
value={modelId}
onChange={(e) => setModelId(e.target.value)}
className="flex-1 px-3 py-2.5 rounded-xl text-sm appearance-none"
onChange={(val) => setModelId(val)}
options={[
{ value: '', label: t('telegram.aiModelAutoSelect', language) },
...models.map(m => ({ value: m.id, label: `${m.name}${m.customModelName ? ` · ${m.customModelName}` : ''}` }))
]}
className="flex-1 px-3 py-2.5 rounded-xl text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: modelId ? '#EAECEF' : '#848E9C',
}}
>
<option value="">{t('telegram.aiModelAutoSelect', language)}</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name}{m.customModelName ? ` · ${m.customModelName}` : ''}
</option>
))}
</select>
/>
<button
onClick={handleSave}
disabled={isSaving || modelId === currentModelId}

View File

@@ -5,6 +5,7 @@ import { t } from '../../i18n/translations'
import { toast } from 'sonner'
import { Pencil, Plus, X as IconX, Sparkles, ExternalLink, UserPlus } from 'lucide-react'
import { httpClient } from '../../lib/httpClient'
import { NofxSelect } from '../ui/select'
// 提取下划线后面的名称部分
function getShortName(fullName: string): string {
@@ -250,38 +251,34 @@ export function TraderConfigModal({
<label className="text-sm text-[#EAECEF] block mb-2">
{t('aiModelRequired', language)}
</label>
<select
<NofxSelect
value={formData.ai_model}
onChange={(e) =>
handleInputChange('ai_model', e.target.value)
onChange={(val) =>
handleInputChange('ai_model', val)
}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
>
{availableModels.map((model) => (
<option key={model.id} value={model.id}>
{getShortName(model.name || model.id).toUpperCase()}
</option>
))}
</select>
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF]"
options={availableModels.map((model) => ({
value: model.id,
label: getShortName(model.name || model.id).toUpperCase(),
}))}
/>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
{t('exchangeRequired', language)}
</label>
<select
<NofxSelect
value={formData.exchange_id}
onChange={(e) =>
handleInputChange('exchange_id', e.target.value)
onChange={(val) =>
handleInputChange('exchange_id', val)
}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
>
{availableExchanges.map((exchange) => (
<option key={exchange.id} value={exchange.id}>
{getShortName(exchange.name || exchange.exchange_type || exchange.id).toUpperCase()}
{exchange.account_name ? ` - ${exchange.account_name}` : ''}
</option>
))}
</select>
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF]"
options={availableExchanges.map((exchange) => ({
value: exchange.id,
label: getShortName(exchange.name || exchange.exchange_type || exchange.id).toUpperCase()
+ (exchange.account_name ? ` - ${exchange.account_name}` : ''),
}))}
/>
{/* Exchange Registration Link */}
{formData.exchange_id && (() => {
// Find the selected exchange to get its type
@@ -323,22 +320,20 @@ export function TraderConfigModal({
<label className="text-sm text-[#EAECEF] block mb-2">
{t('useStrategy', language)}
</label>
<select
<NofxSelect
value={formData.strategy_id}
onChange={(e) =>
handleInputChange('strategy_id', e.target.value)
onChange={(val) =>
handleInputChange('strategy_id', val)
}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
>
<option value="">{t('noStrategyManual', language)}</option>
{strategies.map((strategy) => (
<option key={strategy.id} value={strategy.id}>
{strategy.name}
{strategy.is_active ? t('strategyActive', language) : ''}
{strategy.is_default ? t('strategyDefault', language) : ''}
</option>
))}
</select>
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF]"
options={[
{ value: '', label: t('noStrategyManual', language) },
...strategies.map((strategy) => ({
value: strategy.id,
label: strategy.name + (strategy.is_active ? t('strategyActive', language) : '') + (strategy.is_default ? t('strategyDefault', language) : ''),
})),
]}
/>
{strategies.length === 0 && (
<p className="text-xs text-[#848E9C] mt-2">
{t('noStrategyHint', language)}