summaryrefslogtreecommitdiff
path: root/frontend/components/prism/AdvancedDcfPanel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/components/prism/AdvancedDcfPanel.tsx')
-rw-r--r--frontend/components/prism/AdvancedDcfPanel.tsx218
1 files changed, 218 insertions, 0 deletions
diff --git a/frontend/components/prism/AdvancedDcfPanel.tsx b/frontend/components/prism/AdvancedDcfPanel.tsx
new file mode 100644
index 0000000..9d82f3a
--- /dev/null
+++ b/frontend/components/prism/AdvancedDcfPanel.tsx
@@ -0,0 +1,218 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import { computeAdvancedDcf } from "@/lib/dcf";
+import { fmtCurrency } from "@/lib/format";
+import type { AdvancedDcfInputs as AdvancedDcfInputShape } from "@/lib/dcf";
+
+const YEARS = 5;
+
+const PRESETS: Record<string, AdvancedDcfInputShape> = {
+ bear: {
+ baseRevenue: 1_000,
+ revenueGrowth: [0.02, 0.02, 0.02, 0.02, 0.02],
+ ebitdaMargin: [0.2, 0.2, 0.2, 0.2, 0.2],
+ dnaPctRevenue: [0.05, 0.05, 0.05, 0.05, 0.05],
+ capexPctRevenue: [0.1, 0.1, 0.1, 0.1, 0.1],
+ nwcPctDeltaRev: [0.03, 0.03, 0.03, 0.03, 0.03],
+ taxRate: [0.28, 0.28, 0.28, 0.28, 0.28],
+ },
+ base: {
+ baseRevenue: 1_000,
+ revenueGrowth: [0.05, 0.05, 0.05, 0.05, 0.05],
+ ebitdaMargin: [0.25, 0.25, 0.25, 0.25, 0.25],
+ dnaPctRevenue: [0.05, 0.05, 0.05, 0.05, 0.05],
+ capexPctRevenue: [0.07, 0.07, 0.07, 0.07, 0.07],
+ nwcPctDeltaRev: [0.02, 0.02, 0.02, 0.02, 0.02],
+ taxRate: [0.21, 0.21, 0.21, 0.21, 0.21],
+ },
+ bull: {
+ baseRevenue: 1_000,
+ revenueGrowth: [0.1, 0.1, 0.1, 0.1, 0.1],
+ ebitdaMargin: [0.32, 0.32, 0.32, 0.32, 0.32],
+ dnaPctRevenue: [0.05, 0.05, 0.05, 0.05, 0.05],
+ capexPctRevenue: [0.05, 0.05, 0.05, 0.05, 0.05],
+ nwcPctDeltaRev: [0.015, 0.015, 0.015, 0.015, 0.015],
+ taxRate: [0.21, 0.21, 0.21, 0.21, 0.21],
+ },
+};
+
+const LABELS: Record<keyof AdvancedDcfInputShape, string> = {
+ baseRevenue: "Base Revenue",
+ revenueGrowth: "Revenue Growth",
+ ebitdaMargin: "EBITDA Margin",
+ dnaPctRevenue: "D&A % Rev",
+ capexPctRevenue: "CapEx % Rev",
+ nwcPctDeltaRev: "ΔNWC % ΔRev",
+ taxRate: "Tax Rate",
+};
+
+const YEAR_LABELS = ["Y1", "Y2", "Y3", "Y4", "Y5"];
+
+type Props = {
+ inputs: AdvancedDcfInputShape;
+ onChange: (inputs: AdvancedDcfInputShape) => void;
+ wacc: number;
+ terminalGrowth: number;
+ sharesOutstanding: number;
+ equityBridge: number;
+ seedRevenue?: number | null;
+ currency: string;
+};
+
+function fmtRate(v: number) {
+ return `${(v * 100).toFixed(1)}%`;
+}
+
+function parseRateInput(raw: string): number | null {
+ const cleaned = raw.replace(/[^0-9.\-]/g, "");
+ const num = parseFloat(cleaned) / 100;
+ return Number.isFinite(num) ? num : null;
+}
+
+function fmtNumber(n: number): string {
+ return n.toLocaleString("en-US");
+}
+
+function parseNumber(raw: string): number {
+ const cleaned = raw.replace(/[^0-9.\-]/g, "");
+ const num = parseFloat(cleaned);
+ return Number.isFinite(num) ? num : 0;
+}
+
+export function AdvancedDcfPanel({
+ inputs,
+ onChange,
+ wacc,
+ terminalGrowth,
+ sharesOutstanding,
+ equityBridge,
+ seedRevenue,
+ currency,
+}: Props) {
+ const baseRevenue = seedRevenue ?? 1_000;
+
+ const presets = useMemo<Record<string, AdvancedDcfInputShape>>(
+ () => ({
+ bear: { ...PRESETS.bear, baseRevenue },
+ base: { ...PRESETS.base, baseRevenue },
+ bull: { ...PRESETS.bull, baseRevenue },
+ }),
+ [baseRevenue]
+ );
+
+ const computed = useMemo(() => {
+ return computeAdvancedDcf(inputs, {
+ wacc,
+ terminalGrowth,
+ projectionYears: YEARS,
+ sharesOutstanding,
+ equityBridge,
+ });
+ }, [inputs, wacc, terminalGrowth, sharesOutstanding, equityBridge]);
+
+ const price = computed && !("error" in computed) ? computed.intrinsicValuePerShare : null;
+ const isInvalid = price != null && price < 0;
+
+ function applyPreset(name: string) {
+ const preset = presets[name];
+ if (preset) onChange({ ...preset });
+ }
+
+ function updateScalar(field: "baseRevenue", value: number) {
+ onChange({ ...inputs, [field]: value });
+ }
+
+ function updateArray(field: Exclude<keyof AdvancedDcfInputShape, "baseRevenue">, index: number, value: number) {
+ const next = [...inputs[field]];
+ next[index] = value;
+ onChange({ ...inputs, [field]: next });
+ }
+
+ const [baseRevenueText, setBaseRevenueText] = useState(fmtNumber(inputs.baseRevenue));
+
+ useEffect(() => {
+ setBaseRevenueText(fmtNumber(inputs.baseRevenue));
+ }, [inputs.baseRevenue]);
+
+ return (
+ <div className="psm-val-advanced-panel">
+ <div className="psm-val-preset-row">
+ {Object.keys(PRESETS).map((name) => (
+ <button
+ key={name}
+ className="psm-val-preset-btn"
+ onClick={() => applyPreset(name)}
+ >
+ {name.charAt(0).toUpperCase() + name.slice(1)}
+ </button>
+ ))}
+ </div>
+
+ <div className="psm-val-advanced-grid">
+ {/* Scalar field */}
+ <div className="psm-val-advanced-field">
+ <label className="psm-val-advanced-field-label">{LABELS.baseRevenue}</label>
+ <input
+ type="text"
+ inputMode="numeric"
+ className="psm-val-advanced-input"
+ value={baseRevenueText}
+ onChange={(e) => {
+ const raw = e.target.value;
+ setBaseRevenueText(raw);
+ const num = parseNumber(raw);
+ if (num !== inputs.baseRevenue) {
+ updateScalar("baseRevenue", num);
+ }
+ }}
+ onBlur={() => setBaseRevenueText(fmtNumber(inputs.baseRevenue))}
+ />
+ </div>
+
+ {/* Array fields with year headers */}
+ <div className="psm-val-advanced-table">
+ <div className="psm-val-advanced-table-header">
+ <span className="psm-val-advanced-table-corner" />
+ {YEAR_LABELS.map((yl) => (
+ <span key={yl} className="psm-val-advanced-table-year">{yl}</span>
+ ))}
+ </div>
+ {(Object.keys(LABELS) as Array<keyof AdvancedDcfInputShape>)
+ .filter((key) => key !== "baseRevenue")
+ .map((field) => (
+ <div key={field} className="psm-val-advanced-table-row">
+ <span className="psm-val-advanced-table-label">{LABELS[field]}</span>
+ {inputs[field].map((value, idx) => (
+ <input
+ key={idx}
+ type="text"
+ inputMode="decimal"
+ className="psm-val-advanced-input"
+ value={fmtRate(value)}
+ onChange={(e) => {
+ const num = parseRateInput(e.target.value);
+ if (num !== null) updateArray(field, idx, num);
+ }}
+ />
+ ))}
+ </div>
+ ))}
+ </div>
+ </div>
+
+ <div className="psm-val-advanced-result">
+ <span className="psm-val-advanced-result-label">Advanced Intrinsic Value</span>
+ <span className={`psm-val-advanced-result-price${isInvalid ? " invalid" : ""}`}>
+ {isInvalid
+ ? "Invalid assumptions"
+ : price != null
+ ? fmtCurrency(price, 2, currency)
+ : computed && "error" in computed
+ ? computed.error
+ : "—"}
+ </span>
+ </div>
+ </div>
+ );
+} \ No newline at end of file