Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ dist-ssr
!lib
.react-router
.vite

.claude
15 changes: 15 additions & 0 deletions frontend/src/api/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ export const useStopStrategy = () => {
});
};

export const useDeleteStrategy = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (strategyId: string) =>
apiClient.delete<ApiResponse<null>>(
`/strategies/delete?id=${strategyId}`,
),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: API_QUERY_KEYS.STRATEGY.strategyList,
});
},
});
};

export const useGetStrategyPrompts = () => {
return useQuery({
queryKey: API_QUERY_KEYS.STRATEGY.strategyPrompts,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Plus } from "lucide-react";
import { type FC, useEffect, useState } from "react";
import {
useDeleteStrategy,
useGetStrategyHoldings,
useGetStrategyList,
useGetStrategyPriceCurve,
Expand Down Expand Up @@ -51,6 +52,7 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
);

const { mutateAsync: stopStrategy } = useStopStrategy();
const { mutateAsync: deleteStrategy } = useDeleteStrategy();

useEffect(() => {
if (strategies.length === 0 || selectedStrategy) return;
Expand All @@ -73,6 +75,9 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
onStrategyStop={async (strategyId) =>
await stopStrategy(strategyId)
}
onStrategyDelete={async (strategyId) =>
await deleteStrategy(strategyId)
}
/>
) : (
<div className="flex flex-1 flex-col items-center justify-center gap-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Plus, TrendingUp } from "lucide-react";
import { type FC, memo } from "react";
import { StrategyStatus } from "@/assets/svg";
import { DeleteStrategy, StrategyStatus } from "@/assets/svg";
import {
AlertDialog,
AlertDialogAction,
Expand All @@ -26,20 +26,23 @@ interface TradeStrategyCardProps {
isSelected?: boolean;
onClick?: () => void;
onStop?: () => void;
onDelete?: () => void;
}

interface TradeStrategyGroupProps {
strategies: Strategy[];
selectedStrategy?: Strategy | null;
onStrategySelect?: (strategy: Strategy) => void;
onStrategyStop?: (strategyId: string) => void;
onStrategyDelete?: (strategyId: string) => void;
}

const TradeStrategyCard: FC<TradeStrategyCardProps> = ({
strategy,
isSelected = false,
onClick,
onStop,
onDelete,
}) => {
const stockColors = useStockColors();
const changeType = getChangeType(strategy.unrealized_pnl_pct);
Expand Down Expand Up @@ -88,39 +91,68 @@ const TradeStrategyCard: FC<TradeStrategyCardProps> = ({
</div>

{/* Status Badge */}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
disabled={strategy.status === "stopped"}
size="sm"
className="flex items-center gap-2.5 rounded-md px-2.5 py-1"
>
{strategy.status === "running" && (
<SvgIcon name={StrategyStatus} className="size-4" />
)}
<p className="font-medium text-gray-700 text-sm">
{strategy.status === "running" ? "Running" : "Stopped"}
</p>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Stop Trading Strategy?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to stop the strategy "
{strategy.strategy_name}"? <br /> This action will halt all
trading activities for this strategy.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onStop}>
Confirm Stop
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<div className="flex items-center gap-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
disabled={strategy.status === "stopped"}
size="sm"
className="flex items-center gap-2.5 rounded-md px-2.5 py-1"
>
{strategy.status === "running" && (
<SvgIcon name={StrategyStatus} className="size-4" />
)}
<p className="font-medium text-gray-700 text-sm">
{strategy.status === "running" ? "Running" : "Stopped"}
</p>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Stop Trading Strategy?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to stop the strategy "
{strategy.strategy_name}"? <br /> This action will halt all
trading activities for this strategy.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onStop}>
Confirm Stop
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="flex items-center rounded-md"
>
<SvgIcon name={DeleteStrategy} className="size-6" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Strategy?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete the strategy "
{strategy.strategy_name}"?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onDelete}>
Confirm Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</div>
);
Expand All @@ -131,6 +163,7 @@ const TradeStrategyGroup: FC<TradeStrategyGroupProps> = ({
selectedStrategy,
onStrategySelect,
onStrategyStop,
onStrategyDelete,
}) => {
const hasStrategies = strategies.length > 0;

Expand All @@ -148,6 +181,7 @@ const TradeStrategyGroup: FC<TradeStrategyGroupProps> = ({
}
onClick={() => onStrategySelect?.(strategy)}
onStop={() => onStrategyStop?.(strategy.strategy_id)}
onDelete={() => onStrategyDelete?.(strategy.strategy_id)}
/>
))}
</div>
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/assets/svg/delete-strategy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/assets/svg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { default as AutoTrade } from "./auto-trade.svg";
export { default as BookOpen } from "./book-open.svg";
export { default as ChartBarVertical } from "./char-bar-vertical.svg";
export { default as Conversation } from "./conversation.svg";
export { default as DeleteStrategy } from "./delete-strategy.svg";
export { default as House } from "./house.svg";
export { default as Logo } from "./logo.svg";
export { default as NewsPush } from "./news-push.svg";
Expand Down
39 changes: 38 additions & 1 deletion python/valuecell/server/api/routers/strategy_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import os

from fastapi import APIRouter, Depends
# New imports for delete endpoint
from fastapi import APIRouter, Depends, HTTPException, Query
from loguru import logger
from sqlalchemy.orm import Session

Expand All @@ -16,6 +17,7 @@
from valuecell.config.loader import get_config_loader
from valuecell.core.coordinate.orchestrator import AgentOrchestrator
from valuecell.core.types import CommonResponseEvent, UserInput, UserInputMetadata
from valuecell.server.api.schemas.base import SuccessResponse
from valuecell.server.db.connection import get_db
from valuecell.server.db.repositories import get_strategy_repository
from valuecell.utils.uuid import generate_conversation_id, generate_uuid
Expand Down Expand Up @@ -278,4 +280,39 @@ async def create_strategy_agent(
strategy_id=fallback_strategy_id, status=StrategyStatus.ERROR
)

@router.delete("/delete")
async def delete_strategy_agent(
id: str = Query(..., description="Strategy ID"),
cascade: bool = Query(
True, description="Delete related records (holdings/details/portfolio)"
),
db: Session = Depends(get_db),
):
"""Delete a strategy created by StrategyAgent.

- Validates the strategy exists.
- Optionally cascades deletion to holdings, portfolio snapshots, and details.
- Returns a success response when completed.
"""
try:
repo = get_strategy_repository(db_session=db)
strategy = repo.get_strategy_by_strategy_id(id)
if not strategy:
raise HTTPException(status_code=404, detail="Strategy not found")

ok = repo.delete_strategy(id, cascade=cascade)
if not ok:
raise HTTPException(status_code=500, detail="Failed to delete strategy")

return SuccessResponse.create(
data={"strategy_id": id},
msg=f"Strategy '{id}' deleted successfully",
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Error deleting strategy: {str(e)}"
)

return router
42 changes: 42 additions & 0 deletions python/valuecell/server/db/repositories/strategy_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,48 @@ def get_prompt_by_id(self, prompt_id: str) -> Optional[StrategyPrompt]:
if not self.db_session:
session.close()

def delete_strategy(self, strategy_id: str, cascade: bool = True) -> bool:
"""Delete a strategy by strategy_id.

If cascade=True, remove associated holdings, portfolio snapshots,
and detail records for the strategy before deleting the strategy row.
Returns True on success, False if the strategy does not exist or on error.
"""
session = self._get_session()
try:
# Ensure the strategy exists
strategy = (
session.query(Strategy)
.filter(Strategy.strategy_id == strategy_id)
.first()
)
if not strategy:
return False

if cascade:
session.query(StrategyHolding).filter(
StrategyHolding.strategy_id == strategy_id
).delete(synchronize_session=False)
session.query(StrategyPortfolioView).filter(
StrategyPortfolioView.strategy_id == strategy_id
).delete(synchronize_session=False)
session.query(StrategyDetail).filter(
StrategyDetail.strategy_id == strategy_id
).delete(synchronize_session=False)

session.query(Strategy).filter(Strategy.strategy_id == strategy_id).delete(
synchronize_session=False
)

session.commit()
return True
except Exception:
session.rollback()
return False
finally:
if not self.db_session:
session.close()


# Global repository instance
_strategy_repository: Optional[StrategyRepository] = None
Expand Down