diff --git a/agent_os/backend/routes/runs.py b/agent_os/backend/routes/runs.py
index 8d8f9cdc..7c763c08 100644
--- a/agent_os/backend/routes/runs.py
+++ b/agent_os/backend/routes/runs.py
@@ -104,6 +104,27 @@ async def trigger_auto(
background_tasks.add_task(_run_and_store, run_id, engine.run_auto(run_id, params or {}))
return {"run_id": run_id, "status": "queued"}
+@router.delete("/portfolio-stage")
+async def reset_portfolio_stage(
+ params: Dict[str, Any],
+ user: dict = Depends(get_current_user),
+):
+ """Delete PM decision and execution result for a given date/portfolio_id.
+
+ After calling this, an auto run will re-run Phase 3 from scratch
+ (Phases 1 & 2 are skipped if their cached results still exist).
+ """
+ from tradingagents.portfolio.report_store import ReportStore
+ date = params.get("date")
+ portfolio_id = params.get("portfolio_id")
+ if not date or not portfolio_id:
+ raise HTTPException(status_code=422, detail="date and portfolio_id are required")
+ store = ReportStore()
+ deleted = store.clear_portfolio_stage(date, portfolio_id)
+ logger.info("reset_portfolio_stage date=%s portfolio=%s deleted=%s user=%s", date, portfolio_id, deleted, user["user_id"])
+ return {"deleted": deleted, "date": date, "portfolio_id": portfolio_id}
+
+
@router.get("/")
async def list_runs(user: dict = Depends(get_current_user)):
# Filter by user in production
diff --git a/agent_os/frontend/src/Dashboard.tsx b/agent_os/frontend/src/Dashboard.tsx
index 0f794c26..80317f14 100644
--- a/agent_os/frontend/src/Dashboard.tsx
+++ b/agent_os/frontend/src/Dashboard.tsx
@@ -35,7 +35,7 @@ import {
Collapse,
useToast,
} from '@chakra-ui/react';
-import { LayoutDashboard, Wallet, Settings, Terminal as TerminalIcon, ChevronRight, Eye, Search, BarChart3, Bot, ChevronDown, ChevronUp } from 'lucide-react';
+import { LayoutDashboard, Wallet, Settings, Terminal as TerminalIcon, ChevronRight, Eye, Search, BarChart3, Bot, ChevronDown, ChevronUp, Trash2 } from 'lucide-react';
import { MetricHeader } from './components/MetricHeader';
import { AgentGraph } from './components/AgentGraph';
import { PortfolioViewer } from './components/PortfolioViewer';
@@ -371,6 +371,27 @@ export const Dashboard: React.FC = () => {
}
};
+ const resetPortfolioStage = async () => {
+ if (!params.date || !params.portfolio_id) {
+ toast({ title: 'Date and Portfolio ID are required', status: 'warning', duration: 3000, isClosable: true, position: 'top' });
+ setShowParams(true);
+ return;
+ }
+ try {
+ const res = await axios.delete(`${API_BASE}/run/portfolio-stage`, { data: { date: params.date, portfolio_id: params.portfolio_id } });
+ const deleted: string[] = res.data.deleted;
+ toast({
+ title: deleted.length ? `Cleared: ${deleted.join(', ')}` : 'Nothing to clear — no decision files found',
+ status: deleted.length ? 'success' : 'info',
+ duration: 4000,
+ isClosable: true,
+ position: 'top',
+ });
+ } catch (err) {
+ toast({ title: 'Failed to reset portfolio stage', status: 'error', duration: 3000, isClosable: true, position: 'top' });
+ }
+ };
+
/** Open the full-screen event detail modal */
const openModal = useCallback((evt: AgentEvent) => {
setModalEvent(evt);
@@ -483,6 +504,19 @@ export const Dashboard: React.FC = () => {
);
})}
+
+ }
+ colorScheme="red"
+ variant="outline"
+ onClick={resetPortfolioStage}
+ isDisabled={isRunning}
+ >
+ Reset Decision
+
+
+
{status.toUpperCase()}
diff --git a/tradingagents/portfolio/report_store.py b/tradingagents/portfolio/report_store.py
index e40c32d8..9ee27401 100644
--- a/tradingagents/portfolio/report_store.py
+++ b/tradingagents/portfolio/report_store.py
@@ -303,6 +303,23 @@ class ReportStore:
path = self._portfolio_dir(date) / f"{portfolio_id}_execution_result.json"
return self._read_json(path)
+ def clear_portfolio_stage(self, date: str, portfolio_id: str) -> list[str]:
+ """Delete PM decision and execution result files for a given date/portfolio.
+
+ Returns a list of deleted file names so the caller can log what was removed.
+ """
+ targets = [
+ self._portfolio_dir(date) / f"{portfolio_id}_pm_decision.json",
+ self._portfolio_dir(date) / f"{portfolio_id}_pm_decision.md",
+ self._portfolio_dir(date) / f"{portfolio_id}_execution_result.json",
+ ]
+ deleted = []
+ for path in targets:
+ if path.exists():
+ path.unlink()
+ deleted.append(path.name)
+ return deleted
+
def list_pm_decisions(self, portfolio_id: str) -> list[Path]:
"""Return all saved PM decision JSON paths for portfolio_id, newest first.