This commit is contained in:
薇薇安 2026-02-27 13:58:59 +08:00
parent 861f1dc548
commit d5ef525224

View File

@ -1208,9 +1208,32 @@ const GlobalConfig = () => {
},
]
// STATS_*
const rawStatsSymbolFilters = configs?.STATS_SYMBOL_FILTERS
const rawStatsHourFilters = configs?.STATS_HOUR_FILTERS
const statsSymbolFiltersValue =
rawStatsSymbolFilters && typeof rawStatsSymbolFilters === 'object'
? (rawStatsSymbolFilters.value ?? rawStatsSymbolFilters)
: null
const statsHourFiltersValue =
rawStatsHourFilters && typeof rawStatsHourFilters === 'object'
? (rawStatsHourFilters.value ?? rawStatsHourFilters)
: null
const statsBlacklist = Array.isArray(statsSymbolFiltersValue?.blacklist)
? statsSymbolFiltersValue.blacklist
: []
const statsWhitelist = Array.isArray(statsSymbolFiltersValue?.whitelist)
? statsSymbolFiltersValue.whitelist
: []
const statsHours = Array.isArray(statsHourFiltersValue?.hours)
? statsHourFiltersValue.hours
: []
const statsGeneratedAt = statsSymbolFiltersValue?.generated_at || statsHourFiltersValue?.generated_at || null
const statsDays = statsSymbolFiltersValue?.days || statsHourFiltersValue?.days || null
return (
<div className="global-config">
@ -1653,6 +1676,7 @@ const GlobalConfig = () => {
{ key: 'strategy', label: '策略参数' },
{ key: 'scan', label: '市场扫描' },
{ key: 'position', label: '仓位控制' },
{ key: 'stats', label: '统计过滤' },
].map(tab => (
<button
key={tab.key}
@ -1676,13 +1700,169 @@ const GlobalConfig = () => {
</div>
</div>
{/* 统计过滤可视化(仅在“统计过滤” Tab 展示) */}
{activeTab === 'stats' && (
<div style={{ marginBottom: '24px', padding: '16px', borderRadius: '8px', background: '#f8f9fb', border: '1px solid #e1e5ee' }}>
<div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '12px', flexWrap: 'wrap' }}>
<div>
<h4 style={{ margin: 0, fontSize: '15px' }}>统计过滤总览</h4>
<div style={{ fontSize: '12px', color: '#6c757d', marginTop: '4px' }}>
基于最近 {statsDays || 7} 天的交易统计自动生成差标的与差时段会被自动降权并提高信号门槛仅影响自动交易不会一刀切禁止
</div>
</div>
{statsGeneratedAt && (
<div style={{ fontSize: '12px', color: '#6c757d' }}>生成时间UTC{String(statsGeneratedAt).replace('T', ' ').slice(0, 19)}</div>
)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 2fr) minmax(0, 1.6fr)', gap: '16px' }}>
{/* Symbol 白/黑名单 */}
<div style={{ background: '#ffffff', borderRadius: '6px', padding: '12px', border: '1px solid #e5e9f0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<strong style={{ fontSize: '13px' }}>交易对白名单 / 黑名单</strong>
<span style={{ fontSize: '11px', color: '#6c757d' }}>
白名单 {statsWhitelist.length} · 黑名单 {statsBlacklist.length}
</span>
</div>
{(statsWhitelist.length === 0 && statsBlacklist.length === 0) ? (
<div style={{ fontSize: '12px', color: '#999' }}>暂无统计结果需先定时跑 7 天聚合脚本</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '10px' }}>
<div>
<div style={{ fontSize: '12px', fontWeight: 600, marginBottom: '4px', color: '#155724' }}>白名单近期表现较好</div>
<div style={{ maxHeight: '220px', overflowY: 'auto', border: '1px solid #e9ecef', borderRadius: '4px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '12px' }}>
<thead>
<tr style={{ background: '#f1f3f5' }}>
<th style={{ padding: '4px 6px', textAlign: 'left' }}>Symbol</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>笔数</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>净盈亏</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>胜率%</th>
</tr>
</thead>
<tbody>
{statsWhitelist.slice(0, 20).map((item) => (
<tr key={item.symbol}>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5', fontWeight: 600 }}>{item.symbol}</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5', textAlign: 'right' }}>{item.trade_count}</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5', textAlign: 'right', color: Number(item.net_pnl) >= 0 ? '#28a745' : '#dc3545' }}>
{Number(item.net_pnl) >= 0 ? '+' : ''}{Number(item.net_pnl).toFixed(2)}
</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5', textAlign: 'right' }}>{Number(item.win_rate_pct).toFixed(1)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div>
<div style={{ fontSize: '12px', fontWeight: 600, marginBottom: '4px', color: '#721c24' }}>黑名单软降权考核期中</div>
<div style={{ maxHeight: '220px', overflowY: 'auto', border: '1px solid #f5c6cb', borderRadius: '4px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '12px' }}>
<thead>
<tr style={{ background: '#f8d7da' }}>
<th style={{ padding: '4px 6px', textAlign: 'left' }}>Symbol</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>笔数</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>净盈亏</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>胜率%</th>
</tr>
</thead>
<tbody>
{statsBlacklist.slice(0, 20).map((item) => (
<tr key={item.symbol}>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f5c6cb', fontWeight: 600 }}>{item.symbol}</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f5c6cb', textAlign: 'right' }}>{item.trade_count}</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f5c6cb', textAlign: 'right', color: '#dc3545' }}>
{Number(item.net_pnl).toFixed(2)}
</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f5c6cb', textAlign: 'right' }}>{Number(item.win_rate_pct).toFixed(1)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
{/* 按小时时段过滤 */}
<div style={{ background: '#ffffff', borderRadius: '6px', padding: '12px', border: '1px solid #e5e9f0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<strong style={{ fontSize: '13px' }}>按小时过滤北京时间</strong>
<span style={{ fontSize: '11px', color: '#6c757d' }}>bucket=bad 降仓 & 提高信号门槛</span>
</div>
{statsHours.length === 0 ? (
<div style={{ fontSize: '12px', color: '#999' }}>暂无时段统计结果</div>
) : (
<div style={{ maxHeight: '260px', overflowY: 'auto', border: '1px solid #e9ecef', borderRadius: '4px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '12px' }}>
<thead>
<tr style={{ background: '#f1f3f5' }}>
<th style={{ padding: '4px 6px', textAlign: 'left' }}>小时</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>笔数</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>净盈亏</th>
<th style={{ padding: '4px 6px', textAlign: 'center' }}>分组</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>仓位系数</th>
<th style={{ padding: '4px 6px', textAlign: 'right' }}>信号+档数</th>
</tr>
</thead>
<tbody>
{statsHours
.slice()
.sort((a, b) => Number(a.hour) - Number(b.hour))
.map((item) => {
const bucket = String(item.bucket || '').toLowerCase()
const rowBg =
bucket === 'bad'
? '#fff5f5'
: bucket === 'good'
? '#f0fff4'
: '#ffffff'
const badgeColor =
bucket === 'bad'
? '#dc3545'
: bucket === 'good'
? '#28a745'
: '#6c757d'
return (
<tr key={item.hour} style={{ background: rowBg }}>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5' }}>{item.hour}:00</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5', textAlign: 'right' }}>{item.trade_count}</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5', textAlign: 'right', color: Number(item.net_pnl) >= 0 ? '#28a745' : '#dc3545' }}>
{Number(item.net_pnl) >= 0 ? '+' : ''}{Number(item.net_pnl).toFixed(2)}
</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5', textAlign: 'center' }}>
<span style={{ display: 'inline-block', padding: '1px 6px', borderRadius: '10px', fontSize: '11px', color: badgeColor, border: `1px solid ${badgeColor}33` }}>
{bucket || 'unknown'}
</span>
</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5', textAlign: 'right' }}>
{item.position_factor !== undefined ? Number(item.position_factor).toFixed(2) : '1.00'}
</td>
<td style={{ padding: '3px 6px', borderTop: '1px solid #f1f3f5', textAlign: 'right' }}>
{item.signal_boost || 0}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
)}
{Object.keys(configs).length > 0 ? (
(() => {
const configCategories = {
'risk': '风险控制',
'strategy': '策略参数',
'scan': '市场扫描',
'position': '仓位控制',
risk: '风险控制',
strategy: '策略参数',
scan: '市场扫描',
position: '仓位控制',
stats: '统计过滤',
}
//