diff --git a/web/src/tabs/PositionsTab.dom.test.tsx b/web/src/tabs/PositionsTab.dom.test.tsx
index 20ae6b0..0dbba2e 100644
--- a/web/src/tabs/PositionsTab.dom.test.tsx
+++ b/web/src/tabs/PositionsTab.dom.test.tsx
@@ -386,7 +386,7 @@ describe('PositionsTab DOM behavior', () => {
render();
await user.click(await screen.findByRole('button', { name: 'High Risk Scalper' }));
- const manageButtons = await screen.findAllByRole('button', { name: /Open Plan|Manage in Plans/ });
+ const manageButtons = await screen.findAllByRole('button', { name: 'Open Plan' });
await user.click(manageButtons[0]);
expect(onManageHolding).toHaveBeenCalledWith(expect.objectContaining({
@@ -394,7 +394,46 @@ describe('PositionsTab DOM behavior', () => {
profileId: 'p1',
tradeId: 'TRD-POS-1',
planEntryId: 'simple-setup-1'
- }));
+ }), 'open-plan');
+ });
+
+ it('exposes a create-exit-plan action for unmanaged live holdings', async () => {
+ const now = Date.now();
+ fetchPositionsBootstrapMock.mockResolvedValue({
+ entries: [],
+ orders: [{
+ id: 'entry-order',
+ order_id: 'entry-order',
+ profile_id: 'p1',
+ symbol: 'BTC/USDT',
+ type: 'Market',
+ side: 'BUY',
+ qty: 1,
+ price: 100,
+ status: 'filled',
+ timestamp: now - 1_000,
+ created_at: new Date(now - 1_000).toISOString(),
+ trade_id: 'TRD-POS-1',
+ action: 'ENTRY',
+ source: 'BOT'
+ }],
+ historyTradeKeys: [],
+ profiles: [{ id: 'p1', name: 'High Risk Scalper' }]
+ });
+
+ const onManageHolding = vi.fn();
+ const user = userEvent.setup();
+ render();
+
+ await user.click(await screen.findByRole('button', { name: 'High Risk Scalper' }));
+ const manageButtons = await screen.findAllByRole('button', { name: 'Create Exit Plan' });
+ await user.click(manageButtons[0]);
+
+ expect(onManageHolding).toHaveBeenCalledWith(expect.objectContaining({
+ symbol: 'BTC/USDT',
+ profileId: 'p1',
+ tradeId: 'TRD-POS-1',
+ }), 'create-exit-plan');
});
it('handles non-admin bootstrap failures with empty-state fallback', async () => {
diff --git a/web/src/tabs/PositionsTab.tsx b/web/src/tabs/PositionsTab.tsx
index 1b1f8f9..e54ef8e 100644
--- a/web/src/tabs/PositionsTab.tsx
+++ b/web/src/tabs/PositionsTab.tsx
@@ -10,7 +10,7 @@ import { fetchPositionsBootstrap } from '../lib/positionsApi';
interface PositionsTabProps {
botState: BotState;
- onManageHolding?: (position: HybridPosition) => void;
+ onManageHolding?: (position: HybridPosition, action: 'open-plan' | 'create-exit-plan') => void;
}
interface HybridPosition {
@@ -1453,10 +1453,10 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
{pos.source === 'BOT' && pos.profileId && pos.tradeId && onManageHolding && (
)}
{pos.source === 'BOT' && (
diff --git a/web/src/views/PortfolioView.tsx b/web/src/views/PortfolioView.tsx
index 37b80ff..5c03da8 100644
--- a/web/src/views/PortfolioView.tsx
+++ b/web/src/views/PortfolioView.tsx
@@ -36,14 +36,14 @@ export function PortfolioView() {
{tab === 'Positions & Orders' && (
{
- const params = position.planEntryId
+ onManageHolding={(position, action) => {
+ const params = action === 'open-plan' && position.planEntryId
? new URLSearchParams({ setupId: position.planEntryId })
: new URLSearchParams({
mode: 'sell',
symbol: position.symbol,
});
- if (!position.planEntryId && position.tradeId) params.set('tradeId', position.tradeId);
+ if (action !== 'open-plan' && position.tradeId) params.set('tradeId', position.tradeId);
navigate(`/simple?${params.toString()}`);
}}
/>
diff --git a/web/src/views/SimpleView.tsx b/web/src/views/SimpleView.tsx
index ce3993c..c77870f 100644
--- a/web/src/views/SimpleView.tsx
+++ b/web/src/views/SimpleView.tsx
@@ -631,9 +631,10 @@ export function SimpleView() {
const [selectedHoldingTradeId, setSelectedHoldingTradeId] = useState(null);
const [focusedSetupId, setFocusedSetupId] = useState(null);
const marketPriceRequestSymbolRef = useRef('');
- const consumedPrefillRef = useRef(false);
- const consumedSetupFocusRef = useRef(false);
+ const consumedPrefillKeyRef = useRef('');
+ const consumedSetupFocusKeyRef = useRef('');
const setupCardRefs = useRef>({});
+ const focusedSetupTimerRef = useRef(null);
const normalizedSymbol = draft.symbol.trim().toUpperCase();
const symbolState = botState?.symbols && typeof botState.symbols === 'object' ? botState.symbols : {};
@@ -804,10 +805,11 @@ export function SimpleView() {
}, [draft.side, selectedSellHolding, availableSellHoldings]);
useEffect(() => {
- if (consumedPrefillRef.current) return;
const requestedMode = String(searchParams.get('mode') || '').trim().toLowerCase();
const requestedSymbol = String(searchParams.get('symbol') || '').trim().toUpperCase();
const requestedTradeId = String(searchParams.get('tradeId') || '').trim();
+ const prefillKey = `${requestedMode}|${requestedSymbol}|${requestedTradeId}`;
+ if (!requestedMode || consumedPrefillKeyRef.current === prefillKey) return;
if (requestedMode !== 'sell') return;
if (availableSellHoldings.length === 0) return;
@@ -824,18 +826,18 @@ export function SimpleView() {
setMessage(`Loaded ${selected.symbol} from Portfolio. Configure the profit target and save the plan.`);
setError(null);
}
- consumedPrefillRef.current = true;
+ consumedPrefillKeyRef.current = prefillKey;
setSearchParams({}, { replace: true });
}, [availableSellHoldings, searchParams, setSearchParams]);
useEffect(() => {
- if (consumedSetupFocusRef.current) return;
const requestedSetupId = String(searchParams.get('setupId') || '').trim();
+ if (!requestedSetupId || consumedSetupFocusKeyRef.current === requestedSetupId) return;
if (!requestedSetupId) return;
if (savedSetups.length === 0) return;
const targetEntry = savedSetups.find((entry) => String(entry.stock_instance_id || '') === requestedSetupId) || null;
- consumedSetupFocusRef.current = true;
+ consumedSetupFocusKeyRef.current = requestedSetupId;
setSearchParams({}, { replace: true });
if (!targetEntry) return;
@@ -843,12 +845,26 @@ export function SimpleView() {
setFocusedSetupId(requestedSetupId);
setMessage(`Focused saved plan for ${targetEntry.symbol}.`);
setError(null);
- window.setTimeout(() => setFocusedSetupId((prev) => (prev === requestedSetupId ? null : prev)), 2200);
+ if (focusedSetupTimerRef.current !== null) {
+ window.clearTimeout(focusedSetupTimerRef.current);
+ }
+ focusedSetupTimerRef.current = window.setTimeout(() => {
+ setFocusedSetupId((prev) => (prev === requestedSetupId ? null : prev));
+ focusedSetupTimerRef.current = null;
+ }, 2200);
window.requestAnimationFrame(() => {
setupCardRefs.current[requestedSetupId]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}, [savedSetups, searchParams, setSearchParams]);
+ useEffect(() => {
+ return () => {
+ if (focusedSetupTimerRef.current !== null) {
+ window.clearTimeout(focusedSetupTimerRef.current);
+ }
+ };
+ }, []);
+
async function copyIdentifier(kind: 'trade' | 'order', value: string | null | undefined) {
if (!value) return;
try {
@@ -960,6 +976,7 @@ export function SimpleView() {
await refreshSetupList();
setEditingSetupId(null);
+ setSelectedHoldingTradeId(null);
setMarketPriceSource(draft.currentMarketPrice ? marketPriceSource : null);
setDraft({
...DEFAULT_DRAFT,
@@ -974,6 +991,7 @@ export function SimpleView() {
function handleEdit(entry: ManualEntryPayload) {
setEditingSetupId(String(entry.stock_instance_id || ''));
+ setSelectedHoldingTradeId(String(entry.linked_trade_id || '').trim() || null);
setMarketPriceSource(inferMarketPriceSourceFromEntry(entry));
setDraft(buildDraftFromEntry(entry));
setMessage(null);
@@ -986,6 +1004,7 @@ export function SimpleView() {
await deleteManualEntry(entryId);
if (editingSetupId === entryId) {
setEditingSetupId(null);
+ setSelectedHoldingTradeId(null);
setMarketPriceSource(null);
setDraft(DEFAULT_DRAFT);
}
@@ -1149,6 +1168,9 @@ export function SimpleView() {
value={draft.symbol}
onChange={(e) => {
setMarketPriceSource(null);
+ if (draft.side === 'sell') {
+ setSelectedHoldingTradeId(null);
+ }
setDraft((prev) => ({
...prev,
symbol: e.target.value.toUpperCase(),
@@ -1179,6 +1201,9 @@ export function SimpleView() {
type="button"
onClick={() => {
setMarketPriceSource(null);
+ if (draft.side === 'sell') {
+ setSelectedHoldingTradeId(null);
+ }
setDraft((prev) => ({
...prev,
symbol,