mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 23:07:01 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user