fix: load full watchlist research details
prakhar82 committed
Sep 13, 2026 at 15:18 UTC
289d918aaa46f5e4f3655106a828e62837a7e754
2 files changed
+181
-16
frontend/app/components/investment-workspace.tsx
+128
-16
@@ -3130,8 +3130,8 @@ function StockResearchDrawer({ position, watchlistItem, research, onUpdateDispla
3130
<section><h3>Overview</h3>{editing && position ? <div className="holding-name-editor"><label>Display name<input maxLength={160} value={name} onChange={(event) => setName(event.target.value)} /></label><Button disabled={saving} onClick={() => void saveName()}>Save</Button><Button variant="secondary" disabled={saving} onClick={() => { setEditing(false); setName(position.customDisplayName ?? position.displayName); setRenameError(null); }}>Cancel</Button>{renameError ? <small role="alert">{renameError}</small> : null}</div> : <p><strong>{displayName}</strong> {position?.sourceType === "MANUAL_CSV_IMPORT" && onUpdateDisplayName ? <button className="text-action" type="button" onClick={() => setEditing(true)}>Edit</button> : null}</p>}<p>{ticker} · {isin ?? "ISIN N/A"} · {market?.resolution.exchange ?? exchange} · {country ?? "Country N/A"} · {currency ?? "Currency N/A"} · {market?.resolution.quoteType ?? assetType}</p><p>Provider ticker: {market?.resolution.providerTicker ?? "N/A"} · Provider identity: {market?.resolution.companyName ?? "N/A"}</p><p>Sector: {metricText(structuredFact(research, "sector"))} · Industry: {metricText(structuredFact(research, "industry"))}</p><p>{position ? <>Broker/source: {brokerDisplayName(position.brokerType)} · {position.sourceType}</> : <>Watchlist status: Not held</>}</p></section>
3131
{position ? <section><h3>Position</h3><div className="research-metric-grid"><div className="research-metric"><span>Quantity</span><strong>{position.quantity}</strong></div><div className="research-metric"><span>Average Cost</span><strong>{formatBackendMoney(position.averageCost)}</strong></div><div className="research-metric"><span>Cost Basis</span><strong>{formatBackendMoney(position.costBasis)}</strong></div><div className="research-metric"><span>Imported Price</span><strong>{formatBackendMoney(position.importedPrice)}</strong></div><div className="research-metric"><span>Latest Market Price</span><strong>{formatBackendMoney(position.currentPrice)}</strong></div><div className="research-metric"><span>Market Value</span><strong>{formatBackendMoney(position.marketValue)}</strong></div><div className="research-metric"><span>Unrealized P/L</span><strong>{formatBackendMoney(position.unrealizedProfitLoss)}</strong></div></div></section> : <section><h3>Watchlist status</h3><p>Public company research</p><div className="research-metric-grid">{watchlistItem?.sourcePeriod ? <div className="research-metric"><span>Market return ({watchlistItem.sourcePeriod})</span><strong className={`${performanceRowTone(watchlistItem.sourcePerformancePct)}-text`}>{formatSignedPerformancePct(watchlistItem.sourcePerformancePct)}</strong></div> : null}</div></section>}
3132
<section><h3>Market Data</h3><div className="research-metric-grid"><div className="research-metric"><span>Latest Price</span><strong>{currentPrice}</strong></div><EvidenceMetric label="Previous close" metric={structuredFact(research, "previousClose")} /><EvidenceMetric label="Bid" metric={structuredFact(research, "bid")} /><EvidenceMetric label="Ask" metric={structuredFact(research, "ask")} /><EvidenceMetric label="Volume" metric={structuredFact(research, "volume")} /><EvidenceMetric label="10-day avg volume" metric={structuredFact(research, "averageVolume10Day")} /><EvidenceMetric label="3-month avg volume" metric={structuredFact(research, "averageVolume")} /><EvidenceMetric label="52-week low" metric={structuredFact(research, "fiftyTwoWeekLow")} /><EvidenceMetric label="52-week high" metric={structuredFact(research, "fiftyTwoWeekHigh")} /><div className="research-metric"><span>Price freshness</span><strong>{research?.priceFreshness ?? "UNKNOWN"}</strong></div><div className="research-metric"><span>Market As Of</span><strong>{market?.marketAsOf ? new Date(market.marketAsOf).toLocaleString() : position?.quote?.sourceTimestamp ? new Date(position.quote.sourceTimestamp).toLocaleString() : "N/A"}</strong></div><div className="research-metric"><span>Retrieved At</span><strong>{market?.retrievedAt ? new Date(market.retrievedAt).toLocaleString() : position?.quote?.receivedAt ? new Date(position.quote.receivedAt).toLocaleString() : "N/A"}</strong></div><div className="research-metric"><span>Provider</span><strong>{market?.sourceName ?? position?.quote?.source ?? "N/A"}</strong></div></div></section>
3133
- <section><h3>Valuation</h3><div className="research-metric-grid"><StructuredMetric label="Market cap" research={research} fact="marketCap" /><StructuredMetric label="Enterprise value" research={research} fact="enterpriseValue" /><StructuredMetric label="Trailing P/E" research={research} fact="trailingPE" /><StructuredMetric label="Forward P/E" research={research} fact="forwardPE" /><StructuredMetric label="P/B" research={research} fact="priceToBook" /><StructuredMetric label="P/S" research={research} fact="priceToSales" /><StructuredMetric label="PEG" research={research} fact="pegRatio" />{!financial && !isEtf ? <><StructuredMetric label="EV/revenue" research={research} fact="evToRevenue" /><StructuredMetric label="EV/EBITDA" research={research} fact="evToEbitda" /></> : null}<StructuredMetric label="Trailing EPS" research={research} fact="trailingEps" /><StructuredMetric label="Forward EPS" research={research} fact="forwardEps" /></div><p><Badge tone={valuationTone(research?.valuation.state)}>{research?.valuation.state ?? "UNKNOWN"}</Badge> {research?.valuation.reason ?? "Awaiting contextual public research."}</p></section>
3134
- {!isEtf ? <section><h3>Quality / Fundamentals</h3><div className="research-metric-grid"><EvidenceMetric label="ROE" metric={structuredFact(research, "roe") ?? research?.valuation.roe} /><StructuredMetric label="ROA" research={research} fact="roa" />{!financial ? <EvidenceMetric label="ROCE" metric={structuredFact(research, "roce") ?? research?.valuation.roce} /> : null}<StructuredMetric label="Operating margin" research={research} fact="operatingMargin" /><StructuredMetric label="Net margin" research={research} fact="profitMargin" /><StructuredMetric label="Revenue growth" research={research} fact="revenueGrowth" /><StructuredMetric label="Earnings growth" research={research} fact="earningsGrowth" /><StructuredMetric label="Cash" research={research} fact="totalCash" /><StructuredMetric label="Debt" research={research} fact="totalDebt" /><StructuredMetric label="Debt / equity" research={research} fact="debtToEquity" /><StructuredMetric label="Free cash flow" research={research} fact="freeCashFlow" /><StructuredMetric label="Operating cash flow" research={research} fact="operatingCashFlow" /></div></section> : null}
3133
+ <section><h3>Valuation</h3><div className="research-metric-grid"><StructuredMetric label="Market cap" research={research} fact="marketCap" /><StructuredMetric label="Enterprise value" research={research} fact="enterpriseValue" /><StructuredMetric label="Trailing P/E" research={research} fact="trailingPE" /><StructuredMetric label="Forward P/E" research={research} fact="forwardPE" /><StructuredMetric label="P/B" research={research} fact="priceToBook" /><StructuredMetric label="P/S" research={research} fact="priceToSales" /><StructuredMetric label="PEG" research={research} fact="pegRatio" />{!financial && !isEtf ? <><StructuredMetric label="EV/revenue" research={research} fact="evToRevenue" /><StructuredMetric label="EV/EBITDA" research={research} fact="evToEbitda" /></> : null}<StructuredMetric label="Trailing EPS" research={research} fact="trailingEps" /><StructuredMetric label="Forward EPS" research={research} fact="forwardEps" /></div><p><Badge tone={valuationTone(research?.valuation?.state ?? "UNKNOWN")}>{research?.valuation?.state ?? "UNKNOWN"}</Badge> {research?.valuation?.reason ?? "Awaiting contextual public research."}</p></section>
3134
+ {!isEtf ? <section><h3>Quality / Fundamentals</h3><div className="research-metric-grid"><EvidenceMetric label="ROE" metric={structuredFact(research, "roe") ?? research?.valuation?.roe} /><StructuredMetric label="ROA" research={research} fact="roa" />{!financial ? <EvidenceMetric label="ROCE" metric={structuredFact(research, "roce") ?? research?.valuation?.roce} /> : null}<StructuredMetric label="Operating margin" research={research} fact="operatingMargin" /><StructuredMetric label="Net margin" research={research} fact="profitMargin" /><StructuredMetric label="Revenue growth" research={research} fact="revenueGrowth" /><StructuredMetric label="Earnings growth" research={research} fact="earningsGrowth" /><StructuredMetric label="Cash" research={research} fact="totalCash" /><StructuredMetric label="Debt" research={research} fact="totalDebt" /><StructuredMetric label="Debt / equity" research={research} fact="debtToEquity" /><StructuredMetric label="Free cash flow" research={research} fact="freeCashFlow" /><StructuredMetric label="Operating cash flow" research={research} fact="operatingCashFlow" /></div></section> : null}
3135
<section><h3>Analyst View</h3><p>External public analyst consensus; not an application recommendation.</p><div className="research-metric-grid"><div className="research-metric"><span>Current Price</span><strong>{currentPrice}</strong></div><StructuredMetric label="Target low" research={research} fact="publicAnalystTargetLowPrice" /><StructuredMetric label="Target median" research={research} fact="publicAnalystTargetMedianPrice" /><StructuredMetric label="Target mean" research={research} fact="publicAnalystTargetMeanPrice" /><StructuredMetric label="Target high" research={research} fact="publicAnalystTargetHighPrice" /><StructuredMetric label="Number of analysts" research={research} fact="publicAnalystCount" /><StructuredMetric label="Consensus" research={research} fact="publicAnalystConsensus" /><StructuredMetric label="Consensus score" research={research} fact="publicAnalystRecommendationMean" /></div></section>
3136
<section><h3>Latest Quarterly Result</h3>{result ? <><p>{result.documentTitle ?? "Quarterly financial result"} · {statementPeriod(result.period)} · {result.reportingBasis ?? "Reporting basis N/A"} · {result.resultDate ? statementPeriod(result.resultDate) : "Result date N/A"}</p><div className="research-metric-grid">{financial ? <><EvidenceMetric label="Total income" metric={result.revenue} /><EvidenceMetric label="PAT / net profit" metric={result.pat} /><EvidenceMetric label="EPS" metric={result.eps} /><EvidenceMetric label="NIM" metric={result.nim} /><EvidenceMetric label="ROA" metric={result.roa} /><EvidenceMetric label="ROE" metric={result.roe} /><EvidenceMetric label="Gross NPA" metric={result.grossNpa} /><EvidenceMetric label="Net NPA" metric={result.netNpa} /><EvidenceMetric label="Deposits" metric={result.deposits} /><EvidenceMetric label="Advances" metric={result.advances} /><EvidenceMetric label="Capital adequacy" metric={result.capitalAdequacy} /><EvidenceMetric label="Credit cost" metric={result.creditCost} /></> : <><EvidenceMetric label="Revenue" metric={result.revenue} /><EvidenceMetric label="Revenue YoY" metric={result.revenueYoYPercent} /><EvidenceMetric label="EBITDA / operating profit" metric={result.ebitda} /><EvidenceMetric label="EBITDA / operating margin" metric={result.ebitdaMargin} /><EvidenceMetric label="PAT / net profit" metric={result.pat} /><EvidenceMetric label="PAT YoY" metric={result.patYoYPercent} /><EvidenceMetric label="EPS" metric={result.eps} /><EvidenceMetric label="Debt / borrowings" metric={result.debtOrBorrowings} /></>}</div>{result.yoySummary ? <p>{result.yoySummary}</p> : null}<p>Source: {filingSourceLabel(result.sourceName)} · Published {result.publishedAt ? new Date(result.publishedAt).toLocaleDateString() : "N/A"} · Retrieved {new Date(result.retrievedAt).toLocaleString()}</p><a href={result.sourceUrl} target="_blank" rel="noreferrer">View {filingSourceLabel(result.sourceName)} Filing ↗</a></> : <p>{research?.quarterlyResultStatus === "PDF_SCANNED_OCR_REQUIRED" ? "PDF scanned; OCR required." : "Not publicly available."}</p>}</section>
3137
{!isEtf ? <section><h3>Financial History</h3><h4>Quarterly</h4><FinancialHistoryTable periods={(research?.financialResultHistory ?? []).filter((period) => period.periodType === "QUARTERLY").slice(0, 4)} /><h4>Annual</h4><FinancialHistoryTable periods={(research?.financialResultHistory ?? []).filter((period) => period.periodType === "ANNUAL")} /></section> : null}
@@ -3144,7 +3144,7 @@ function StockResearchDrawer({ position, watchlistItem, research, onUpdateDispla
3144
<DurableEvidenceSection title="Customers" evidence={research?.durableCategoryEvidence?.CLIENTS} />
3145
<DurableEvidenceSection title="Catalysts" evidence={research?.durableCategoryEvidence?.CATALYSTS} />
3146
<section><h3>News</h3>{market?.news?.length ? market.news.map((article) => <article key={article.url}><strong>{article.headline}</strong><p>{article.publisher} · {article.publishedAt ? new Date(article.publishedAt).toLocaleString() : "Date unavailable"}</p><a href={article.url} target="_blank" rel="noreferrer">Open source</a></article>) : <p>No public provider news available.</p>}</section>
3147
- <section><h3>Research & Evidence</h3><p>Status: {research?.status?.replaceAll("_", " ") ?? "Awaiting research"}; {research?.sourceDiversity.domainsFound ?? 0} unique domains, {research?.sourceDiversity.exchangeSources ?? 0} exchange sources, {research?.sourceDiversity.companySources ?? 0} company sources, {research?.sourceDiversity.secondarySources ?? 0} secondary sources.</p>{research?.latestEvent ? <a href={research.latestEvent.sourceUrl} target="_blank" rel="noreferrer">Latest evidence source</a> : null}</section>
3147
+ <section><h3>Research & Evidence</h3><p>Status: {research?.status?.replaceAll("_", " ") ?? "Awaiting research"}; {research?.sourceDiversity?.domainsFound ?? 0} unique domains, {research?.sourceDiversity?.exchangeSources ?? 0} exchange sources, {research?.sourceDiversity?.companySources ?? 0} company sources, {research?.sourceDiversity?.secondarySources ?? 0} secondary sources.</p>{research?.latestEvent ? <a href={research.latestEvent.sourceUrl} target="_blank" rel="noreferrer">Latest evidence source</a> : null}</section>
3148
</aside>
3149
</div>
3150
);
@@ -3495,6 +3495,42 @@ function ResearchView({
3495
useEffect(() => {
3496
setResearchDetail(null);
3497
}, [researchContext.kind, selectedResearchInstrumentId]);
3498
+ const [watchlistDetailResearch, setWatchlistDetailResearch] = useState<{
3499
+ instrumentId: string;
3500
+ research: PortfolioResearchCompany;
3501
+ } | null>(null);
3502
+ const [watchlistDetailError, setWatchlistDetailError] = useState<string | null>(null);
3503
+ const [watchlistDetailLoading, setWatchlistDetailLoading] = useState(false);
3504
+ const [watchlistDetailRetryKey, setWatchlistDetailRetryKey] = useState(0);
3505
+ const watchlistDetailFetchId = useRef(0);
3506
+ const detailRegion =
3507
+ researchContext.kind === "WATCHLIST" ? researchContext.region : undefined;
3508
+ useEffect(() => {
3509
+ const detail = researchDetail;
3510
+ const region = detailRegion;
3511
+ if (!detail || !detail.watchlistInstrumentId || !region) {
3512
+ setWatchlistDetailResearch(null);
3513
+ setWatchlistDetailError(null);
3514
+ setWatchlistDetailLoading(false);
3515
+ return;
3516
+ }
3517
+ const fetchId = ++watchlistDetailFetchId.current;
3518
+ const instrumentId = detail.researchInstrumentId;
3519
+ setWatchlistDetailLoading(true);
3520
+ setWatchlistDetailError(null);
3521
+ researchApi
3522
+ .getCompanyPresentation(instrumentId, region)
3523
+ .then((research) => {
3524
+ if (fetchId !== watchlistDetailFetchId.current) return;
3525
+ setWatchlistDetailResearch({ instrumentId, research });
3526
+ setWatchlistDetailLoading(false);
3527
+ })
3528
+ .catch((error) => {
3529
+ if (fetchId !== watchlistDetailFetchId.current) return;
3530
+ setWatchlistDetailError(getApiFailure(error).message);
3531
+ setWatchlistDetailLoading(false);
3532
+ });
3533
+ }, [researchDetail?.watchlistInstrumentId, researchDetail?.researchInstrumentId, detailRegion, watchlistDetailRetryKey]);
3534
const filteredEvents = (summary?.recentEvents ?? []).filter((event) => {
3535
return (!eventType || event.eventType === eventType) && (!impact || event.impact === impact);
3536
});
@@ -3513,11 +3549,19 @@ function ResearchView({
3549
const detailWatchlistItem = researchDetail?.watchlistInstrumentId
3550
? watchlistResearch?.instruments.find((item) => item.globalInstrumentId === researchDetail.watchlistInstrumentId)
3551
: undefined;
3516
- const detailResearch = researchDetail
3517
- ? (detailWatchlistItem?.company
3518
- ?? portfolioResearchSummary?.companies.find((company) => company.instrumentId === researchDetail.researchInstrumentId)
3519
- ?? (researchContext.kind === "SEARCH" ? searchPresentation ?? null : null))
3552
+ const portfolioDetailResearch = researchDetail
3553
+ ? portfolioResearchSummary?.companies.find(
3554
+ (company) => company.instrumentId === researchDetail.researchInstrumentId
3555
+ )
3556
: undefined;
3557
+ const detailResearch: PortfolioResearchCompany | undefined = !researchDetail
3558
+ ? undefined
3559
+ : researchDetail.watchlistInstrumentId
3560
+ ? watchlistDetailResearch?.instrumentId === researchDetail.researchInstrumentId
3561
+ ? watchlistDetailResearch.research
3562
+ : undefined
3563
+ : portfolioDetailResearch
3564
+ ?? (researchContext.kind === "SEARCH" ? searchPresentation ?? undefined : undefined);
3565
const researchOptions = useMemo(() => {
3566
if (researchContext.kind === "WATCHLIST") {
3567
return (watchlistResearch?.instruments ?? []).map((item) => ({
@@ -3648,6 +3692,33 @@ function ResearchView({
3692
</div>
3693
{researchContext.kind === "WATCHLIST" ? (watchlistResearch?.instruments ?? []).map((item) => {
3694
const company = item.company;
3695
+ const companyStatus = company.status ?? "";
3696
+
3697
+ const watchlistPrice =
3698
+ item.marketData?.quote?.last?.amount
3699
+ ?? item.marketData?.snapshot?.price
3700
+ ?? null;
3701
+
3702
+ const watchlistPe =
3703
+ item.marketData?.snapshot?.peRatio
3704
+ ?? null;
3705
+
3706
+ const watchlistCurrency =
3707
+ item.marketData?.quote?.currency
3708
+ ?? item.marketData?.snapshot?.currency
3709
+ ?? item.currency
3710
+ ?? undefined;
3711
+
3712
+ const valuationState = company.valuation?.state;
3713
+ const valuationReason = company.valuation?.reason;
3714
+
3715
+ const ownershipIncreases = company.ownershipIncreases ?? [];
3716
+ const shareholdingChanges = company.shareholdingChanges ?? [];
3717
+ const catalysts = company.currentQuarterCatalysts ?? [];
3718
+
3719
+ const sourceCount = company.sourceCount;
3720
+ const documentCount = company.documentCount;
3721
+
3722
const performanceTone = performanceRowTone(item.sourcePerformancePct);
3723
return (
3724
<button
@@ -3669,18 +3740,43 @@ function ResearchView({
3740
}}
3741
>
3742
<span role="cell">
3672
- <strong>{company.companyName} <span className={`research-status-dot research-status-${researchStatusTone(company.status)}`} role="img" aria-label={researchStatusDescription(company.status)} title={researchStatusDescription(company.status)} /></strong>
3743
+ <strong>{company.companyName} <span className={`research-status-dot research-status-${researchStatusTone(companyStatus)}`} role="img" aria-label={researchStatusDescription(companyStatus)} title={researchStatusDescription(companyStatus)} /></strong>
3744
<small>{[company.ticker, company.exchange, company.isin].filter(Boolean).join(" / ")}</small>
3745
<small>{researchContext.name} · Public company research · Not held</small>
3746
</span>
3676
- <span role="cell">{company.currentPrice == null ? "—" : formatMoney(Number(company.currentPrice), company.structuredMarket?.resolution.currency ?? item.currency ?? undefined)}<small>P/E {metricText(company.valuation.currentPe)}</small></span>
3677
- <span role="cell"><Badge tone={valuationTone(company.valuation.state)}>{company.valuation.state}</Badge><small>{company.valuation.reason}</small></span>
3678
- <span role="cell">{latestResultText(company)}</span>
3679
- <span role="cell">{company.ownershipIncreases.length ? company.ownershipIncreases.map((value) => value === "FII_FPI" ? "FII/FPI" : value.replaceAll("_", " ")).join(" · ") : "Unavailable"}<small>{company.shareholdingChanges.length ? `${company.shareholdingChanges.length} comparable trends` : "Previous comparable period not found"}</small></span>
3680
- <span role="cell">{company.currentQuarterCatalysts.length ? `${company.currentQuarterCatalysts.length} current` : company.catalystScore ?? "None verified"}</span>
3681
- <span role="cell">{company.sourceCount} sources / {company.documentCount} docs</span>
3747
<span role="cell">
3683
- <Badge tone={researchStatusTone(company.status)}>{company.status.replaceAll("_", " ")}</Badge>
3748
+ {watchlistPrice == null
3749
+ ? "N/A"
3750
+ : formatMoney(Number(watchlistPrice), watchlistCurrency)}
3751
+ <small>
3752
+ {watchlistPe == null
3753
+ ? "P/E N/A"
3754
+ : `P/E ${new Intl.NumberFormat("en-IN", {
3755
+ maximumFractionDigits: 2
3756
+ }).format(Number(watchlistPe))}`}
3757
+ </small>
3758
+ </span>
3759
+ <span role="cell">
3760
+ <Badge tone={valuationTone(valuationState)}>{valuationState ?? "N/A"}</Badge>
3761
+ <small>{valuationReason ?? ""}</small>
3762
+ </span>
3763
+ <span role="cell">Not publicly available</span>
3764
+ <span role="cell">
3765
+ {ownershipIncreases.length
3766
+ ? ownershipIncreases.map((value) => value === "FII_FPI" ? "FII/FPI" : value.replaceAll("_", " ")).join(" · ")
3767
+ : "Unavailable"}
3768
+ <small>{shareholdingChanges.length ? `${shareholdingChanges.length} comparable trends` : "Previous comparable period not found"}</small>
3769
+ </span>
3770
+ <span role="cell">
3771
+ {catalysts.length ? `${catalysts.length} current` : company.catalystScore ?? "None verified"}
3772
+ </span>
3773
+ <span role="cell">
3774
+ {sourceCount == null && documentCount == null
3775
+ ? "N/A"
3776
+ : `${sourceCount ?? 0} sources / ${documentCount ?? 0} docs`}
3777
+ </span>
3778
+ <span role="cell">
3779
+ <Badge tone={researchStatusTone(companyStatus)}>{companyStatus.replaceAll("_", " ")}</Badge>
3780
{item.sourcePeriod ? <strong className={`market-intelligence-table-return ${performanceTone}-text`}>Market return ({item.sourcePeriod}) {formatSignedPerformancePct(item.sourcePerformancePct)}</strong> : null}
3781
</span>
3782
</button>
@@ -3735,7 +3831,23 @@ function ResearchView({
3831
: "Choose a Market Intelligence stock to add it to this regional watchlist."}
3832
/>
3833
)}
3738
- {researchDetail && detailResearch ? <StockResearchDrawer position={researchDetail.position} watchlistItem={detailWatchlistItem} research={detailResearch} onClose={() => setResearchDetail(null)} /> : null}
3834
+ {researchDetail && researchDetail.watchlistInstrumentId
3835
+ ? watchlistDetailLoading
3836
+ ? <>
3837
+ <Skeleton rows={4} />
3838
+ <p>Loading public research...</p>
3839
+ </>
3840
+ : watchlistDetailError
3841
+ ? <ErrorState
3842
+ message={watchlistDetailError}
3843
+ action={<Button variant="secondary" onClick={() => setWatchlistDetailRetryKey((key) => key + 1)}>Retry</Button>}
3844
+ />
3845
+ : detailResearch
3846
+ ? <StockResearchDrawer position={researchDetail.position} watchlistItem={detailWatchlistItem} research={detailResearch} onClose={() => setResearchDetail(null)} />
3847
+ : null
3848
+ : researchDetail && detailResearch
3849
+ ? <StockResearchDrawer position={researchDetail.position} watchlistItem={detailWatchlistItem} research={detailResearch} onClose={() => setResearchDetail(null)} />
3850
+ : null}
3851
</Card>
3852
3853
<Card className="wide-panel research-sticky-panel">
frontend/app/lib/portfolio-api.ts
+53
@@ -205,6 +205,7 @@ export type WatchlistResearchInstrument = {
205
sourcePeriod?: SectorPerformance["period"] | null;
206
sourcePerformancePct?: number | null;
207
addedAt?: string | null;
208
+ marketData?: WatchlistMarketData | null;
209
company: PortfolioResearchCompany;
210
};
211
export type WatchlistResearchPresentation = {
@@ -273,6 +274,58 @@ export type Quote = {
274
receivedAt?: string;
275
};
276
277
+export type WatchlistSnapshot = {
278
+ providerTicker?: string | null;
279
+ exchange?: string | null;
280
+ currency?: string | null;
281
+ quoteType?: string | null;
282
+ price?: number | null;
283
+ priceCurrency?: string | null;
284
+ marketAsOf?: string | null;
285
+ retrievedAt?: string | null;
286
+ source?: string | null;
287
+ status?: string | null;
288
+ previousClose?: number | null;
289
+ bid?: number | null;
290
+ ask?: number | null;
291
+ volume?: number | null;
292
+ averageVolume10Day?: number | null;
293
+ averageVolume3Month?: number | null;
294
+ fiftyTwoWeekHigh?: number | null;
295
+ fiftyTwoWeekLow?: number | null;
296
+ marketCap?: number | null;
297
+ enterpriseValue?: number | null;
298
+ peRatio?: number | null;
299
+ forwardPe?: number | null;
300
+ pbRatio?: number | null;
301
+ psRatio?: number | null;
302
+ pegRatio?: number | null;
303
+ evEbitda?: number | null;
304
+ trailingEps?: number | null;
305
+ forwardEps?: number | null;
306
+ roe?: number | null;
307
+ roa?: number | null;
308
+ roce?: number | null;
309
+ netMargin?: number | null;
310
+ operatingMargin?: number | null;
311
+ debt?: number | null;
312
+ cash?: number | null;
313
+ debtEquity?: number | null;
314
+ revenueGrowth?: number | null;
315
+ earningsGrowth?: number | null;
316
+ freeCashFlow?: number | null;
317
+ operatingCashFlow?: number | null;
318
+};
319
+
320
+export type WatchlistMarketData = {
321
+ canonicalName?: string | null;
322
+ isin?: string | null;
323
+ providerMappings?: Instrument["providerMappings"];
324
+ snapshot?: WatchlistSnapshot | null;
325
+ quote?: Quote | null;
326
+ marketDataError?: string | null;
327
+};
328
+
329
export type PortfolioPosition = {
330
positionId: string;
331
portfolioId: string;