- Add EvaluationHook for post-execution agent evaluation - Add SkillAdaptationHook for dynamic skill adaptation - Add team/ directory with team coordination logic - Add TEAM_PIPELINE.yaml for smoke_fullstack pipeline config - Update RuntimeView, TraderView and RuntimeSettingsPanel UI - Add runtimeApi and websocket services - Add runtime_state.json to smoke_fullstack state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
577 lines
21 KiB
JavaScript
577 lines
21 KiB
JavaScript
import React from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
|
||
export default function RuntimeSettingsPanel({
|
||
showTrigger = true,
|
||
isOpen,
|
||
isConnected,
|
||
isSaving,
|
||
feedback,
|
||
scheduleMode,
|
||
intervalMinutes,
|
||
triggerTime,
|
||
maxCommCycles,
|
||
initialCash,
|
||
marginRequirement,
|
||
enableMemory,
|
||
mode,
|
||
pollInterval,
|
||
startDate,
|
||
endDate,
|
||
enableMock,
|
||
watchlistSymbols,
|
||
watchlistInputValue,
|
||
watchlistSuggestions,
|
||
onToggle,
|
||
onClose,
|
||
onScheduleModeChange,
|
||
onIntervalMinutesChange,
|
||
onTriggerTimeChange,
|
||
onMaxCommCyclesChange,
|
||
onInitialCashChange,
|
||
onMarginRequirementChange,
|
||
onEnableMemoryChange,
|
||
onModeChange,
|
||
onPollIntervalChange,
|
||
onStartDateChange,
|
||
onEndDateChange,
|
||
onEnableMockChange,
|
||
onWatchlistInputChange,
|
||
onWatchlistInputKeyDown,
|
||
onWatchlistAdd,
|
||
onWatchlistRemove,
|
||
onWatchlistRestoreCurrent,
|
||
onWatchlistRestoreDefault,
|
||
onWatchlistSuggestionClick,
|
||
onSave,
|
||
onRestoreDefaults
|
||
}) {
|
||
return (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
||
{showTrigger && (
|
||
<button
|
||
onClick={onToggle}
|
||
style={{
|
||
padding: '6px 10px',
|
||
borderRadius: 4,
|
||
border: '1px solid #333333',
|
||
background: isOpen ? '#1E1E1E' : '#111111',
|
||
color: '#FFFFFF',
|
||
fontSize: '11px',
|
||
fontWeight: 700,
|
||
letterSpacing: '0.6px',
|
||
cursor: 'pointer',
|
||
whiteSpace: 'nowrap'
|
||
}}
|
||
>
|
||
启动配置
|
||
</button>
|
||
)}
|
||
|
||
{isOpen && createPortal((
|
||
<div
|
||
onClick={onClose}
|
||
style={{
|
||
position: 'fixed',
|
||
inset: 0,
|
||
background: 'rgba(15, 23, 42, 0.28)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
padding: 24,
|
||
zIndex: 9998
|
||
}}
|
||
>
|
||
<div
|
||
onClick={(event) => event.stopPropagation()}
|
||
style={{
|
||
width: 'min(760px, 92vw)',
|
||
maxHeight: '80vh',
|
||
overflowY: 'auto',
|
||
borderRadius: 16,
|
||
border: '1px solid #D9E0E7',
|
||
background: '#FFFFFF',
|
||
boxShadow: '0 24px 60px rgba(15, 23, 42, 0.18)',
|
||
padding: 18,
|
||
paddingTop: 22,
|
||
display: 'grid',
|
||
gap: 16,
|
||
position: 'relative',
|
||
zIndex: 9999
|
||
}}
|
||
>
|
||
<button
|
||
onClick={onClose}
|
||
style={{
|
||
position: 'absolute',
|
||
top: 16,
|
||
right: 16,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
borderRadius: 999,
|
||
width: 40,
|
||
height: 40,
|
||
fontSize: 16,
|
||
lineHeight: 1,
|
||
color: '#111111',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
cursor: 'pointer',
|
||
boxShadow: '0 4px 12px rgba(15, 23, 42, 0.08)'
|
||
}}
|
||
aria-label="关闭启动配置"
|
||
>
|
||
×
|
||
</button>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center', paddingRight: 56 }}>
|
||
<div style={{ display: 'grid', gap: 4 }}>
|
||
<div style={{ fontSize: 14, fontWeight: 800, color: '#111111' }}>启动配置</div>
|
||
<div style={{ fontSize: 11, color: '#6B7280' }}>
|
||
配置本次任务的启动参数与调度方式
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{
|
||
border: '1px solid #E5EAF1',
|
||
borderRadius: 12,
|
||
background: '#FCFDFE',
|
||
padding: 14,
|
||
display: 'grid',
|
||
gap: 12
|
||
}}>
|
||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>自选股</div>
|
||
|
||
<div style={{
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
gap: 8,
|
||
minHeight: 40,
|
||
padding: '2px 0'
|
||
}}>
|
||
{watchlistSymbols.map((symbol) => (
|
||
<button
|
||
key={symbol}
|
||
onClick={() => onWatchlistRemove(symbol)}
|
||
style={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
padding: '6px 10px',
|
||
borderRadius: 999,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '11px',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
<span>{symbol}</span>
|
||
<span style={{ color: '#777777' }}>×</span>
|
||
</button>
|
||
))}
|
||
{watchlistSymbols.length === 0 && (
|
||
<div style={{ fontSize: '11px', color: '#888888', padding: '8px 2px' }}>
|
||
还没有股票,输入代码后回车添加
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<input
|
||
value={watchlistInputValue}
|
||
onChange={(e) => onWatchlistInputChange(e.target.value)}
|
||
onKeyDown={onWatchlistInputKeyDown}
|
||
placeholder="输入股票代码,回车添加"
|
||
style={{
|
||
flex: 1,
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}
|
||
/>
|
||
<button
|
||
onClick={onWatchlistAdd}
|
||
style={{
|
||
padding: '9px 12px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '11px',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
添加
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||
{watchlistSuggestions.map((symbol) => {
|
||
const active = watchlistSymbols.includes(symbol);
|
||
return (
|
||
<button
|
||
key={symbol}
|
||
onClick={() => onWatchlistSuggestionClick(symbol)}
|
||
disabled={active}
|
||
style={{
|
||
padding: '5px 8px',
|
||
borderRadius: 999,
|
||
border: '1px solid',
|
||
borderColor: active ? '#B6E3C5' : '#D0D7DE',
|
||
background: active ? '#ECFDF3' : '#FFFFFF',
|
||
color: active ? '#157347' : '#4A5568',
|
||
fontSize: '10px',
|
||
fontWeight: 700,
|
||
cursor: active ? 'default' : 'pointer'
|
||
}}
|
||
>
|
||
{symbol}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||
<button
|
||
onClick={onWatchlistRestoreCurrent}
|
||
style={{
|
||
padding: '8px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '11px',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
恢复当前
|
||
</button>
|
||
<button
|
||
onClick={onWatchlistRestoreDefault}
|
||
style={{
|
||
padding: '8px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '11px',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
恢复默认
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{
|
||
border: '1px solid #E5EAF1',
|
||
borderRadius: 12,
|
||
background: '#FCFDFE',
|
||
padding: 14,
|
||
display: 'grid',
|
||
gap: 12
|
||
}}>
|
||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>调度参数</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>调度模式</span>
|
||
<select
|
||
value={scheduleMode}
|
||
onChange={(e) => onScheduleModeChange(e.target.value)}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px'
|
||
}}
|
||
>
|
||
<option value="daily">每日定时</option>
|
||
<option value="intraday">盘中轮询</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>间隔(分钟)</span>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={intervalMinutes}
|
||
onChange={(e) => onIntervalMinutesChange(e.target.value)}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>每日定时时间 (NYSE)</span>
|
||
<input
|
||
type="time"
|
||
value={triggerTime}
|
||
onChange={(e) => onTriggerTimeChange(e.target.value)}
|
||
disabled={scheduleMode !== 'daily'}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: scheduleMode === 'daily' ? '#FFFFFF' : '#F3F4F6',
|
||
color: '#111111',
|
||
fontSize: '12px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}
|
||
/>
|
||
</label>
|
||
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>讨论轮数上限</span>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={maxCommCycles}
|
||
onChange={(e) => onMaxCommCyclesChange(e.target.value)}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}
|
||
/>
|
||
</label>
|
||
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>初始资金</span>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
step="1000"
|
||
value={initialCash}
|
||
onChange={(e) => onInitialCashChange(e.target.value)}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}
|
||
/>
|
||
</label>
|
||
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>保证金要求</span>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
step="0.01"
|
||
value={marginRequirement}
|
||
onChange={(e) => onMarginRequirementChange(e.target.value)}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}
|
||
/>
|
||
</label>
|
||
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 2 }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={enableMemory}
|
||
onChange={(e) => onEnableMemoryChange(e.target.checked)}
|
||
style={{
|
||
width: 16,
|
||
height: 16,
|
||
accentColor: '#0D47A1',
|
||
cursor: 'pointer'
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '11px', color: '#111111', fontWeight: 700 }}>启用长期记忆</span>
|
||
</label>
|
||
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>运行模式</span>
|
||
<select
|
||
value={mode}
|
||
onChange={(e) => onModeChange(e.target.value)}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px'
|
||
}}
|
||
>
|
||
<option value="live">实盘模式 (Live)</option>
|
||
<option value="backtest">回测模式 (Backtest)</option>
|
||
</select>
|
||
</label>
|
||
|
||
{mode === 'backtest' && (
|
||
<>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>回测开始日期</span>
|
||
<input
|
||
type="date"
|
||
value={startDate}
|
||
onChange={(e) => onStartDateChange(e.target.value)}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}
|
||
/>
|
||
</label>
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>回测结束日期</span>
|
||
<input
|
||
type="date"
|
||
value={endDate}
|
||
onChange={(e) => onEndDateChange(e.target.value)}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}
|
||
/>
|
||
</label>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
<label style={{ display: 'grid', gap: 4 }}>
|
||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>轮询间隔(秒)</span>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
max="300"
|
||
value={pollInterval}
|
||
onChange={(e) => onPollIntervalChange(e.target.value)}
|
||
style={{
|
||
padding: '9px 10px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '12px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}
|
||
/>
|
||
</label>
|
||
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 2 }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={enableMock}
|
||
onChange={(e) => onEnableMockChange(e.target.checked)}
|
||
style={{
|
||
width: 16,
|
||
height: 16,
|
||
accentColor: '#0D47A1',
|
||
cursor: 'pointer'
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '11px', color: '#111111', fontWeight: 700 }}>启用模拟数据 (Mock)</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div style={{
|
||
border: '1px solid #E5EAF1',
|
||
borderRadius: 12,
|
||
background: '#FCFDFE',
|
||
padding: 14,
|
||
display: 'grid',
|
||
gap: 10
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>操作</div>
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||
<button
|
||
onClick={onRestoreDefaults}
|
||
style={{
|
||
padding: '9px 12px',
|
||
borderRadius: 8,
|
||
border: '1px solid #D0D7DE',
|
||
background: '#FFFFFF',
|
||
color: '#111111',
|
||
fontSize: '11px',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
恢复默认
|
||
</button>
|
||
<button
|
||
onClick={onSave}
|
||
disabled={!isConnected || isSaving}
|
||
style={{
|
||
padding: '9px 14px',
|
||
borderRadius: 8,
|
||
border: '1px solid #1565C0',
|
||
background: isConnected && !isSaving ? '#0D47A1' : '#94A3B8',
|
||
color: '#FFFFFF',
|
||
fontSize: '11px',
|
||
fontWeight: 700,
|
||
letterSpacing: '0.4px',
|
||
cursor: isConnected && !isSaving ? 'pointer' : 'not-allowed'
|
||
}}
|
||
>
|
||
{isSaving ? '启动中' : '启动任务'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{feedback && (
|
||
<span style={{
|
||
color: feedback.type === 'success' ? '#00C853' : '#FF5252',
|
||
fontSize: '11px',
|
||
fontFamily: '"Courier New", monospace'
|
||
}}>
|
||
{feedback.text}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
), document.body)}
|
||
</div>
|
||
);
|
||
}
|