summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-19 00:41:35 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-19 00:41:35 -0700
commit927a77d68b138778690d380fe7931cc37ce06c9e (patch)
tree050e8bec85837dd6db98188c88349959a1925040
parent0a6fa2a6566a6d20b9cd587f1d188869971871c5 (diff)
feat: fetch valuation on overview tab, add DCF block to ValuationOverviewCard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--frontend/app/page.tsx35
-rw-r--r--frontend/components/prism/ValuationOverviewCard.tsx44
2 files changed, 74 insertions, 5 deletions
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index 150aaff..07bdfa4 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -17,10 +17,11 @@ import { VolumeCard } from "@/components/prism/VolumeCard";
import { ApiError, api } from "@/lib/api";
import { deltaClass, fmtNumber, fmtPct } from "@/lib/format";
import { buildKpis, limitIndices, marketClock, OVERVIEW_NAV_ITEMS, signalTone } from "@/lib/overview";
-import type { HistoryPoint, MarketIndex, SearchResult, TickerOverview, WatchlistResponse } from "@/types/api";
+import type { HistoryPoint, MarketIndex, SearchResult, TickerOverview, ValuationResponse, WatchlistResponse } from "@/types/api";
type LoadState = "idle" | "loading" | "ready" | "invalid" | "error";
type ChartState = "idle" | "loading" | "ready" | "error";
+type ValState = "idle" | "loading" | "ready" | "error";
export default function OverviewPage() {
return (
@@ -49,6 +50,8 @@ function OverviewClient() {
const [overviewError, setOverviewError] = useState<string | null>(null);
const [chartState, setChartState] = useState<ChartState>("idle");
const [chartError, setChartError] = useState<string | null>(null);
+ const [valuation, setValuation] = useState<ValuationResponse | null>(null);
+ const [valState, setValState] = useState<ValState>("idle");
const [watchlistError, setWatchlistError] = useState<string | null>(null);
const [clockSnapshot, setClockSnapshot] = useState(() => marketClock());
@@ -229,6 +232,34 @@ function OverviewClient() {
};
}, [selectedTicker, period]);
+ useEffect(() => {
+ if (!selectedTicker) {
+ setValuation(null);
+ setValState("idle");
+ return;
+ }
+
+ let cancelled = false;
+ setValState("loading");
+ setValuation(null);
+
+ api
+ .valuation(selectedTicker)
+ .then((res) => {
+ if (!cancelled) {
+ setValuation(res);
+ setValState("ready");
+ }
+ })
+ .catch(() => {
+ if (!cancelled) setValState("error");
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [selectedTicker]);
+
async function onSearchSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (results[0]) {
@@ -318,7 +349,7 @@ function OverviewClient() {
<div className="psm-column">
<ProfileCard overview={overview} />
<ShortInterestCard overview={overview} />
- <ValuationOverviewCard overview={overview} />
+ <ValuationOverviewCard overview={overview} valuation={valuation} valState={valState} />
<QualityCard overview={overview} />
</div>
</div>
diff --git a/frontend/components/prism/ValuationOverviewCard.tsx b/frontend/components/prism/ValuationOverviewCard.tsx
index f85e443..9e59787 100644
--- a/frontend/components/prism/ValuationOverviewCard.tsx
+++ b/frontend/components/prism/ValuationOverviewCard.tsx
@@ -1,11 +1,15 @@
-import type { TickerOverview } from "@/types/api";
-import { fmtCurrency, fmtLarge, fmtNumber } from "@/lib/format";
+import type { TickerOverview, ValuationResponse } from "@/types/api";
+import { fmtCurrency, fmtLarge, fmtNumber, fmtPct } from "@/lib/format";
+
+type ValState = "idle" | "loading" | "ready" | "error";
type Props = {
overview: TickerOverview;
+ valuation: ValuationResponse | null;
+ valState: ValState;
};
-export function ValuationOverviewCard({ overview }: Props) {
+export function ValuationOverviewCard({ overview, valuation, valState }: Props) {
const rows = [
{ label: "Market Cap", value: fmtLarge(overview.stats.market_cap), missing: overview.stats.market_cap == null },
{ label: "P/E TTM", value: overview.stats.trailing_pe == null ? "-" : `${fmtNumber(overview.stats.trailing_pe)}x`, missing: overview.stats.trailing_pe == null },
@@ -17,6 +21,8 @@ export function ValuationOverviewCard({ overview }: Props) {
];
const visible = rows.filter((r) => !r.missing);
+ const dcf = valuation?.dcf;
+ const showDcf = dcf?.available && dcf.intrinsic_value_per_share != null;
return (
<section className="psm-card">
@@ -36,6 +42,38 @@ export function ValuationOverviewCard({ overview }: Props) {
))}
</div>
)}
+ {valState === "loading" && (
+ <>
+ <hr className="psm-divider" />
+ <div className="psm-stat-list">
+ <div className="psm-stat-row">
+ <span className="psm-stat-label psm-skeleton" style={{ width: "120px", height: "14px", display: "inline-block" }} />
+ <span className="psm-stat-value psm-skeleton" style={{ width: "60px", height: "14px", display: "inline-block" }} />
+ </div>
+ <div className="psm-stat-row">
+ <span className="psm-stat-label psm-skeleton" style={{ width: "100px", height: "14px", display: "inline-block" }} />
+ <span className="psm-stat-value psm-skeleton" style={{ width: "48px", height: "14px", display: "inline-block" }} />
+ </div>
+ </div>
+ </>
+ )}
+ {valState === "ready" && showDcf && dcf && (
+ <>
+ <hr className="psm-divider" />
+ <div className="psm-stat-list">
+ <div className="psm-stat-row">
+ <span className="psm-stat-label">DCF Intrinsic Value</span>
+ <span className="psm-stat-value">{fmtCurrency(dcf.intrinsic_value_per_share)}</span>
+ </div>
+ {dcf.growth_rate_used != null && (
+ <div className="psm-stat-row">
+ <span className="psm-stat-label">FCF Growth Used</span>
+ <span className="psm-stat-value">{fmtPct(dcf.growth_rate_used)}</span>
+ </div>
+ )}
+ </div>
+ </>
+ )}
</section>
);
}