summaryrefslogtreecommitdiff
path: root/frontend/lib/overview.ts
blob: db300504737acd3df96614157db61ff62aeb3573 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { fmtCurrency, fmtLarge, fmtNumber, fmtPct } from "@/lib/format";
import type { MarketIndex, Signal, TickerOverview, WatchlistItem } from "@/types/api";

export type KpiItem = {
  key: string;
  value: string;
  sublabel: string;
  missing?: boolean;
};

export type NavItem = {
  key: string;
  label: string;
  icon: string;
  disabled?: boolean;
};

export const OVERVIEW_NAV_ITEMS: NavItem[] = [
  { key: "overview", label: "Overview", icon: "chart" },
  { key: "financials", label: "Financials", icon: "ledger", disabled: true },
  { key: "valuation", label: "Valuation", icon: "dollar", disabled: true },
  { key: "options", label: "Options", icon: "window", disabled: true },
  { key: "insiders", label: "Insiders", icon: "pulse", disabled: true },
  { key: "filings", label: "Filings", icon: "folder", disabled: true },
  { key: "news", label: "News", icon: "terminal", disabled: true }
];

export function buildIdentityLine(overview: TickerOverview): string {
  const parts = [overview.profile.sector, overview.profile.industry, overview.profile.exchange].filter(Boolean);
  return parts.length ? parts.join(" · ") : "Profile details unavailable";
}

export function buildKpis(overview: TickerOverview): KpiItem[] {
  return [
    {
      key: "Market Cap",
      value: fmtLarge(overview.stats.market_cap),
      sublabel: `${fmtNumber(overview.stats.volume, 0)} volume`,
      missing: overview.stats.market_cap == null
    },
    {
      key: "P / E",
      value: overview.stats.trailing_pe == null ? "Unavailable" : `${fmtNumber(overview.stats.trailing_pe)}x`,
      sublabel: `EPS ${fmtCurrency(overview.stats.trailing_eps)}`,
      missing: overview.stats.trailing_pe == null
    },
    {
      key: "EPS · TTM",
      value: fmtCurrency(overview.stats.trailing_eps),
      sublabel: `Prev close ${fmtCurrency(overview.quote.prev_close)}`,
      missing: overview.stats.trailing_eps == null
    },
    {
      key: "52W Position",
      value: formatRangePosition(overview),
      sublabel: `${fmtCurrency(overview.range_52w.low)} to ${fmtCurrency(overview.range_52w.high)}`,
      missing: rangePercent(overview) == null
    },
    {
      key: "Short Float",
      value: fmtPct(overview.short_interest.short_percent_of_float),
      sublabel: `${fmtNumber(overview.short_interest.short_ratio)} days cover`,
      missing: overview.short_interest.short_percent_of_float == null
    },
    {
      key: "Beta",
      value: fmtNumber(overview.stats.beta),
      sublabel: `Avg vol ${fmtNumber(overview.stats.average_volume, 0)}`,
      missing: overview.stats.beta == null
    }
  ];
}

export function formatRangePosition(overview: TickerOverview): string {
  const pct = rangePercent(overview);
  if (pct == null) return "Unavailable";
  return `${pct.toFixed(0)}%`;
}

export function rangePercent(overview: TickerOverview): number | null {
  const low = overview.range_52w.low;
  const high = overview.range_52w.high;
  const price = overview.range_52w.price ?? overview.quote.price;
  if (low == null || high == null || price == null || high <= low) return null;
  return Math.max(0, Math.min(100, ((price - low) / (high - low)) * 100));
}

export function unavailableFields(overview: TickerOverview): string[] {
  return Object.entries(overview.meta.field_availability)
    .filter(([, available]) => !available)
    .map(([field]) => field);
}

export function availableFieldSummary(overview: TickerOverview): string {
  const fields = Object.values(overview.meta.field_availability);
  if (!fields.length) return "Availability metadata unavailable";
  const available = fields.filter(Boolean).length;
  return `${available}/${fields.length} tracked fields available`;
}

export function watchlistSubtitle(item: WatchlistItem): string {
  return new Date(item.created_at).toLocaleDateString(undefined, {
    month: "short",
    day: "numeric"
  });
}

export function sortIndices(indices: MarketIndex[]): MarketIndex[] {
  return [...indices].slice(0, 4);
}

export function signalTone(signal: Signal["state"]): "pos" | "warn" | "neg" | "neu" {
  return signal;
}

export function marketClock(now = new Date()) {
  const eastern = new Date(now.toLocaleString("en-US", { timeZone: "America/New_York" }));
  const day = eastern.getDay();
  const minutes = eastern.getHours() * 60 + eastern.getMinutes();
  const isWeekday = day >= 1 && day <= 5;
  const open = isWeekday && minutes >= 570 && minutes < 960;

  return {
    isOpen: open,
    status: open ? "US Market Open" : "US Market Closed",
    time: new Intl.DateTimeFormat("en-US", {
      hour: "numeric",
      minute: "2-digit",
      timeZone: "America/New_York",
      timeZoneName: "short"
    }).format(now)
  };
}