>({});
@@ -1433,41 +1434,51 @@ export const PositionsTab = ({ botState }: PositionsTabProps) => {
-
)}
-
- {pos.source === 'BOT' && (
-
- )}
- |
+
+
+ {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 f79b18f..6672e19 100644
--- a/web/src/views/PortfolioView.tsx
+++ b/web/src/views/PortfolioView.tsx
@@ -1,4 +1,5 @@
import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import { useAppContext } from '../context/AppContext';
import { PositionsTab } from '../tabs/PositionsTab';
import { HistoryTab } from '../tabs/HistoryTab';
@@ -9,6 +10,7 @@ type Tab = typeof TABS[number];
export function PortfolioView() {
const { botState } = useAppContext();
+ const navigate = useNavigate();
const [tab, setTab] = useState('Positions & Orders');
return (
@@ -31,7 +33,19 @@ export function PortfolioView() {
))}
- {tab === 'Positions & Orders' && }
+ {tab === 'Positions & Orders' && (
+ {
+ const params = new URLSearchParams({
+ mode: 'sell',
+ symbol: position.symbol,
+ });
+ if (position.tradeId) params.set('tradeId', position.tradeId);
+ navigate(`/simple?${params.toString()}`);
+ }}
+ />
+ )}
{tab === 'Trade History' && }
);
diff --git a/web/src/views/SimpleView.test.ts b/web/src/views/SimpleView.test.ts
index fcf15fb..a92df99 100644
--- a/web/src/views/SimpleView.test.ts
+++ b/web/src/views/SimpleView.test.ts
@@ -205,6 +205,6 @@ describe('SimpleView helpers', () => {
profitValue: '6',
notes: '',
},
- })).toThrow('Sell setups require an existing Simple holding for this symbol.');
+ })).toThrow('Sell setups require an existing holding for this symbol.');
});
});
diff --git a/web/src/views/SimpleView.tsx b/web/src/views/SimpleView.tsx
index b26f651..1f84e56 100644
--- a/web/src/views/SimpleView.tsx
+++ b/web/src/views/SimpleView.tsx
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import type { FormEvent } from 'react';
import { Pencil, RefreshCw, Trash2 } from 'lucide-react';
+import { useSearchParams } from 'react-router-dom';
import { useAppContext } from '../context/AppContext';
import { fetchChartBars, fetchResearchProfile } from '../lib/marketApi';
import {
@@ -201,7 +202,7 @@ export function buildSimpleSetupPayload(input: {
const automationState = normalizeAutomationState(existingEntry?.automation_state, existingEntry || { status: side === 'buy' ? 'simple_armed_buy' : 'simple_armed_sell' } as ManualEntryPayload);
if (side === 'sell' && !holding) {
- throw new Error('Sell setups require an existing Simple holding for this symbol.');
+ throw new Error('Sell setups require an existing holding for this symbol.');
}
if (side === 'buy' && parseNonNegativeNumber(input.draft.dropValue) === null) {
@@ -280,7 +281,7 @@ function buildPreviewText(draft: SimpleSetupDraft, holding: SimpleHolding | null
}
if (!holding) {
- return `Sell ${symbol} only works when a Simple holding already exists.`;
+ return `Sell ${symbol} only works when an eligible existing holding is available.`;
}
const profitTargetPrice = computeProfitTargetPrice(holding.entryPrice, draft.profitMode, draft.profitValue);
@@ -616,6 +617,7 @@ function describeSavedSetup(entry: ManualEntryPayload): string {
export function SimpleView() {
const { botState } = useAppContext();
+ const [searchParams, setSearchParams] = useSearchParams();
const [profiles, setProfiles] = useState([]);
const [savedSetups, setSavedSetups] = useState([]);
const [editingSetupId, setEditingSetupId] = useState(null);
@@ -626,7 +628,9 @@ export function SimpleView() {
const [copiedKey, setCopiedKey] = useState(null);
const [message, setMessage] = useState(null);
const [error, setError] = useState(null);
+ const [selectedHoldingTradeId, setSelectedHoldingTradeId] = useState(null);
const marketPriceRequestSymbolRef = useRef('');
+ const consumedPrefillRef = useRef(false);
const normalizedSymbol = draft.symbol.trim().toUpperCase();
const symbolState = botState?.symbols && typeof botState.symbols === 'object' ? botState.symbols : {};
@@ -651,6 +655,24 @@ export function SimpleView() {
.filter((position) => position.symbol && position.size > 0 && position.entryPrice > 0);
}, [botState?.positions, simpleAutoProfile?.id]);
+ const availableSellHoldings = useMemo(() => {
+ const positions = normalizeRuntimeArray(botState?.positions);
+ return positions
+ .map((position) => ({
+ symbol: String(position.symbol || '').trim().toUpperCase(),
+ size: Number(position.size || 0),
+ entryPrice: Number(position.entryPrice || 0),
+ profileId: position.profileId,
+ tradeId: position.tradeId,
+ }))
+ .filter((position) => position.symbol && position.size > 0 && position.entryPrice > 0 && position.profileId && position.tradeId)
+ .sort((left, right) => {
+ const bySymbol = left.symbol.localeCompare(right.symbol);
+ if (bySymbol !== 0) return bySymbol;
+ return String(left.tradeId || '').localeCompare(String(right.tradeId || ''));
+ });
+ }, [botState?.positions]);
+
const runtimeOrders = useMemo(() => {
const orders = normalizeRuntimeArray>(botState?.orders);
return orders.filter((order) => {
@@ -663,21 +685,19 @@ export function SimpleView() {
[botState?.operationalEvents],
);
- const matchingHolding = useMemo(
- () => simpleHoldings.find((holding) => holding.symbol === normalizedSymbol) || null,
- [simpleHoldings, normalizedSymbol],
- );
- const availableSellHoldings = useMemo(
- () => [...simpleHoldings].sort((left, right) => left.symbol.localeCompare(right.symbol)),
- [simpleHoldings],
- );
+ const selectedSellHolding = useMemo(() => {
+ if (selectedHoldingTradeId) {
+ return availableSellHoldings.find((holding) => holding.tradeId === selectedHoldingTradeId) || null;
+ }
+ return availableSellHoldings.find((holding) => holding.symbol === normalizedSymbol) || null;
+ }, [availableSellHoldings, normalizedSymbol, selectedHoldingTradeId]);
const supportedSymbols = useMemo(() => {
const fromState = Object.keys(symbolState || {}).map(normalizeKnownSymbol).filter(Boolean) as string[];
const fromSetups = savedSetups.map((entry) => normalizeKnownSymbol(entry.symbol)).filter(Boolean) as string[];
- const fromHoldings = simpleHoldings.map((holding) => normalizeKnownSymbol(holding.symbol)).filter(Boolean) as string[];
+ const fromHoldings = availableSellHoldings.map((holding) => normalizeKnownSymbol(holding.symbol)).filter(Boolean) as string[];
return Array.from(new Set([...fromState, ...fromSetups, ...fromHoldings, ...COMMON_SIMPLE_SYMBOLS])).sort((left, right) => left.localeCompare(right));
- }, [symbolState, savedSetups, simpleHoldings]);
+ }, [symbolState, savedSetups, availableSellHoldings]);
const filteredSymbolSuggestions = useMemo(() => {
if (!normalizedSymbol) {
@@ -747,16 +767,23 @@ export function SimpleView() {
}, [normalizedSymbol, livePrice, draft.currentMarketPrice, loadingPrice, supportedSymbols]);
const previewText = useMemo(
- () => buildPreviewText(draft, draft.side === 'sell' ? matchingHolding : null),
- [draft, matchingHolding],
+ () => buildPreviewText(draft, draft.side === 'sell' ? selectedSellHolding : null),
+ [draft, selectedSellHolding],
);
function updateDraft(key: K, value: SimpleSetupDraft[K]) {
+ if (key === 'side' && value === 'buy') {
+ setSelectedHoldingTradeId(null);
+ }
+ if (key === 'symbol' && draft.side === 'sell') {
+ setSelectedHoldingTradeId(null);
+ }
setDraft((prev) => ({ ...prev, [key]: value }));
}
function applyHoldingToDraft(holding: SimpleHolding) {
setMarketPriceSource(null);
+ setSelectedHoldingTradeId(holding.tradeId || null);
setDraft((prev) => ({
...prev,
side: 'sell',
@@ -768,10 +795,35 @@ export function SimpleView() {
useEffect(() => {
if (draft.side !== 'sell') return;
- if (matchingHolding) return;
+ if (selectedSellHolding) return;
if (availableSellHoldings.length === 0) return;
applyHoldingToDraft(availableSellHoldings[0]);
- }, [draft.side, matchingHolding, availableSellHoldings]);
+ }, [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();
+ if (requestedMode !== 'sell') return;
+ if (availableSellHoldings.length === 0) return;
+
+ const selected = (requestedTradeId
+ ? availableSellHoldings.find((holding) => holding.tradeId === requestedTradeId)
+ : null)
+ || (requestedSymbol
+ ? availableSellHoldings.find((holding) => holding.symbol === requestedSymbol)
+ : null)
+ || availableSellHoldings[0];
+
+ if (selected) {
+ applyHoldingToDraft(selected);
+ setMessage(`Loaded ${selected.symbol} from Portfolio. Configure the profit target and save the plan.`);
+ setError(null);
+ }
+ consumedPrefillRef.current = true;
+ setSearchParams({}, { replace: true });
+ }, [availableSellHoldings, searchParams, setSearchParams]);
async function copyIdentifier(kind: 'trade' | 'order', value: string | null | undefined) {
if (!value) return;
@@ -867,7 +919,7 @@ export function SimpleView() {
const payload = buildSimpleSetupPayload({
draft,
existingId: editingSetupId || undefined,
- holding: draft.side === 'sell' ? matchingHolding : null,
+ holding: draft.side === 'sell' ? selectedSellHolding : null,
existingEntry,
});
@@ -958,7 +1010,7 @@ export function SimpleView() {
}
const saveButtonLabel = editingSetupId ? 'Update setup' : 'Save setup';
- const saveButtonDisabled = submitting || loadingPrice || (draft.side === 'sell' && !matchingHolding);
+ const saveButtonDisabled = submitting || loadingPrice || (draft.side === 'sell' && !selectedSellHolding);
return (
@@ -978,6 +1030,7 @@ export function SimpleView() {
type="button"
onClick={() => {
setEditingSetupId(null);
+ setSelectedHoldingTradeId(null);
setMarketPriceSource(draft.currentMarketPrice ? marketPriceSource : null);
setDraft({
...DEFAULT_DRAFT,
@@ -1002,6 +1055,7 @@ export function SimpleView() {
onClick={() => {
setError(null);
setMessage(null);
+ setSelectedHoldingTradeId(null);
setDraft((prev) => ({ ...prev, side: 'buy' }));
}}
className={`rounded-[1.25rem] border px-4 py-4 text-left transition ${
@@ -1041,9 +1095,9 @@ export function SimpleView() {