feat: Add task launcher API with timestamp-based run directories

- Add POST /runtime/start, /stop, /restart APIs
- Run directories use timestamp format: YYYYMMDD_HHMMSS
- Add force stop support in TradingRuntimeManager
- Frontend: use REST API to start runtime instead of WebSocket
- Remove RuntimeView page from navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 17:14:22 +08:00
parent 3174734f26
commit 9ec4a8702d
4 changed files with 355 additions and 54 deletions

View File

@@ -5,6 +5,7 @@ import { AGENTS, INITIAL_TICKERS } from './config/constants';
// Services
import { ReadOnlyClient } from './services/websocket';
import { startRuntime } from './services/runtimeApi';
// Hooks
import { useFeedProcessor } from './hooks/useFeedProcessor';
@@ -17,7 +18,6 @@ import NetValueChart from './components/NetValueChart';
import StockLogo from './components/StockLogo';
import Header from './components/Header.jsx';
import RuntimeSettingsPanel from './components/RuntimeSettingsPanel.jsx';
import RuntimeView from './components/RuntimeView.jsx';
// Utils
import { formatNumber, formatTickerPrice } from './utils/formatters';
@@ -555,7 +555,7 @@ export default function LiveTradingApp() {
}
}, [enableMemoryDraft, initialCashDraft, intervalMinutesDraft, marginRequirementDraft, maxCommCyclesDraft, scheduleModeDraft, triggerTimeDraft]);
const handleLaunchConfigSave = useCallback(() => {
const handleLaunchConfigSave = useCallback(async () => {
const pendingTickers = parseWatchlistInput(watchlistInputValue);
const nextTickers = Array.from(new Set([...watchlistDraftSymbols, ...pendingTickers]));
if (nextTickers.length === 0) {
@@ -563,11 +563,6 @@ export default function LiveTradingApp() {
return;
}
if (!clientRef.current) {
setRuntimeConfigFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
const interval = Number(intervalMinutesDraft);
const maxCommCycles = Number(maxCommCyclesDraft);
const initialCash = Number(initialCashDraft);
@@ -596,26 +591,35 @@ export default function LiveTradingApp() {
setWatchlistDraftSymbols(nextTickers);
setWatchlistInputValue('');
const watchlistSuccess = clientRef.current.send({
type: 'update_watchlist',
tickers: nextTickers
});
try {
// Call API to start new runtime with timestamp-based run directory
const result = await startRuntime({
tickers: nextTickers,
schedule_mode: scheduleModeDraft,
interval_minutes: interval,
trigger_time: triggerTimeDraft,
max_comm_cycles: maxCommCycles,
initial_cash: initialCash,
margin_requirement: marginRequirement,
enable_memory: Boolean(enableMemoryDraft),
mode: serverMode || 'live'
});
const runtimeSuccess = clientRef.current.send({
type: 'update_runtime_config',
schedule_mode: scheduleModeDraft,
interval_minutes: interval,
trigger_time: triggerTimeDraft,
max_comm_cycles: maxCommCycles,
initial_cash: initialCash,
margin_requirement: marginRequirement,
enable_memory: Boolean(enableMemoryDraft)
});
if (!watchlistSuccess || !runtimeSuccess) {
setIsRuntimeConfigSaving(false);
setIsWatchlistSaving(false);
setRuntimeConfigFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
setIsRuntimeSettingsOpen(false);
setRuntimeConfigFeedback({
type: 'success',
text: `任务已启动: ${result.run_id}`
});
addSystemMessage(`新任务已启动: ${result.run_id}`);
} catch (error) {
setIsRuntimeConfigSaving(false);
setIsWatchlistSaving(false);
setRuntimeConfigFeedback({
type: 'error',
text: `启动失败: ${error.message}`
});
}
}, [
intervalMinutesDraft,
@@ -627,7 +631,9 @@ export default function LiveTradingApp() {
marginRequirementDraft,
enableMemoryDraft,
watchlistDraftSymbols,
watchlistInputValue
watchlistInputValue,
serverMode,
addSystemMessage
]);
const handleRuntimeDefaultsRestore = useCallback(() => {
@@ -2476,33 +2482,8 @@ export default function LiveTradingApp() {
>
统计
</button>
<button
className={`view-nav-btn ${currentView === 'runtime' ? 'active' : ''}`}
onClick={() => setCurrentView('runtime')}
>
运行态
</button>
</div>
{currentView === 'runtime' ? (
<div
style={{
position: 'absolute',
top: 40,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
minWidth: 0,
minHeight: 0
}}
>
<RuntimeView />
</div>
) : (
<div className={`view-slider-five ${
currentView === 'traders'
? 'show-traders'
@@ -2644,7 +2625,6 @@ export default function LiveTradingApp() {
</Suspense>
</div>
</div>
)}
</div>
</div>
</div>

View File

@@ -81,3 +81,40 @@ export function loadAllRuntimeState(onSuccess, onError) {
}
});
}
/**
* Start a new trading runtime with the given configuration.
* If a runtime is already running, it will be forcefully stopped first.
*/
export function startRuntime(config) {
return safeRequest('/runtime/start', {
method: 'POST',
body: JSON.stringify(config)
});
}
/**
* Stop the current running runtime.
*/
export function stopRuntime(force = true) {
return safeRequest(`/runtime/stop?force=${force}`, {
method: 'POST'
});
}
/**
* Restart the runtime with a new configuration.
*/
export function restartRuntime(config) {
return safeRequest('/runtime/restart', {
method: 'POST',
body: JSON.stringify(config)
});
}
/**
* Get information about the currently running runtime.
*/
export function fetchCurrentRuntime() {
return safeFetch('/runtime/current');
}