Add restore-mode task launch flow
This commit is contained in:
@@ -96,7 +96,40 @@ export default function LiveTradingApp() {
|
||||
setWorkspaceDraftContent,
|
||||
} = useAgentStore();
|
||||
|
||||
const { feed, processHistoricalFeed, processFeedEvent, addSystemMessage } = useFeedProcessor();
|
||||
const { feed, processHistoricalFeed, processFeedEvent, addSystemMessage, clearFeed } = useFeedProcessor();
|
||||
const resetRuntimeViewState = useCallback(() => {
|
||||
clearFeed();
|
||||
|
||||
useMarketStore.getState().setPriceHistoryByTicker({});
|
||||
useMarketStore.getState().setOhlcHistoryByTicker({});
|
||||
useMarketStore.getState().setHistorySourceByTicker({});
|
||||
useMarketStore.getState().setExplainEventsByTicker({});
|
||||
useMarketStore.getState().setNewsByTicker({});
|
||||
useMarketStore.getState().setInsiderTradesByTicker({});
|
||||
useMarketStore.getState().setTechnicalIndicatorsByTicker({});
|
||||
|
||||
usePortfolioStore.getState().setHoldings([]);
|
||||
usePortfolioStore.getState().setTrades([]);
|
||||
usePortfolioStore.getState().setStats(null);
|
||||
usePortfolioStore.getState().setLeaderboard([]);
|
||||
usePortfolioStore.getState().setPortfolioData({
|
||||
netValue: 10000,
|
||||
pnl: 0,
|
||||
equity: [],
|
||||
baseline: [],
|
||||
baseline_vw: [],
|
||||
momentum: [],
|
||||
strategies: [],
|
||||
equity_return: 0,
|
||||
baseline_return: 0,
|
||||
baseline_vw_return: 0,
|
||||
momentum_return: 0,
|
||||
});
|
||||
|
||||
useRuntimeStore.getState().setLastDayHistory([]);
|
||||
useUIStore.getState().setBubbles({});
|
||||
}, [clearFeed]);
|
||||
|
||||
const {
|
||||
clientRef,
|
||||
setRequestStockHistory,
|
||||
@@ -112,6 +145,7 @@ export default function LiveTradingApp() {
|
||||
clientRef,
|
||||
currentTickers: tickers,
|
||||
addSystemMessage,
|
||||
onRuntimeStarted: resetRuntimeViewState,
|
||||
});
|
||||
|
||||
const stockRequests = useStockDataRequests(clientRef, {
|
||||
@@ -367,6 +401,9 @@ export default function LiveTradingApp() {
|
||||
isWatchlistSaving={runtimeControls.isWatchlistSaving}
|
||||
runtimeConfigFeedback={runtimeControls.runtimeConfigFeedback}
|
||||
watchlistFeedback={runtimeControls.watchlistFeedback}
|
||||
launchModeDraft={runtimeControls.launchModeDraft}
|
||||
restoreRunIdDraft={runtimeControls.restoreRunIdDraft}
|
||||
runtimeHistoryRuns={runtimeControls.runtimeHistoryRuns}
|
||||
scheduleModeDraft={runtimeControls.scheduleModeDraft}
|
||||
intervalMinutesDraft={runtimeControls.intervalMinutesDraft}
|
||||
triggerTimeDraft={runtimeControls.triggerTimeDraft}
|
||||
@@ -382,6 +419,8 @@ export default function LiveTradingApp() {
|
||||
watchlistDraftSymbols={runtimeControls.watchlistDraftSymbols}
|
||||
watchlistInputValue={runtimeControls.watchlistInputValue}
|
||||
watchlistSuggestions={runtimeControls.watchlistSuggestions}
|
||||
onLaunchModeChange={runtimeControls.setLaunchModeDraft}
|
||||
onRestoreRunIdChange={runtimeControls.setRestoreRunIdDraft}
|
||||
onScheduleModeChange={runtimeControls.setScheduleModeDraft}
|
||||
onIntervalMinutesChange={runtimeControls.setIntervalMinutesDraft}
|
||||
onTriggerTimeChange={runtimeControls.setTriggerTimeDraft}
|
||||
|
||||
@@ -58,6 +58,9 @@ export default function AppShell({
|
||||
isWatchlistSaving,
|
||||
runtimeConfigFeedback,
|
||||
watchlistFeedback,
|
||||
launchModeDraft,
|
||||
restoreRunIdDraft,
|
||||
runtimeHistoryRuns,
|
||||
scheduleModeDraft,
|
||||
intervalMinutesDraft,
|
||||
triggerTimeDraft,
|
||||
@@ -73,6 +76,8 @@ export default function AppShell({
|
||||
watchlistDraftSymbols,
|
||||
watchlistInputValue,
|
||||
watchlistSuggestions,
|
||||
onLaunchModeChange,
|
||||
onRestoreRunIdChange,
|
||||
onScheduleModeChange,
|
||||
onIntervalMinutesChange,
|
||||
onTriggerTimeChange,
|
||||
@@ -300,6 +305,9 @@ export default function AppShell({
|
||||
isConnected={isConnected}
|
||||
isSaving={isRuntimeConfigSaving || isWatchlistSaving}
|
||||
feedback={runtimeConfigFeedback || watchlistFeedback}
|
||||
launchMode={launchModeDraft}
|
||||
restoreRunId={restoreRunIdDraft}
|
||||
runtimeHistoryRuns={runtimeHistoryRuns}
|
||||
scheduleMode={scheduleModeDraft}
|
||||
intervalMinutes={intervalMinutesDraft}
|
||||
triggerTime={triggerTimeDraft}
|
||||
@@ -317,6 +325,8 @@ export default function AppShell({
|
||||
watchlistSuggestions={watchlistSuggestions}
|
||||
onToggle={onRuntimeSettingsToggle}
|
||||
onClose={() => setIsRuntimeSettingsOpen(false)}
|
||||
onLaunchModeChange={onLaunchModeChange}
|
||||
onRestoreRunIdChange={onRestoreRunIdChange}
|
||||
onScheduleModeChange={onScheduleModeChange}
|
||||
onIntervalMinutesChange={onIntervalMinutesChange}
|
||||
onTriggerTimeChange={onTriggerTimeChange}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const formatHistorySummary = (run) => {
|
||||
const updatedAt = run?.updated_at ? String(run.updated_at).replace("T", " ").slice(0, 16) : "未知时间";
|
||||
const mode = run?.bootstrap?.mode ? String(run.bootstrap.mode).toUpperCase() : "LIVE";
|
||||
const tickers = Array.isArray(run?.bootstrap?.tickers) ? run.bootstrap.tickers.length : 0;
|
||||
const assetValue = Number(run?.total_asset_value ?? 0).toFixed(2);
|
||||
const trades = Number(run?.total_trades ?? 0);
|
||||
return `${run.run_id} · ${updatedAt} · ${mode} · ${tickers}标的 · ${trades}笔交易 · $${assetValue}`;
|
||||
};
|
||||
|
||||
export default function RuntimeSettingsPanel({
|
||||
showTrigger = true,
|
||||
isOpen,
|
||||
isConnected,
|
||||
isSaving,
|
||||
feedback,
|
||||
launchMode,
|
||||
restoreRunId,
|
||||
runtimeHistoryRuns,
|
||||
scheduleMode,
|
||||
intervalMinutes,
|
||||
triggerTime,
|
||||
@@ -25,6 +37,8 @@ export default function RuntimeSettingsPanel({
|
||||
onToggle,
|
||||
onClose,
|
||||
onScheduleModeChange,
|
||||
onLaunchModeChange,
|
||||
onRestoreRunIdChange,
|
||||
onIntervalMinutesChange,
|
||||
onTriggerTimeChange,
|
||||
onMaxCommCyclesChange,
|
||||
@@ -142,6 +156,75 @@ export default function RuntimeSettingsPanel({
|
||||
display: 'grid',
|
||||
gap: 12
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>启动形式</div>
|
||||
<label style={{ display: 'grid', gap: 4 }}>
|
||||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>任务模式</span>
|
||||
<select
|
||||
value={launchMode}
|
||||
onChange={(e) => onLaunchModeChange(e.target.value)}
|
||||
style={{
|
||||
padding: '9px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
<option value="fresh">重新启动</option>
|
||||
<option value="restore">从历史任务恢复</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{launchMode === 'restore' && (
|
||||
<>
|
||||
<label style={{ display: 'grid', gap: 4 }}>
|
||||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>历史任务</span>
|
||||
<select
|
||||
value={restoreRunId}
|
||||
onChange={(e) => onRestoreRunIdChange(e.target.value)}
|
||||
style={{
|
||||
padding: '9px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
<option value="">请选择历史任务</option>
|
||||
{runtimeHistoryRuns.map((run) => (
|
||||
<option key={run.run_id} value={run.run_id}>
|
||||
{formatHistorySummary(run)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#6B7280',
|
||||
lineHeight: 1.6,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 8,
|
||||
background: '#FFFFFF',
|
||||
border: '1px dashed #D0D7DE'
|
||||
}}>
|
||||
恢复启动会从所选历史任务复制运行状态、组合、交易记录和 Agent 工作区资产,并以新的任务 ID 继续运行。
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{launchMode === 'fresh' && (
|
||||
<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={{
|
||||
@@ -272,16 +355,18 @@ export default function RuntimeSettingsPanel({
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 12
|
||||
}}>
|
||||
{launchMode === 'fresh' && (
|
||||
<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 }}>
|
||||
@@ -510,7 +595,8 @@ export default function RuntimeSettingsPanel({
|
||||
/>
|
||||
<span style={{ fontSize: '11px', color: '#111111', fontWeight: 700 }}>启用模拟数据 (Mock)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { INITIAL_TICKERS } from "../config/constants";
|
||||
import { startRuntime } from "../services/runtimeApi";
|
||||
import { fetchRuntimeHistory, startRuntime } from "../services/runtimeApi";
|
||||
import {
|
||||
buildRuntimeSummaryLabel,
|
||||
normalizeTickerSymbols,
|
||||
@@ -19,7 +19,7 @@ const DEFAULT_MARGIN_REQUIREMENT = "0";
|
||||
const DEFAULT_MODE = "live";
|
||||
const DEFAULT_POLL_INTERVAL = "10";
|
||||
|
||||
export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage }) {
|
||||
export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage, onRuntimeStarted }) {
|
||||
const {
|
||||
runtimeConfig,
|
||||
setRuntimeConfig,
|
||||
@@ -35,6 +35,12 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
setWatchlistFeedback,
|
||||
isWatchlistSaving,
|
||||
setIsWatchlistSaving,
|
||||
launchModeDraft,
|
||||
setLaunchModeDraft,
|
||||
restoreRunIdDraft,
|
||||
setRestoreRunIdDraft,
|
||||
runtimeHistoryRuns,
|
||||
setRuntimeHistoryRuns,
|
||||
scheduleModeDraft,
|
||||
setScheduleModeDraft,
|
||||
intervalMinutesDraft,
|
||||
@@ -152,6 +158,32 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
setTriggerTimeDraft
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRuntimeSettingsOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void fetchRuntimeHistory(20)
|
||||
.then((payload) => {
|
||||
if (cancelled) return;
|
||||
const runs = Array.isArray(payload?.runs) ? payload.runs : [];
|
||||
setRuntimeHistoryRuns(runs);
|
||||
if (!restoreRunIdDraft && runs.length > 0) {
|
||||
setRestoreRunIdDraft(runs[0].run_id);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setRuntimeHistoryRuns([]);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isRuntimeSettingsOpen, restoreRunIdDraft, setRestoreRunIdDraft, setRuntimeHistoryRuns]);
|
||||
|
||||
const commitWatchlistInput = useCallback((value) => {
|
||||
const parsed = parseWatchlistInput(value);
|
||||
if (parsed.length === 0) {
|
||||
@@ -340,6 +372,10 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
setRuntimeConfigFeedback({ type: "error", text: "保证金要求不能为负数" });
|
||||
return;
|
||||
}
|
||||
if (launchModeDraft === "restore" && !restoreRunIdDraft) {
|
||||
setRuntimeConfigFeedback({ type: "error", text: "请选择一个历史任务用于恢复启动" });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRuntimeConfigSaving(true);
|
||||
setIsWatchlistSaving(true);
|
||||
@@ -350,6 +386,8 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
|
||||
try {
|
||||
const result = await startRuntime({
|
||||
launch_mode: launchModeDraft,
|
||||
restore_run_id: launchModeDraft === "restore" ? restoreRunIdDraft : null,
|
||||
tickers: nextTickers,
|
||||
schedule_mode: scheduleModeDraft,
|
||||
interval_minutes: interval,
|
||||
@@ -373,6 +411,7 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
text: `任务已启动: ${result.run_id}`
|
||||
});
|
||||
addSystemMessage(`新任务已启动: ${result.run_id}`);
|
||||
onRuntimeStarted?.(result);
|
||||
} catch (error) {
|
||||
setIsRuntimeConfigSaving(false);
|
||||
setIsWatchlistSaving(false);
|
||||
@@ -389,10 +428,12 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
endDateDraft,
|
||||
initialCashDraft,
|
||||
intervalMinutesDraft,
|
||||
launchModeDraft,
|
||||
marginRequirementDraft,
|
||||
maxCommCyclesDraft,
|
||||
modeDraft,
|
||||
pollIntervalDraft,
|
||||
restoreRunIdDraft,
|
||||
scheduleModeDraft,
|
||||
setIsRuntimeConfigSaving,
|
||||
setIsRuntimeSettingsOpen,
|
||||
@@ -402,6 +443,7 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
setWatchlistFeedback,
|
||||
setWatchlistInputValue,
|
||||
startDateDraft,
|
||||
onRuntimeStarted,
|
||||
triggerTimeDraft,
|
||||
watchlistDraftSymbols,
|
||||
watchlistInputValue
|
||||
@@ -415,6 +457,8 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
setInitialCashDraft(DEFAULT_INITIAL_CASH);
|
||||
setMarginRequirementDraft(DEFAULT_MARGIN_REQUIREMENT);
|
||||
setEnableMemoryDraft(false);
|
||||
setLaunchModeDraft("fresh");
|
||||
setRestoreRunIdDraft("");
|
||||
setModeDraft(DEFAULT_MODE);
|
||||
setPollIntervalDraft(DEFAULT_POLL_INTERVAL);
|
||||
setStartDateDraft("");
|
||||
@@ -427,10 +471,12 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
setEndDateDraft,
|
||||
setInitialCashDraft,
|
||||
setIntervalMinutesDraft,
|
||||
setLaunchModeDraft,
|
||||
setMarginRequirementDraft,
|
||||
setMaxCommCyclesDraft,
|
||||
setModeDraft,
|
||||
setPollIntervalDraft,
|
||||
setRestoreRunIdDraft,
|
||||
setRuntimeConfigFeedback,
|
||||
setScheduleModeDraft,
|
||||
setStartDateDraft,
|
||||
@@ -482,6 +528,9 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
watchlistInputValue,
|
||||
watchlistFeedback,
|
||||
isWatchlistSaving,
|
||||
launchModeDraft,
|
||||
restoreRunIdDraft,
|
||||
runtimeHistoryRuns,
|
||||
scheduleModeDraft,
|
||||
intervalMinutesDraft,
|
||||
triggerTimeDraft,
|
||||
@@ -527,6 +576,8 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
|
||||
setInitialCashDraft,
|
||||
setMarginRequirementDraft,
|
||||
setEnableMemoryDraft,
|
||||
setLaunchModeDraft,
|
||||
setRestoreRunIdDraft,
|
||||
setModeDraft,
|
||||
setPollIntervalDraft,
|
||||
setStartDateDraft,
|
||||
|
||||
@@ -38,6 +38,10 @@ export function fetchRuntimeEvents() {
|
||||
return safeFetch(RUNTIME_API_BASE, '/events');
|
||||
}
|
||||
|
||||
export function fetchRuntimeHistory(limit = 20) {
|
||||
return safeFetch(RUNTIME_API_BASE, `/history?limit=${limit}`);
|
||||
}
|
||||
|
||||
export function fetchPendingApprovals() {
|
||||
return safeFetch(CONTROL_API_BASE, '/guard/pending');
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ export const useRuntimeStore = create((set) => ({
|
||||
setIsRuntimeSettingsOpen: (isRuntimeSettingsOpen) => set((state) => ({ isRuntimeSettingsOpen: resolveValue(isRuntimeSettingsOpen, state.isRuntimeSettingsOpen) })),
|
||||
|
||||
// Runtime config drafts
|
||||
launchModeDraft: 'fresh',
|
||||
restoreRunIdDraft: '',
|
||||
runtimeHistoryRuns: [],
|
||||
scheduleModeDraft: 'daily',
|
||||
intervalMinutesDraft: '60',
|
||||
triggerTimeDraft: 'now',
|
||||
@@ -73,6 +76,9 @@ export const useRuntimeStore = create((set) => ({
|
||||
startDateDraft: '',
|
||||
endDateDraft: '',
|
||||
enableMockDraft: false,
|
||||
setLaunchModeDraft: (launchModeDraft) => set((state) => ({ launchModeDraft: resolveValue(launchModeDraft, state.launchModeDraft) })),
|
||||
setRestoreRunIdDraft: (restoreRunIdDraft) => set((state) => ({ restoreRunIdDraft: resolveValue(restoreRunIdDraft, state.restoreRunIdDraft) })),
|
||||
setRuntimeHistoryRuns: (runtimeHistoryRuns) => set((state) => ({ runtimeHistoryRuns: resolveValue(runtimeHistoryRuns, state.runtimeHistoryRuns) })),
|
||||
setScheduleModeDraft: (scheduleModeDraft) => set((state) => ({ scheduleModeDraft: resolveValue(scheduleModeDraft, state.scheduleModeDraft) })),
|
||||
setIntervalMinutesDraft: (intervalMinutesDraft) => set((state) => ({ intervalMinutesDraft: resolveValue(intervalMinutesDraft, state.intervalMinutesDraft) })),
|
||||
setTriggerTimeDraft: (triggerTimeDraft) => set((state) => ({ triggerTimeDraft: resolveValue(triggerTimeDraft, state.triggerTimeDraft) })),
|
||||
|
||||
Reference in New Issue
Block a user